Video Controls minimum implementation
– Android TV app hands on Tutorial 8

action_icon_change

Video Controls minimum implementation

Video is streamed in VideoView.

* I referred Google’s latest sample Android TV application. AOSP sample application implementation is defferent.

For Video controls, we have several stuff to explain.

  1. Action’s UI update part (this chapter)
  2. Video control part (this chapter)
  3. MediaSession implementation, Video control via MediaController’s TransportControls (next 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.  
  4. set MediaMetadata to MediaSession (next chapter)
    – “Now playing card” will appear at the top of recommendation row.

In this chapter, Video controls implementation is explained. Since Google’s sample application implements all 1~4, source code is bit long and difficult to understand for beginners. I did a minimum implementation for only 1~2. I will just explain these implementation for each part in this chapter, so please download & refer source code on github at first (I did a refactoring for using MovieProvider class to prepare movie contents). The Technic in this chapter is general in Android.

Next chapter I will explain about MediaSession implementation. We can pass VideoView control to LeanbackLauncher by using MediaSession, which results to achieve playing video background in LeanbackLauncher. 

VideoView handling 

PlaybackOverlayActivity need to have VideoView field variable “mVideoView” to control video.

public class PlaybackOverlayActivity extends Activity {

    private static final String TAG = PlaybackOverlayActivity.class.getSimpleName();

    private VideoView mVideoView;

    private LeanbackPlaybackState mPlaybackState = LeanbackPlaybackState.IDLE;

    private int mPosition = 0;
    private long mStartTimeMillis;
    private long mDuration = -1;

    /*
     * List of various states that we can be in
     */
    public enum LeanbackPlaybackState {
        PLAYING, PAUSED, IDLE
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_playback_overlay);

        loadViews();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        stopPlayback();
        mVideoView.suspend();
        mVideoView.setVideoURI(null);
    }

    private void loadViews() {
        mVideoView = (VideoView) findViewById(R.id.videoView);
        mVideoView.setFocusable(false);
        mVideoView.setFocusableInTouchMode(false);

        Movie movie = (Movie) getIntent().getSerializableExtra(DetailsActivity.MOVIE);
        setVideoPath(movie.getVideoUrl());

    }

    public void setVideoPath(String videoUrl) {
        setPosition(0);
        mVideoView.setVideoPath(videoUrl);
        mStartTimeMillis = 0;
        mDuration = Utils.getDuration(videoUrl);
    }

    private void stopPlayback() {
        if (mVideoView != null) {
            mVideoView.stopPlayback();
        }
    }

Implement setOnActionClickedListener & onActionClicked callback 

To assign action of each video control button, we use setOnActionClickedListener which is a method of PlaybackControlsRowPresenter.

    private void setUpRows() {

        ...
 
        /* add ListRow to second row of mRowsAdapter */
        addOtherRows();

        /* onClick */
        playbackControlsRowPresenter.setOnActionClickedListener(new OnActionClickedListener() {
            public void onActionClicked(Action action) {
                if (action.getId() == mPlayPauseAction.getId()) {
                    /* PlayPause action */
                    togglePlayback(mPlayPauseAction.getIndex() == PlaybackControlsRow.PlayPauseAction.PLAY);
                } else if (action.getId() == mSkipNextAction.getId()) {
                    /* SkipNext action */
                    next(mCurrentPlaybackState == PlaybackState.STATE_PLAYING);
                } else if (action.getId() == mSkipPreviousAction.getId()) {
                    /* SkipPrevious action */
                    prev(mCurrentPlaybackState == PlaybackState.STATE_PLAYING);
                } else if (action.getId() == mFastForwardAction.getId()) {
                    /* FastForward action  */
                    fastForward();
                } else if (action.getId() == mRewindAction.getId()) {
                    /* Rewind action */
                    rewind();
                }
                if (action instanceof PlaybackControlsRow.MultiAction) {
                    /* Following action is subclass of MultiAction
                     * - PlayPauseAction
                     * - FastForwardAction
                     * - RewindAction
                     * - ThumbsAction
                     * - RepeatAction
                     * - ShuffleAction
                     * - HighQualityAction
                     * - ClosedCaptioningAction
                     */
                    notifyChanged(action);
                }
            }
        });

        setAdapter(mRowsAdapter);

    }

From here, Each action’s implementation is explained. Note that it is important to differentiate “UI update part” and “Video control part”, because Video control part will move to MediaSession in next chapter.

In the source code, I implemented “UI update part” in PlaybackOverlayFragment.java, while “Video control part” is implemented in PlaybackOverlayActivity.java.

PlayPauseAction

    private void togglePlayback(boolean playPause) {
        /* Video control part */
        ((PlaybackOverlayActivity) getActivity()).playPause(playPause);

        /* UI control part */
        playbackStateChanged();
    }

Video control part will handle play/pause video in VideoView.

public void playPause(boolean doPlay) {
    if (mPlaybackState == LeanbackPlaybackState.IDLE) {
        /* Callbacks for mVideoView */
        setupCallbacks();
    }

    if (doPlay && mPlaybackState != LeanbackPlaybackState.PLAYING) {
        mPlaybackState = LeanbackPlaybackState.PLAYING;
        if (mPosition > 0) {
            mVideoView.seekTo(mPosition);
        }
        mVideoView.start();
        mStartTimeMillis = System.currentTimeMillis();
    } else {
        mPlaybackState = LeanbackPlaybackState.PAUSED;
        int timeElapsedSinceStart = (int) (System.currentTimeMillis() - mStartTimeMillis);
        setPosition(mPosition + timeElapsedSinceStart);
        mVideoView.pause();
    }
}

private void setupCallbacks() {

    mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {

        @Override
        public boolean onError(MediaPlayer mp, int what, int extra) {
            mVideoView.stopPlayback();
            mPlaybackState = LeanbackPlaybackState.IDLE;
            return false;
        }
    });

    mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
        @Override
        public void onPrepared(MediaPlayer mp) {
            if (mPlaybackState == LeanbackPlaybackState.PLAYING) {
                mVideoView.start();
            }
        }
    });

    mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
        @Override
        public void onCompletion(MediaPlayer mp) {
            mPlaybackState = LeanbackPlaybackState.IDLE;
        }
    });
}

