Designing a Modular, Scalable Android Player Architecture at Tubi
This article describes how Tubi's Android team redesigned their video player with a modular, distributed architecture—introducing PlayerContext, PlayerHandlerWrapper, and composable modules for ads, event tracking, and TTS—to improve flexibility, testability, and scalability while sharing lessons learned from the refactoring process.
Background
About four years ago, Tubi's Android team completed a major player refactor that prioritized flexibility and scalability. They built a data‑driven MVP (Model‑View‑Presenter) layered architecture for the then‑simple full‑screen playback scenario.
Adding new features made the old player architecture complex and tangled
After the refactor, a feature that switched between full‑screen and an in‑app mini‑player exposed shortcomings in the framework, forcing the team to add several global classes and static methods. As more globals accumulated, new code deviated from the original architecture, increasing coupling and making the codebase harder to maintain.
Without timely small‑scale refactors, the framework grew increasingly complex and bloated, eroding the original flexibility and extensibility, which led to the planning of a new all‑scenario Android player architecture.
Software change management
Fred Brooks famously said that handling change is one of the fundamental complexities of software development. Software development is more like gardening than building—a system left untouched quickly becomes overrun with weeds. These challenges motivated the new round of refactoring.
New player architecture
The new design introduces core components such as PlayerContext and PlayerHandlerWrapper to organize business logic.
PlayerContext
PlayerContext holds fields such as business state, external dependencies, and public methods. Inspired by Kotlin’s CoroutineContext , the team groups related fields into multiple PlayerContext instances, allowing each module to declare its own context and select the appropriate one for a given playback scenario.
PlayerHandler and PlayerHandlerWrapper
A base Core PlayerHandler implements basic initialization and playback functions and exposes interfaces for overrides. PlayerHandlerWrapper extends this core via overridden fields and methods, enabling custom creation, release, and event‑callback behavior. Kotlin extensions are used to create and manage wrapper instances, and wrappers are linked in a chain so each focuses on a specific concern while cooperating with others.
New player architecture principles
Distributed solution replaces the old centralized architecture.
Composition over inheritance.
Modular design.
Distributed solution replaces the old centralized architecture
As playback scenarios multiplied, the monolithic system became unsustainable. Tubi replaced it with a distributed solution that isolates new features and avoids conflicts across scenes.
Composition over inheritance
The core PlayerHandler provides a basic playback environment, while PlayerHandlerWrapper modules add discrete functionality. Dozens of modules have been implemented, each focusing on a single aspect that can be combined as needed.
New player ad implementation
During player initialization, an advertising module and an event‑tracking module play key roles.
Ad module : injects components such as AdsFetcher and UIController to manage ad retrieval and UI.
Event tracking module : sets up trackers like AdsEventTracker and QOSEventTracker to report ad impressions, user interactions, buffering, and latency metrics.
During playback, the ad and tracking modules cooperate to ensure efficient ad integration and comprehensive event monitoring.
TTS feature example (pseudo‑code)
// 1. Provide ability to read text.
class TTSSpeaker : PlayerContext {
fun speak(text : String)
}
// 2. Monitor view focus changes
class TTSHandlerWrapper : PlayerHandlerWrapper {
override fun attachPlayer(player : Player, playerView : PlayerView){
playerView.onFocusChangeListener { view ->
// Read text and track event.
TTSSpeakerContext.speak(view.text)
EventTrackerContext.trackTTSEvent()
}
}
}
// 3. Declare core PlayerHandler and add TTSSpeaker
val corePlayerHandler = PlayerHandler.create()
corePlayerHandler.append(TTSSpeaker)
// 4. Wrap with TTSHandlerWrapper to enable TTS
val playerHandlerWithTTS = TTSHandlerWrapper(corePlayerHandler)Error handling module
The module uses a chain‑of‑responsibility pattern so each error handler is independent and extensible; new error types can be added without modifying existing handlers.
Advantages of the new player architecture
Unit testing is simplified because each module can be tested in isolation.
Code readability improves thanks to a decentralized structure.
Feature extension is easy without disturbing existing functionality.
UI changes can be made quickly.
The ad module is isolated from the core framework, enhancing flexibility and maintainability.
Refactoring process
Refactoring the player was complex; despite a solid underlying architecture, the team faced device compatibility issues and insufficient exception handling that only surfaced after large‑scale deployment. After five iterations, key business metrics improved, and the team added monitoring to guard against recurring pitfalls.
Conclusion
Four years after the previous player rebuild, Tubi's Android team delivered a new modular architecture that restores flexibility and scalability, improves unit testing, code readability, robustness, UI adaptability, and isolates the ad library for a more reliable playback experience.
We are hiring!
If you are interested in high‑impact, large‑scale projects, feel free to join Tubi.
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.