Media Playback and UI Controls
This module provides the core media playback capabilities paired with rich user interface controls to deliver an interactive video viewing experience. It integrates adaptive streaming playback using ExoPlayer and exposes user interactions such as play/pause toggling, seeking, subtitle management, and ad-related UI feedback through data-binding-enabled controllers.
Overview
The **Media Playback and UI Controls** module focuses on managing the media player lifecycle, rendering video and subtitles, and providing a responsive UI layer tightly coupled with playback state. It addresses the need for:
Adaptive and flexible media playback supporting multiple streaming formats.
Clear separation of playback logic and user interaction.
Seamless synchronization between media state changes and UI updates.
Subtitle rendering and toggle functionality.
Handling ad playback metadata and user interactions (e.g., "Learn More" clicks).
Managing playback progress, buffering, and error states.
Efficient user controls visibility and interaction patterns.
This module is designed to be extensible for various playback activities and UI designs, supporting both content and ad playback scenarios.
Key Components and Workflows
1. Base Playback Activity (TubiPlayerActivity)
The `TubiPlayerActivity` class is an abstract base activity that encapsulates the setup and lifecycle management of a single ExoPlayer instance intended for content playback. It provides foundational functionality, including:
Parsing incoming media metadata (
MediaModel) from intents.Initializing ExoPlayer with adaptive track selection and bandwidth metering.
Building appropriate media sources based on streaming type (DASH, HLS, SmoothStreaming, or other).
Integrating subtitle sources into playback via
MergingMediaSource.Managing ExoPlayer lifecycle events (start, resume, pause, stop) to allocate and release resources properly.
Abstract hooks (
addUserInteractionView(),onPlayerReady(),updateResumePosition(),isCaptionPreferenceEnable()) for subclasses to customize UI additions and respond to player readiness or state persistence.
This class also provides methods to access the playback controller interface (`TubiPlaybackControlInterface`) via the player view.
protected void initMoviePlayer() {
mMainHandler = new Handler();
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
mTrackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
mMoviePlayer = ExoPlayerFactory.newSimpleInstance(this, mTrackSelector);
mEventLogger = new EventLogger(mTrackSelector);
mMoviePlayer.addAnalyticsListener(mEventLogger);
mMoviePlayer.addMetadataOutput(mEventLogger);
mTubiPlayerView.setPlayer(mMoviePlayer, this);
mTubiPlayerView.setMediaModel(mediaModel);
}
2. Custom Player View (TubiExoPlayerView)
`TubiExoPlayerView` is a custom view component that hosts the ExoPlayer video surface, subtitles, and user interaction controls. It encapsulates:
Video rendering surface (
SurfaceVieworTextureView) managed inside anAspectRatioFrameLayoutfor correct aspect ratio handling.A
SubtitleViewconfigured with styling (font, color, background) that can adapt to device types (e.g., larger text size on TV devices).A placeholder for user interaction controls, allowing dynamic injection of custom UI views for playback controls.
Direct binding to a
SimpleExoPlayerinstance, setting video listeners, text output, and lifecycle event listeners.Delegation of playback control logic to an internal
UserControllerinstance that handles user commands and player event responses.
This view acts as the primary UI container for video playback and controls.
public void setPlayer(SimpleExoPlayer player, @NonNull PlaybackActionCallback playbackActionCallback) {
// Setup player listeners and assign video surface
this.player = player;
if (userController != null) {
userController.setPlayer(player, playbackActionCallback, this);
}
// Additional setup omitted for brevity
}
3. User Interaction Controller (UserController)
`UserController` serves as the bridge between the ExoPlayer playback engine and the UI layer. It is a `BaseObservable` class enabling data binding with UI components. Its responsibilities include:
Exposing observable fields representing playback state (play/pause), video metadata (title, poster, duration, current time), subtitles status, and ad-related information.
Handling playback commands such as play/pause toggling, seek forward/rewind, subtitle toggling, and "Learn More" ad clicks.
Listening to ExoPlayer events to update observable fields, ensuring UI reflects accurate playback state changes.
Managing seek bar interaction events to synchronize manual user seeking with player position updates.
Coordinating with a
PlaybackActionCallbackinterface to notify higher-level components about user actions (e.g., seeking, play toggling).
This class abstracts player control logic and maintains UI state consistency.
public void togglePlayPause() {
triggerPlayOrPause(!isVideoPlayWhenReady.get());
}
@Override
public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
playerPlaybackState.set(playbackState);
isVideoPlayWhenReady.set(playWhenReady);
updateProgress();
}
4. UI Playback Controller View (UIControllerView)
`UIControllerView` is a `FrameLayout` subclass that hosts the actual playback controls UI, which is bound to a `UserController` instance. Key features include:
Using Android Data Binding (
UiControllerViewBinding) to link UI components with observable properties inUserController.Managing visibility of playback controls with a timeout mechanism to auto-hide controls after user inactivity.
Handling touch events to toggle control panel visibility, enhancing user experience by reducing UI clutter when not interacting.
This view is designed to be embedded into player layouts to provide a ready-to-use control interface.
@Override
public boolean onTouchEvent(final MotionEvent event) {
countdownHandler.removeCallbacks(hideUIAction);
// Toggle visibility on user touch
if (binding.controllerPanel.getVisibility() == VISIBLE) {
binding.controllerPanel.setVisibility(GONE);
} else {
if (userController.playerPlaybackState.get() != Player.STATE_IDLE) {
binding.controllerPanel.setVisibility(VISIBLE);
hideUiTimeout();
}
}
return super.onTouchEvent(event);
}
5. Player UI Controller (PlayerUIController)
This class manages the relationship between the content player and ad player instances along with VPAID WebView integration and playback state history. It provides:
References to ExoPlayer instances for content and ads, distinguishing cases where a single player instance is reused for both.
Management of resume positions for both movie content and ads, allowing playback to continue from saved points.
Facilities to manage the player UI state indicating whether ads are currently playing.
This controller enables smooth transitions and state preservation between content and ad playback.
public SimpleExoPlayer getAdPlayer() {
if (PlayerDeviceUtils.useSinglePlayer()) {
return contentPlayer;
}
return adPlayer;
}
public void setPlayFromHistory(long pos) {
hasHistory = true;
historyPosition = pos;
}
Interactions Between Components
The Activity (
TubiPlayerActivityor subclass) initializes the playback environment by creating an ExoPlayer instance and associating it with theTubiExoPlayerView.The
TubiExoPlayerViewreceives the player instance and manages rendering and subtitle display, delegating playback control logic to theUserController.The
UserControllerexposes observable data properties to the UI, which are bound in theUIControllerView. It listens to player events and user inputs to update playback state and send commands to the player.The
PlayerUIControllermanages dual player instances (content/ad) and WebView for VPAID ads, maintaining playback state, resuming positions, and feeding relevant state info to the UI controllers.UI components, bound through data binding, react automatically to changes in
UserController, updating controls such as play/pause buttons, seek bars, and subtitle toggles.User interactions such as seeking or toggling subtitles invoke methods on
UserController, which relays commands to the ExoPlayer and triggers callbacks to the hosting activity for additional handling.
Design Patterns and Concepts
Model-View-ViewModel (MVVM): The
UserControlleracts as a ViewModel exposing observable properties that the UI binds to, keeping UI and business logic loosely coupled.Observer Pattern: Playback state changes in ExoPlayer trigger event listeners in
UserController, which update observables and notify bound UI elements.Builder Pattern:
PlayerUIController.Builderenables flexible construction of controller instances with optional components like ad players or WebView.Separation of Concerns: Playback engine setup, UI rendering, user interaction handling, and ad state management are cleanly separated into distinct classes.
Adaptive Streaming Handling: Media source construction in
TubiPlayerActivityadapts to different streaming protocols, ensuring smooth playback across formats.
Code Snippets Illustrating Core Concepts
Media Source Construction Based on Streaming Type
protected MediaSource buildMediaSource(MediaModel model) {
int type = TextUtils.isEmpty(model.getMediaExtension()) ? Util.inferContentType(model.getVideoUrl())
: Util.inferContentType("." + model.getMediaExtension());
switch (type) {
case C.TYPE_SS:
return new SsMediaSource(model.getVideoUrl(), buildDataSourceFactory(false),
new DefaultSsChunkSource.Factory(mMediaDataSourceFactory), mMainHandler, mEventLogger);
case C.TYPE_DASH:
return new DashMediaSource(model.getVideoUrl(), buildDataSourceFactory(false),
new DefaultDashChunkSource.Factory(mMediaDataSourceFactory), mMainHandler, mEventLogger);
case C.TYPE_HLS:
return new HlsMediaSource(model.getVideoUrl(), mMediaDataSourceFactory, mMainHandler,
mEventLogger);
case C.TYPE_OTHER:
return new ExtractorMediaSource(model.getVideoUrl(), mMediaDataSourceFactory,
new DefaultExtractorsFactory(), mMainHandler, mEventLogger);
default:
throw new IllegalStateException("Unsupported type: " + type);
}
}
Playback State Update and UI Synchronization in UserController
@Override
public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
playerPlaybackState.set(playbackState);
isVideoPlayWhenReady.set(playWhenReady);
updateProgress();
}
User Interaction Handling for SeekBar
@Override
public void onStartTrackingTouch(final SeekBar seekBar) {
isDraggingSeekBar.set(true);
}
@Override
public void onStopTrackingTouch(final SeekBar seekBar) {
if (mPlayer != null) {
seekTo(Utils.progressToMilli(mPlayer.getDuration(), seekBar));
}
isDraggingSeekBar.set(false);
}
Mermaid Diagram: Media Playback and UI Interaction Flow
sequenceDiagram
participant Activity as TubiPlayerActivity
participant PlayerView as TubiExoPlayerView
participant UserCtrl as UserController
participant ExoPlayer as SimpleExoPlayer
participant UI as UIControllerView (Data Binding)
Activity->>PlayerView: setPlayer(ExoPlayer instance)
PlayerView->>UserCtrl: setPlayer(ExoPlayer, PlaybackCallback)
UserCtrl->>ExoPlayer: register event listeners
ExoPlayer-->>UserCtrl: playback events (state changes, position)
UserCtrl->>UI: update observable fields (playback state, position)
UI->>UserCtrl: user interactions (play/pause, seek, toggle subtitles)
UserCtrl->>ExoPlayer: control playback accordingly
UserCtrl->>Activity: notify user actions via PlaybackActionCallback
This detailed explanation clarifies how media playback and user interface controls are structured and operate cohesively within the module, highlighting their roles, interactions, and design principles. The integration of adaptive streaming, data binding, and player event handling enables a responsive and maintainable playback experience.