UI control part will handle 

  • Toggling icon of Play/Pause
  • Update current time of video
    public void playbackStateChanged() {

        if (mCurrentPlaybackState != PlaybackState.STATE_PLAYING) {
            mCurrentPlaybackState = PlaybackState.STATE_PLAYING;
            startProgressAutomation();
            setFadingEnabled(true);
            mPlayPauseAction.setIndex(PlaybackControlsRow.PlayPauseAction.PAUSE);
            mPlayPauseAction.setIcon(mPlayPauseAction.getDrawable(PlaybackControlsRow.PlayPauseAction.PAUSE));
            notifyChanged(mPlayPauseAction);
        } else if (mCurrentPlaybackState != PlaybackState.STATE_PAUSED) {
            mCurrentPlaybackState = PlaybackState.STATE_PAUSED;
            stopProgressAutomation();
            //setFadingEnabled(false); // if set to false, PlaybackcontrolsRow will always be on the screen
            mPlayPauseAction.setIndex(PlaybackControlsRow.PlayPauseAction.PLAY);
            mPlayPauseAction.setIcon(mPlayPauseAction.getDrawable(PlaybackControlsRow.PlayPauseAction.PLAY));
            notifyChanged(mPlayPauseAction);
        }

        int currentTime = ((PlaybackOverlayActivity) getActivity()).getPosition();
        mPlaybackControlsRow.setCurrentTime(currentTime);
        mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME);

    }

    private void startProgressAutomation() {
        if (mRunnable == null) {
            mRunnable = new Runnable() {
                @Override
                public void run() {
                    int updatePeriod = getUpdatePeriod();
                    int currentTime = mPlaybackControlsRow.getCurrentTime() + updatePeriod;
                    int totalTime = mPlaybackControlsRow.getTotalTime();
                    mPlaybackControlsRow.setCurrentTime(currentTime);
                    mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME);

                    if (totalTime > 0 && totalTime <= currentTime) {
                        stopProgressAutomation();
                        //next(true);
                    } else {
                        mHandler.postDelayed(this, updatePeriod);
                    }
                }
            };
            mHandler.postDelayed(mRunnable, getUpdatePeriod());
        }
    }

    private void stopProgressAutomation() {
        if (mHandler != null && mRunnable != null) {
            mHandler.removeCallbacks(mRunnable);
            mRunnable = null;
        }
    }

Rewind & FastForward

    private void fastForward() {
        /* Video control part */
        ((PlaybackOverlayActivity) getActivity()).fastForward();

        /* UI part */
        int currentTime = ((PlaybackOverlayActivity) getActivity()).getPosition();
        mPlaybackControlsRow.setCurrentTime(currentTime);
        mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME);
    }

    private void rewind() {
        /* Video control part */
        ((PlaybackOverlayActivity) getActivity()).rewind();

        /* UI part */
        int currentTime = ((PlaybackOverlayActivity) getActivity()).getPosition();
        mPlaybackControlsRow.setCurrentTime(currentTime);
        mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME);
    }

Here, rewind & fast forward Video control implementations are done in easy way, just rewind/fast forward 10 seconds from current position.

    public void fastForward() {
        if (mDuration != -1) {
            // Fast forward 10 seconds.
            setPosition(mVideoView.getCurrentPosition() + (10 * 1000));
            mVideoView.seekTo(mPosition);
        }
    }

    public void rewind() {
        // rewind 10 seconds
        setPosition(mVideoView.getCurrentPosition() - (10 * 1000));
        mVideoView.seekTo(mPosition);
    }

