Contents
- 1 Video Controls implementation with MediaSession
- 2 Implement requestVisibleBehind
- 3 Class structure of this chapter
- 4 Create & release MediaSession
- 5 Video control functions
- 6 Video control by remote controller key
- 7 Video control from UI – MediaController.getTransportControls
- 8 Updating UI of VideoDetailsFragment
- 9 Updating video control icons in onPlaybackStateChanged
- 10 update Media information in onMetadataChanged
- 11 Build and run
- 12 Now Playing Card
Video Controls implementation with MediaSession
The sample implementation is done in Google’s latest sample Android TV application. AOSP sample application implementation does not have MediaSession implementation yet (in API 21, 22).
In previous chapter, I explained that following Video controls are needed to be impmented.
- Action’s UI update part (done in previous chapter)
- Video control part (done in previous chapter) MediaSession implementation, Video control via MediaController’s TransportControls (this chapter)
- – MediaSession can handle the action when user presses TV remote controller’s video control button.
– It allows other activity to inherite video control. Especially LeanbackLauncher, Home display, can play video in background. - set MediaMetadata to MediaSession (this chapter)
– “Now playing card” will appear at the top of recommendation row.
In this chapter, we proceed to implement Video controls using MediaSession. We can pass VideoView control to LeanbackLauncher by using MediaSession, which results to achieve playing video background in LeanbackLauncher.
For 3, we create MediaSession in PlaybackOverlayActivity, and control it from MediaController in PlaybackOverlayFragment.
For 4, MediaSession’s Metadata is updated using MediaMetadata & PlaybackState class to update “Now playing card”.
I recommend you to read Displaying a Now Playing Card, for official explanation.
This chapter’s implementation is almost independent with previous chapter’s implementation. Before implementing MediaSession, I will implement requestVisibleBehind method, so that we can play video in background of LeanbackLauncher app.
Implement requestVisibleBehind
This method is added in API 21 (Lolipop), AOSP explanation explains that
If this call is successful then the activity will remain visible after onPause() is called, and is allowed to continue playing media in the background.
Sample implementation is as follows.
@Override public void onPause() { super.onPause(); if (!requestVisibleBehind(true)) { // Try to play behind launcher, but if it fails, stop playback. mPlaybackController.playPause(false); } }
After this implementation, when you play video contents in your application and press “Home” key to go back LeanbackLauncher, the video is remain playing in background. .
Class structure of this chapter
We handle 3 classes in this chapter.
- PlaybackOverlayActivity
– manage lifecycle, pass intent information to PlaybackController
– MediaSession’s life time is linked with activity - PlaybackOverlayFragment
– handle UI of PlaybackControlsRow
– MediaController Callback function is used to update UI according to the current playback state - PlaybackController
– manage video playback
– video control functions
– MediaSessionCallback for receiving Video control key from TV remote controller
Create & release MediaSession
So far, we cannot control this video by using video control key in remote controller. Let’s implement MediaSession to define the action for each video control key in remote controller. At first, we create MediaSession in Constructor of PlaybackController, which is invoked by PlaybackOverlayActivity.
public PlaybackController(Activity activity) { mActivity = activity; // mVideoView = (VideoView) activity.findViewById(VIDEO_VIEW_RESOURCE_ID); createMediaSession(mActivity); } private void createMediaSession(Activity activity) { if (mSession == null) { mSession = new MediaSession(activity, MEDIA_SESSION_TAG); mMediaSessionCallback = new MediaSessionCallback(); mSession.setCallback(mMediaSessionCallback); mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); mSession.setActive(true); activity.setMediaController(new MediaController(activity, mSession.getSessionToken())); } }
We can set callback, MediaSessionCallback, to MediaSession. It defines the precise behavior of each video control button which will be explained later.
setFlags method with the argument FLAG_HANDLES_MEDIA_BUTTONS & FLAG_HANDLES_TRANSPORT_CONTROLS are necessary to enable remote controller key to control video.
After create, we must release MediaSession when it finishes.
public void releaseMediaSession() { if(mSession != null) { mSession.release(); } }
Video control functions
Video control functions are implemented in MediaSessionCallback class. As name suggests, each video control action is implemented in corresponding callback function. This callback is called from 2 ways, which is “remote controller media key” or “UI video control button in PlaybackControlsRow”.
public void playPause(boolean doPlay) { if (mCurrentPlaybackState == PlaybackState.STATE_NONE) { /* Callbacks for mVideoView */ setupCallbacks(); } //if (doPlay && mCurrentPlaybackState != PlaybackState.STATE_PLAYING) { if (doPlay) { // Play Log.d(TAG, "playPause: play"); if(mCurrentPlaybackState == PlaybackState.STATE_PLAYING) { /* if current state is already playing, do nothing */ return; } else { mCurrentPlaybackState = PlaybackState.STATE_PLAYING; mVideoView.start(); mStartTimeMillis = System.currentTimeMillis(); } } else { // Pause Log.d(TAG, "playPause: pause"); if(mCurrentPlaybackState == PlaybackState.STATE_PAUSED) { /* if current state is already paused, do nothing */ return; } else { mCurrentPlaybackState = PlaybackState.STATE_PAUSED; } setPosition(mVideoView.getCurrentPosition()); mVideoView.pause(); } updatePlaybackState(); } public void fastForward() { if (mDuration != -1) { // Fast forward 10 seconds. setPosition(getCurrentPosition() + (10 * 1000)); mVideoView.seekTo(mPosition); } } public void rewind() { // rewind 10 seconds setPosition(getCurrentPosition() - (10 * 1000)); mVideoView.seekTo(mPosition); } private class MediaSessionCallback extends MediaSession.Callback { @Override public void onPlay() { playPause(true); } @Override public void onPause() { playPause(false); } @Override public void onSkipToNext() { if (++mCurrentItem >= mItems.size()) { // Current Item is set to next here mCurrentItem = 0; } Movie movie = mItems.get(mCurrentItem); //Movie movie = VideoProvider.getMovieById(mediaId); if (movie != null) { setVideoPath(movie.getVideoUrl()); //mCurrentPlaybackState = PlaybackState.STATE_PAUSED; //updateMetadata(movie); updateMetadata(); playPause(mCurrentPlaybackState == PlaybackState.STATE_PLAYING); } else { Log.e(TAG, "onSkipToNext movie is null!"); } } @Override public void onSkipToPrevious() { if (--mCurrentItem < 0) { // Current Item is set to previous here mCurrentItem = mItems.size()-1; } Movie movie = mItems.get(mCurrentItem); //Movie movie = VideoProvider.getMovieById(mediaId); if (movie != null) { setVideoPath(movie.getVideoUrl()); //mCurrentPlaybackState = PlaybackState.STATE_PAUSED; updateMetadata(); playPause(mCurrentPlaybackState == PlaybackState.STATE_PLAYING); } else { Log.e(TAG, "onSkipToPrevious movie is null!"); } } @Override public void onPlayFromMediaId(String mediaId, Bundle extras) { mCurrentItem = Integer.parseInt(mediaId); Movie movie = mItems.get(mCurrentItem); //Movie movie = VideoProvider.getMovieById(mediaId); if (movie != null) { setVideoPath(movie.getVideoUrl()); // mCurrentPlaybackState = PlaybackState.STATE_PAUSED; // updateMetadata(movie); updateMetadata(); playPause(mCurrentPlaybackState == PlaybackState.STATE_PLAYING); } } @Override public void onSeekTo(long pos) { setPosition((int) pos); mVideoView.seekTo(mPosition); updatePlaybackState(); } @Override public void onFastForward() { fastForward(); } @Override public void onRewind() { rewind(); } }
Video control by remote controller key
To enable video control from remote controller, we need to explicitly set available actions to MediaSession. Available actions are defined in PlaybackState class, sample implementation is below.
private void updatePlaybackState() { PlaybackState.Builder stateBuilder = new PlaybackState.Builder() .setActions(getAvailableActions()); int state = PlaybackState.STATE_PLAYING; if (mCurrentPlaybackState == PlaybackState.STATE_PAUSED || mCurrentPlaybackState == PlaybackState.STATE_NONE) { state = PlaybackState.STATE_PAUSED; } stateBuilder.setState(state, getCurrentPosition(), 1.0f); mSession.setPlaybackState(stateBuilder.build()); } private long getAvailableActions() { long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_REWIND | PlaybackState.ACTION_FAST_FORWARD | PlaybackState.ACTION_SKIP_TO_PREVIOUS | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | PlaybackState.ACTION_PLAY_FROM_SEARCH; return actions; }
In this example, available actions are decided in getAvailableActions
method, by adding actions using logical disjunction.
Video control from UI – MediaController.getTransportControls
To control MediaSession from PlaybackControlsRow in VideoDetailsFragment, we use MediaController. The MediaController is created in the constructor of PlaybackController and it owns token of MediaSession.
When user clicks video control button, it will invoke MediaSessionCallback method using MediaController.getTransportControls() method.
/* onClick */ playbackControlsRowPresenter.setOnActionClickedListener(new OnActionClickedListener() { public void onActionClicked(Action action) { if (action.getId() == mPlayPauseAction.getId()) { /* PlayPause action */ if (mPlayPauseAction.getIndex() == PlaybackControlsRow.PlayPauseAction.PLAY) { mMediaController.getTransportControls().play(); } else if (mPlayPauseAction.getIndex() == PlaybackControlsRow.PlayPauseAction.PAUSE) { mMediaController.getTransportControls().pause(); } } else if (action.getId() == mSkipNextAction.getId()) { /* SkipNext action */ mMediaController.getTransportControls().skipToNext(); } else if (action.getId() == mSkipPreviousAction.getId()) { /* SkipPrevious action */ mMediaController.getTransportControls().skipToPrevious(); } else if (action.getId() == mFastForwardAction.getId()) { /* FastForward action */ mMediaController.getTransportControls().fastForward(); } else if (action.getId() == mRewindAction.getId()) { /* Rewind action */ mMediaController.getTransportControls().rewind(); } }
Video control part is done. However, we need to update UI of PlayControlsRow depending according to the video control.
Updating UI of VideoDetailsFragment
We need to change the UI when video control action has executed, and video playback status has changed. We can get this event by using MediaController’s callback function. 2 callback methods are introduced in the following.
- onPlaybackStateChanged
- onMetadataChanged
To use these callback methods, you can make subclass which extends MediaController.Callback class, and override these methods. To use this class, we can call MediaController’s registerCallback/unregisterCallback method to get event of MediaController.
@Override public void onAttach(Activity activity) { super.onAttach(activity); mMediaController = getActivity().getMediaController(); Log.d(TAG, "register callback of mediaController"); if(mMediaController == null){ Log.e(TAG, "mMediaController is null"); } mMediaController.registerCallback(mMediaControllerCallback); } @Override public void onDetach() { if (mMediaController != null) { Log.d(TAG, "unregister callback of mediaController"); mMediaController.unregisterCallback(mMediaControllerCallback); } super.onDetach(); } private class MediaControllerCallback extends MediaController.Callback { @Override public void onPlaybackStateChanged(final PlaybackState state) { Log.d(TAG, "playback state changed: " + state.toString()); } @Override public void onMetadataChanged(final MediaMetadata metadata) { Log.d(TAG, "received update of media metadata"); } }
Updating video control icons in onPlaybackStateChanged
Update PlaybackState
PlaybackState is updated in PlaybackController.
private void updatePlaybackState() { PlaybackState.Builder stateBuilder = new PlaybackState.Builder() .setActions(getAvailableActions()); int state = PlaybackState.STATE_PLAYING; if (mCurrentPlaybackState == PlaybackState.STATE_PAUSED || mCurrentPlaybackState == PlaybackState.STATE_NONE) { state = PlaybackState.STATE_PAUSED; } // stateBuilder.setState(state, mPosition, 1.0f); stateBuilder.setState(state, getCurrentPosition(), 1.0f); mSession.setPlaybackState(stateBuilder.build()); }
For example, it will be called in playPause method. When user start play video state will change from STATE_PLAYING to STATE_PAUSED, or vice versa. PlaybackState update is set (notified) to MediaSession.
Callback
When PlaybackState has changed by setPlaybackState above, this event can be received with onPlaybackStateChanged callback. We can update play/pause icon in PlaybackControlsRow.
private class MediaControllerCallback extends MediaController.Callback { @Override public void onPlaybackStateChanged(final PlaybackState state) { Log.d(TAG, "playback state changed: " + state.toString()); mHandler.post(new Runnable() { @Override public void run() { if (state.getState() == PlaybackState.STATE_PLAYING) { mPlaybackController.setCurrentPlaybackState(PlaybackState.STATE_PLAYING); startProgressAutomation(); // setFadingEnabled(false); mPlayPauseAction.setIndex(PlaybackControlsRow.PlayPauseAction.PAUSE); mPlayPauseAction.setIcon(mPlayPauseAction.getDrawable(PlaybackControlsRow.PlayPauseAction.PAUSE)); notifyChanged(mPlayPauseAction); } else if (state.getState() == PlaybackState.STATE_PAUSED) { mPlaybackController.setCurrentPlaybackState(PlaybackState.STATE_PAUSED); // setFadingEnabled(false); mPlayPauseAction.setIndex(PlaybackControlsRow.PlayPauseAction.PLAY); mPlayPauseAction.setIcon(mPlayPauseAction.getDrawable(PlaybackControlsRow.PlayPauseAction.PLAY)); notifyChanged(mPlayPauseAction); } int currentTime = (int) state.getPosition(); mPlaybackControlsRow.setCurrentTime(currentTime); // mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME); mPlaybackControlsRow.setBufferedProgress(mPlaybackController.calcBufferedTime(currentTime)); } }); } ... }
update Media information in onMetadataChanged
Update MediaMetadata
MediaMetadata class is used to set metadata information of the video. We can set each attribute of metadata by using put method in MediaMetadata.Builder. Again, MediaMetadata update is set (notified) to MediaSession.
public void updateMetadata(Movie movie) { final MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder(); String title = movie.getTitle().replace("_", " -"); metadataBuilder.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Long.toString(movie.getId())); metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, title); metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, movie.getStudio()); metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_DESCRIPTION, movie.getDescription()); metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, movie.getCardImageUrl()); metadataBuilder.putLong(MediaMetadata.METADATA_KEY_DURATION, mDuration); // And at minimum the title and artist for legacy support metadataBuilder.putString(MediaMetadata.METADATA_KEY_TITLE, title); metadataBuilder.putString(MediaMetadata.METADATA_KEY_ARTIST, movie.getStudio()); Glide.with(mActivity) .load(Uri.parse(movie.getCardImageUrl())) .asBitmap() .into(new SimpleTarget<Bitmap>(500, 500) { @Override public void onResourceReady(Bitmap bitmap, GlideAnimation anim) { metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ART, bitmap); mSession.setMetadata(metadataBuilder.build()); } }); }
By setting correct MediaMetadata to MediaSession, Android TV will show Now Playing Card on LeanbackLauncher, which will be explained again later.
Callback
When MediaMetadata has changed by setMetadata above, this event can be received with onMetadataChanged callback. We can update item value of PlaybackControlsRow.
private class MediaControllerCallback extends MediaController.Callback { ... @Override public void onMetadataChanged(final MediaMetadata metadata) { Log.d(TAG, "received update of media metadata"); updateMovieView( metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE), metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE), metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI), metadata.getLong(MediaMetadata.METADATA_KEY_DURATION) ); } } private void updateMovieView(String title, String studio, String cardImageUrl, long duration) { Log.d(TAG, "updateMovieView"); if (mPlaybackControlsRow.getItem() != null) { Movie item = (Movie) mPlaybackControlsRow.getItem(); item.setTitle(title); item.setStudio(studio); } else { Log.e(TAG, "mPlaybackControlsRow.getItem is null!"); } mPlaybackControlsRow.setTotalTime((int) duration); mPlaybackControlsRow.setCurrentTime(0); mPlaybackControlsRow.setBufferedProgress(0); mRowsAdapter.notifyArrayItemRangeChanged(0, mRowsAdapter.size()); // Show the video card image if there is enough room in the UI for it. // If you have many primary actions, you may not have enough room. if (SHOW_IMAGE) { mPlaybackControlsRowTarget = new PicassoPlaybackControlsRowTarget(mPlaybackControlsRow); updateVideoImage(cardImageUrl); } }
Build and run
Now video playcontrol is possible from both clicking button on PlayControlsRow and pushing media key in remote controller. In the above video, I’m controlling video contents via remote controller and not using UI on the screen 🙂
Now Playing Card
If the MediaMetadata is correctly set to MediaSession, Now Playing Card will appear to the LeanbackLauncher (Home display). It notifies information of current playing media to user. Also, Now Playing Card enables user to go back to your app to control video (pause/go to next video etc).
Reference
Source code is on github.