Design and Architecture of the Tubi Web Player: A Modular Frontend Solution
Facing rapid growth and the need to support dozens of OTT platforms, Tubi’s frontend team redesigned its web player into an independent, TypeScript‑driven module with a clear Adapter‑Player‑Action‑Reducer architecture, monorepo management, comprehensive testing and documentation, enabling higher code quality and a better cross‑platform playback experience.
Tubi experienced rapid business growth, which introduced challenges in maintaining a single code repository for over a dozen OTT platforms; the existing player became tightly coupled with business logic, lacked clear interface definitions, and suffered from low code quality and insufficient testing.
To address these issues, the team set clear goals: create an independent, focused module with a simple, platform‑agnostic API; establish a clean, layered architecture; ensure comprehensive unit, integration, and E2E testing; and provide detailed documentation to improve developer efficiency.
The codebase was reorganized into a monorepo managed by Lerna, with the following structure:
<span>.</span>
<span>├── lerna.json</span>
<span>├── package.json</span>
<span>├── packages</span>
<span>│ └── player</span>
<span>└── src</span>TypeScript is used throughout the project, providing static type checking, autocompletion, and enabling the generation of documentation via TypeDoc.
The architecture consists of four layers:
Adapter : platform‑specific adapters (e.g., HLS, WebMAF, AVPlay) implement a common Adapter interface, exposing a unified set of methods to the upper layers.
// types.ts
interface Adapter extends EventEmitter {
setup(): Promise<void>;
getState(): State;
getPosition(): number;
getDuration(): number;
play(): void;
pause(): void;
seek(offset: number): void;
remove(): void;
isAd(): boolean;
playAd(tag: string): void;
setQuality?(index: number): void;
...
}
// HlsAdapter.ts
class HlsAdapter extends EventEmitter implements Adapter {
play() {
this.emit(PLAYER_EVENTS.play);
this.videoElement.play();
}
...
}
// WebMAFAdapter.ts
class WebMAFPlayerAdapter extends EventEmitter implements Adapter {
play() {
this.emit(PLAYER_EVENTS.play);
webMAF.nativeCommand('play');
}
...
}Player : a thin layer that forwards high‑level calls to the appropriate adapter and emits events for the application.
class Player {
play(): Promise<void> { return this.adapter.play(); }
seek(position: number) { this.adapter.seek(position); }
...
}Action & Reducer : Redux is used to manage playback state; actions invoke player methods, and reducers store the resulting state.
// reducer.ts
const initialState: PlayerStoreState = {
playerState: State.idle,
contentType: PLAYER_CONTENT_TYPE.video,
seekRate: -1,
progress: { position: 0, duration: 0, bufferPosition: 0, isBuffering: false },
quality: { qualityList: [], qualityIndex: 0, isHD: false },
...
};
// action example
getPlayerInstance().on(PLAYER_EVENTS.play, () => {
dispatch(transit(State.playing, { contentType: PLAYER_CONTENT_TYPE.video }));
});
const controlActions = {
seek: (position: number) => (dispatch, getState) => createTimeBoundedPromise(
(resolve) => {
const { progress: { duration } } = getState().player;
const targetPosition = clamp(position, 0, duration || Number.POSITIVE_INFINITY);
getPlayerInstance().once(PLAYER_EVENTS.seeked, resolve);
getPlayerInstance().seek(targetPosition);
return { destroy: () => getPlayerInstance().removeListener(PLAYER_EVENTS.seeked, resolve) };
}
),
...
};An integration example shows a React component ( WebPlayerOverlay) that connects to the Redux store, renders a progress bar and player controls, and uses the seek action to update playback position.
// WebPlayerOverlay.ts
class WebPlayerOverlay extends Component<Props, State> {
seek(position) {
const { dispatch, trackEvent, video: { id } } = this.props;
return dispatch(controlActions.seek(position))
.then(() => {
this.refreshActiveTimer();
if (trackEvent) { trackEvent(eventTypes.SEEK, position, id); }
});
}
render() {
return (
<div className={styles.webPlayerOverlay}>
<ProgressBar bufferPosition={bufferPosition} duration={duration} isAd={isAd} position={position} seek={this.seek} />
<PlayerControls ... />
</div>
);
}
}The new implementation has already improved development focus, code quality, and the ability to fine‑tune platform‑specific parameters without regressions. Future work includes adding DASH support, cross‑platform DRM, a lightweight MSE wrapper, state‑machine‑driven playback state transitions, and expanded E2E testing.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Bitu Technology
Bitu Technology is the registered company of Tubi's China team. We are engineers passionate about leveraging advanced technology to improve lives, and we hope to use this channel to connect and advance together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