UI control part is updating current time of video.

SkipPrevious & SkipNext

    private void next(boolean autoPlay) {
        /* Video control part */
        if (++mCurrentItem >= mItems.size()) { // Current Item is set to next here
            mCurrentItem = 0;
        }

        if (autoPlay) {
            mCurrentPlaybackState = PlaybackState.STATE_PAUSED;
        }

        Movie movie = mItems.get(mCurrentItem);
        if (movie != null) {
            ((PlaybackOverlayActivity) getActivity()).setVideoPath(movie.getVideoUrl());
            ((PlaybackOverlayActivity) getActivity()).setPlaybackState(PlaybackOverlayActivity.LeanbackPlaybackState.PAUSED);
            ((PlaybackOverlayActivity) getActivity()).playPause(autoPlay); 
        }

        /* UI part */
        playbackStateChanged();
        updatePlaybackRow(mCurrentItem);
    }

    private void prev(boolean autoPlay) {
        /* Video control part */
        if (--mCurrentItem < 0) { // Current Item is set to previous here
            mCurrentItem = mItems.size() - 1;
        }
        if (autoPlay) {
            mCurrentPlaybackState = PlaybackState.STATE_PAUSED;
        }

        Movie movie = mItems.get(mCurrentItem);
        if (movie != null) {
            ((PlaybackOverlayActivity) getActivity()).setVideoPath(movie.getVideoUrl());
            ((PlaybackOverlayActivity) getActivity()).setPlaybackState(PlaybackOverlayActivity.LeanbackPlaybackState.PAUSED);
            ((PlaybackOverlayActivity) getActivity()).playPause(autoPlay);
        }

        /* UI part */
        playbackStateChanged();
        updatePlaybackRow(mCurrentItem);
    }

For Video control part, 2 functions are doing same thing except for first line. mCurrentItem is set to previous/next followed by setting proper video path by using setVideoPath method & play/pause depending on current play/pause status by using playPause method.

UI control part, first line calls playbackStateChanged() method, but it is only necessary to control startProgressAutomation/stopProgressAutomation to update current time status of video. updateplaybackRow method is to update DetailsDescription information of video content.

    private void updatePlaybackRow(int index) {
        Log.d(TAG, "updatePlaybackRow");
        if (mPlaybackControlsRow.getItem() != null) {
            Movie item = (Movie) mPlaybackControlsRow.getItem();
            item.setTitle(mItems.get(mCurrentItem).getTitle());
            item.setStudio(mItems.get(mCurrentItem).getStudio());

            mRowsAdapter.notifyArrayItemRangeChanged(0, 1);
            /* total time is necessary to show video playing time progress bar */
            int duration = (int) Utils.getDuration(mItems.get(mCurrentItem).getVideoUrl());
            Log.i(TAG, "videoUrl: " + mItems.get(mCurrentItem).getVideoUrl());
            Log.i(TAG, "duration = " + duration);
            mPlaybackControlsRow.setTotalTime(duration);
            mPlaybackControlsRow.setCurrentTime(0);
            mPlaybackControlsRow.setBufferedProgress(0);
        }
        if (SHOW_IMAGE) {
            mPlaybackControlsRowTarget = new PicassoPlaybackControlsRowTarget(mPlaybackControlsRow);
            updateVideoImage(mItems.get(mCurrentItem).getCardImageURI());
        }
    }


ThumbUp & ThumbDown & Repeat & Shuffle & HighQuality & ClosedCaptioning & MoreActions

How to toggle icon’s color? You can change by setting index of the action. Implement below in onActionClicked method to check how each index setting will behave.

                    /* Change icon */
                    if (action instanceof PlaybackControlsRow.ThumbsUpAction ||
                            action instanceof PlaybackControlsRow.ThumbsDownAction ||
                            action instanceof PlaybackControlsRow.RepeatAction ||
                            action instanceof PlaybackControlsRow.ShuffleAction ||
                            action instanceof PlaybackControlsRow.HighQualityAction ||
                            action instanceof PlaybackControlsRow.ClosedCaptioningAction) {
                        ((PlaybackControlsRow.MultiAction) action).nextIndex();
                    }
action_icon_change
Action icon change in SecondaryActionsAdapter.


Build & Run

Now you can check that Video control is working correctly for PrimaryRow.
*SecondaryRow’s action implementation may differ depending on your desire and I will skip here.

* Note. I could take VideoView’s image by screen recording, but I couldn’t take VideoView’s screen capture via Android studio’s debugging tool… (I’m using Sony Android TV for development of Android TV now.)

Again source code is on github.

Next chapter, we implement MediaSession.

Leave a Comment

Your email address will not be published. Required fields are marked *