Saturday, May 24, 2014

Creating a Native Video Player Activity

    One of the major uses for mobile devices is to watch videos. While YouTube is great for this, not all content is posted there. Given that sometimes proprietary online content will need to be played, I decided to make a simple native video player to play content from a URL. All code for this demo is available on GitHub.

Video player with an online video in landscape orientation
    The MainActivity for this application is a simple button that, when pressed, launches an intent with a URL for the native video player activity.

private void launchVideoPlayer() {
        Intent i = new Intent( this, VideoPlayerActivity.class );
        i.putExtra( VideoPlayerActivity.EXTRA_VIDEO_URL, "http://www.pocketjourney.com/downloads/pj/video/famous.3gp" );
        startActivity( i );
    }

    The layout for the video player activity consists of a VideoView and an indeterminate ProgressBar spinner. The spinner is shown while the video is buffered.

Loading spinner for the video player
    When the video player is created, MediaPlayer listeners and the URL that was passed through the intent are attached to the VideoView. 

mVideoView = (VideoView) findViewById( R.id.video_view );
mVideoView.setOnCompletionListener( onCompletionListener );
mVideoView.setOnErrorListener( onErrorListener );
mVideoView.setOnPreparedListener( onPreparedListener );

if( mVideoView == null ) {
    throw new IllegalArgumentException( "Layout must contain a video view with ID video_view" );
}

mUri = Uri.parse( getIntent().getExtras().getString( EXTRA_VIDEO_URL ) );
mVideoView.setVideoURI( mUri );

    A MediaController object is then created to add playback controls for the VideoView.

mMediaController = new MediaController( this );
mMediaController.setEnabled( true );
mMediaController.show();
mMediaController.setMediaPlayer( mVideoView );

    The OnCompletionListener resets the video to its initial position, pauses it and shows the playback controls.

protected MediaPlayer.OnCompletionListener onCompletionListener = new MediaPlayer.OnCompletionListener() {
    @Override
    public void onCompletion(MediaPlayer mediaPlayer) {
        mVideoView.seekTo( 0 );
        if( mVideoView.isPlaying() )
            mVideoView.pause();

        if( !mMediaController.isShowing() )
            mMediaController.show();
    }
};

    The OnPreparedListener is triggered when the video URL has been found and buffered. This is where the progress spinner is hidden and the media controller is attached to the VideoView, and the video is started.

protected MediaPlayer.OnPreparedListener onPreparedListener = new MediaPlayer.OnPreparedListener() {
    @Override
    public void onPrepared(MediaPlayer mediaPlayer) {
        if( mediaPlayer == null )
            return;
        mMediaPlayer = mediaPlayer;
        mediaPlayer.start();
        if( mSpinningProgressBar != null )
            mSpinningProgressBar.setVisibility( View.GONE );

        mVideoView.setMediaController( mMediaController );
    }
};

    Finally the OnErrorListener simply shows an AlertDialog that closes the video activity when the dialog is closed. Once the basic infrastructure is put together for playing the remote video, handling rotation and leaving/coming back to the app should be considered. In regards to hitting home and coming back to the app, the onPause and onResume methods are used to save the video position, restore it and set the spinner to visible. Given that the OnPreparedListener is still attached to the video view, that listener will be triggered when the video is ready to be played again and the app can continue as normal.

@Override
protected void onResume() {
    super.onResume();
    mVideoView.seekTo( mPosition );
    mSpinningProgressBar.setVisibility( View.VISIBLE );
}

@Override
protected void onPause() {
    super.onPause();
    mPosition = mVideoView.getCurrentPosition();
}

    For device rotation, we take advantage of the activity lifecycle and onSaveInstanceState and onRestoreInstanceState to save whether the video is playing and its position on rotation.

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    if( mVideoView == null || mVideoView.getCurrentPosition() == 0 )
        return;

    outState.putInt( EXTRA_CURRENT_POSITION, mVideoView.getCurrentPosition() );
    outState.putBoolean( EXTRA_IS_PLAYING, mVideoView.isPlaying() );
    mVideoView.pause();
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    if( mVideoView == null || savedInstanceState == null )
        return;

    if( savedInstanceState.getBoolean( EXTRA_IS_PLAYING, false ) ) {
        mVideoView.seekTo(savedInstanceState.getInt(EXTRA_CURRENT_POSITION, 0));
        mVideoView.start();
    }
}

Video in portrait orientation
    And with that, we have a simple native video player for remote content using native controls and some versatility. There's still a whole lot more that can be done with the video player, such as customizing UI controls, that I haven't touched on, but can add a good deal of polish to any media app.