Skip to content

Latest commit

 

History

History
478 lines (374 loc) · 18.6 KB

README.md

File metadata and controls

478 lines (374 loc) · 18.6 KB

ExoPlayback

Introduction to Media Playback using ExoPlayer

Player UI Structure

  • Media Player takes some Media and render it as Audio or Video
  • Media Controller includes all of the Playback buttons

The Android Framework provides 2 classes:

  • Media Session
  • Media Controller

They communicate with each other so that the UI stays in sync with the Player using predefined callbacks: play, pause, stop etc. Think of it as a Standard Decoupling Pattern, the Media Session isolates the Player from the UI:

  • So you can easily swap in different players without affecting the UI.
  • Or change the look & feel without changing the Player's features.

It's a Client Server Architecture:

  • The Media Session becomes the Server that holds the info about the Media and the state of the Player
  • Each controller becomes the Client that needs to sustain sync with the Media Session

Audio/Video

Components of a Media Application can be implemented in 1 Activity:

  • Media Controller hooked to the UI
  • Media Session which controls the Player

Audio Apps

If the user navigates away:

  • The Player may outlive the Activity that started it
  • Media Session should run into a Media Browsing Service which updates the UI when the Player state changes
  • The Activity-Service communication is simplified by some framework classes

Comparing Players

MediaPlayer

  • The basic functionality for a bared boned Player
  • Supports the most common Audio and Video formats
  • Supports very little customizations
  • Very straight forward to use
  • Good enough for many simple use cases

ExoPlayer

  • An Open Source Library that exposes the lower level Android Audio APIs
  • Supports High performance features like Dash, HLS...
  • You can customize the ExoPlayer code making it easy to add new components
  • Can only be used with Android version 4.1 or higher

Youtube Player

Custom Player

ExoPlayer is the preferred choice:

  • It supports many different formats and extensible features
  • It's a library you include in you application APK
  • You have control over which version you use
  • You can easily update to a newer version as part of a regular application update

Add ExoPlayer

  • Media Player belongs in a Service or an Activity: depending on wether the App requires Background PlayBack
  • Here we expect the user to be in the Activity when they are listening to the music
  • We will implement our MediaPlayer inside the MainActivity.

Add SimpleExoPlayerView

In your layout add the following:

<com.google.android.exoplayer2.ui.SimpleExoPlayerView
	android:id="@+id/playerView"
	android:layout_width="match_parent"
	android:layout_height="wrap_content" />
  • Initialize the View in OnCreate
  • Load a Background Image in the Player
  • Create a SimpleExoPlayer instance by calling initializePlayer
mPlayerView = (SimpleExoPlayerView) findViewById(R.id.playerView);
mPlayerView.setDefaultArtwork(BitmapFactory.decodeResource(getResources(), R.drawable.player_image)); 
initializePlayer(Uri.parse(answerSample.getUri()));

private void initializePlayer(Uri mediaUri){
	if (mExoPlayer == null){
		//Create an instance of the ExoPlayer
		TrackSelector trackSelector = new DefaultTrackSelector();
		LoadControl loadControl = new DefaultLoadControl(); 
		mExoPlayer = ExoPlayerFactory.newSimpleInstance(this, trackSelector, loadControl);
		mPlayerView.setPlayer(mExoPlayer);
		//Prepare the MediaSource
		String userAgent = Util.getUserAgent(this, "ClassicalMusicQuiz");
		MediaSource mediaSource = new ExtractorMediaSource(mediaUri, new DefaultDataSourceFactory(
			this, userAgent), new DefaultExtractorsFactory(), null, null);
		mExoPlayer.prepare(mediaSource);
		mExoPlayer.setPlayWhenReady(true);
	}
}
  • Finally we want to stop and release the Player when the Activity is destroyed or also on OnPause or onStop, if you don't want the music to continue playing when the app is not visible.
  • We will leave the music playing while the user might be checking other apps, but we do run the risk that the Activity might be destroyed by the System and therefore unexpectedly terminating PlayBack.
@Override 
protected void onDestroy(){
	super.onDestroy();
	releasePlayer();
}

private void releasePlayer(){
	mExoPlayer.stop();
	mExoPlayer.release();
	mExoPlayer = null;
}

Customizing ExoPlayer UI

ExoPlayer comes with 2 notable UI elements:

  • PlaybackControlView is a view for controlling ExoPlayer instances. It displays standard playback controls including a play/pause button, fast-forward and rewind buttons, and a seek bar.
  • SimpleExoPlayerView is a high level view for SimpleExoPlayer media playbacks. It displays video (or album art) and displays playback controls using a PlaybackControlView.

Overriding layout files

ExoPlayer Event Listening

  • ExoPlayer.EventListener is a interface that allows you to monitor any changes in the ExoPlayer. It requires that you implement 6 methods but the only one we're interested in is OnPlayerStateChanged.
@Override 
onPlayerStateChanged(boolean playWhenReady, int playbackState)
  • playWhenReady is a play/pause state indicator

  • true = playing, false = paused.

  • playbackState tells what state the ExoPlayer is in:

    • STATE_IDLE
    • STATE_BUFFERING
    • STATE_READY
    • STATE_ENDED
  • Use this method in a if/else statement to log when the ExoPlayer is playing or paused:

    @Override
    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
        if((playbackState == ExoPlayer.STATE_READY) && playWhenReady){
            mStateBuilder.setState(PlaybackStateCompat.STATE_PLAYING,
                    mExoPlayer.getCurrentPosition(), 1f);
            Log.d("onPlayerStateChanged:", "PLAYING");
        } else if((playbackState == ExoPlayer.STATE_READY)){
            mStateBuilder.setState(PlaybackStateCompat.STATE_PAUSED,
                    mExoPlayer.getCurrentPosition(), 1f);
            Log.d("onPlayerStateChanged:", "PAUSED");
        }
        mMediaSession.setPlaybackState(mStateBuilder.build());
        showNotification(mStateBuilder.build());
    }
  • We will use this method to update the MediaSession

Add Media Session - Part 1

Creating a MediaSession

  1. Create a MediaSessionCompat object (when the activity is created to initialize the MediaSession)
mMediaSession = new MediaSessionCompat(this, TAG);
  1. Set the Flags using the features you want to support
mMediaSession.setFlags(
	MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | 
		MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
  1. Set an optional Media Button Receiver component
mMediaSession.setMediaButtonReceiver(null);
  • We will set it to null since we don't want a MediaPlayer button to start our app if it has been stopped
  1. Set the available actions and initial state
mStateBuilder = new PlaybackStateCompat.Builder()
	.setActions(
		PlaybackStateCompat.ACTION_PLAY |
		PlaybackStateCompat.ACTION_PAUSE |
		PlaybackStateCompat.ACTION_PLAY_PAUSE |
		PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS);

mMediaSession.setPlaybackState(mStateBuilder.build());
  1. Set the callbacks
mMediaSession.setCallback(new MySessionCallback());
  1. Start the session
mMediaSession.setActive(true);
  1. End the session when it is no longer needed (when the activity is destroyed to release the MediaSession)
mMediaSession.setActive(false);
/**
* Media Session Callbacks, where all external clients control the player.
*/

private class MySessionCallback extends MediaSessionCompat.Callback {
	@Override
	public void onPlay() {
		mExoPlayer.setPlayWhenReady(true);
	}

	@Override 
	public void onPause() {
		mExoPlayer.setPlayWhenReady(false);
	}

	@Override
	public void onSkipToPrevious(){
		mExoPlayer.seekTo(0);
	}
}
  • We need to make sure that wether the app is controlled from the internal client (ExoPlayerView) or any external client (through the MediaSession), both the ExoPlayer and the MediaSession contain the State information and they remain in sync.

Internal Client

  • We need to make sure that when the State changes from the UI, the MediaSession is updated by using this method:
// Call this every time the Player State changes

mStateBuilder.setState(PlaybackStateCompat.STATE_PLAYING, mExoPlayer.getCurrentPosition(), 1f)

mMediaSession.setPlaybackState(mStateBuilder.build());

Add Media Session - Part 2

  • Now every time you press the Media Button in the ExoPlayerView, our MediaSession will have its state updated

  • Next, we will need to make sure that external clients can control the ExoPlayer as well

  • Our MediaSession.Callback will automatically be called by external clients

  • We just need to make sure it calls the appropriate ExoPlayer methods which will then trigger the ExoPlayer.EventListener and therefore update the MediaSession

  • The only problem is we still don't have any external player setup. Let's create a Media Style Notification aka an external client

Add MediaStyle Notification

  • To verify that our MediaSession is working as intended, we will add a MediaStyle Notification

  • Let's create a method called showNotification:

private void showNotification(PlaybackCompat state){
	NotificationCompat.Builder builder = new NotificationCompat.Builder(this);

	int icon;
	String play_pause;
	if(state.getState() == PlaybackCompat.STATE_PLAYING){
		icon = R.drawable.exo_controls_pause;
		play_pause = getString(R.string.pause);
	}
	else {
		icon = R.drawable.exo_controls_play;
		play_pause = getString(R.string.play);
	}

	NotificationCompat.Action playPauseAction = new NotificationCompat.Action(
		icon, play_pause,
		MediaButtonReceiver.buildMediaButtonPendingIntent(this,
			PlaybackStateCompat.ACTION_PLAY_PAUSE));

	NotificationCompat.Action restartAction = new NotificationCompat.Action(
		R.drawable.exo_controls_previous, getString(R.string.restart),
		MediaButtonReceiver.buildMediaButtonPendingIntent(this,
			PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS));

	PendingIntent contentPendingIntent = PendingIntent.getActivity
		(this, 0, new Intent(this, QuizActivity.class), 0);

	builder.setContentTitle(getString(R.string.guess))
		.setContentText(getString(R.string.notification_text))
		.setContentIntent(contentPendingIntent)
		.setSmallIcon(R.drawable.ic_music_note)
		.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
		.addAction(restartAction)
		.addAction(playPauseAction)
		.setStyle(new Notification.MediaStyle()
			.setMediaSession(mMediaSession.getSessionToken())
			.setShowActionsInCompatView(0,1));

	mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
	mNotificationManager.notify(0, builder.build());
}
  • We call our new showNotification in the OnPlayerStateChanged method
showNotification(mStateBuilder.build());
  • We should also call cancelAll on the NotificationManager when the activity is destroyed
private void releasePlayer(){
	mNotificationManager.cancelAll();
	mExoPlayer.stop();
	mExoPlayer.release();
	mExoPlayer = null;
}

Add Media Button Receiver

  • We need to create a BroadcastReceiver with an intent-filter for the MediaButton Intent Action (AndroidManifest)
<application 
	...
	<receiver android:name=".QuizActivity$MediaReceiver">
		<intent-filter>
			<action android:name="android.intent.action.MEDIA_BUTTON" />
		</intent-filter>
	</receiver>
</application>
  • We can create it as a static inner class inside the MainActivity
/**
* Broadcast Receiver registered to receive the MEDIA_BUTTON intent coming from clients
*/

public static class MediaReceiver extends BroadcastReceiver {
	
	public MediaReceiver(){
	}
	
	@Override
	public void onReceive(Context context, Intent intent){
		MediaButtonReceiver.handleIntent(mMediaSession, intent);
	}
}

Change your import statements

import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.media.app.NotificationCompat.MediaStyle;

You might have an import statement from v7, that you no longer need:

import android.support.v7.app.NotificationCompat;

In your build.gradle, you now only need to import the media-compat support library.

implementation ‘com.android.support:support-media-compat:26.+’

Use NotificationCompat with channels

The v4 support library now has a new constructor for creating notification builders:

NotificationCompat.Builder notificationBuilder =
        new NotificationCompat.Builder(mContext, CHANNEL_ID);

The NotificationCompat class does not create a channel for you. You still have to create a channel yourself.

private static final String CHANNEL_ID = "media_playback_channel";

    @RequiresApi(Build.VERSION_CODES.O)
    private void createChannel() {
        NotificationManager
                mNotificationManager =
                (NotificationManager) mContext
                        .getSystemService(Context.NOTIFICATION_SERVICE);
        // The id of the channel.
        String id = CHANNEL_ID;
        // The user-visible name of the channel.
        CharSequence name = "Media playback";
        // The user-visible description of the channel.
        String description = "Media playback controls";
        int importance = NotificationManager.IMPORTANCE_LOW;
        NotificationChannel mChannel = new NotificationChannel(id, name, importance);
        // Configure the notification channel.
        mChannel.setDescription(description);
        mChannel.setShowBadge(false);
        mChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
        mNotificationManager.createNotificationChannel(mChannel);
    }

Here’s code that creates a MediaStyle notification with NotificationCompat.

// You only need to create the channel on API 26+ devices
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  createChannel();
}
NotificationCompat.Builder notificationBuilder =
       new NotificationCompat.Builder(mContext, CHANNEL_ID);
notificationBuilder
       .setContentTitle(getString(R.string.guess))
                .setContentText(getString(R.string.notification_text))
                .setContentIntent(contentPendingIntent)
                .setSmallIcon(R.drawable.ic_music_note)
                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
                .addAction(restartAction)
                .addAction(playPauseAction)
                .setStyle(new MediaStyle()
                        .setMediaSession(token)
                        .setShowActionsInCompactView(0, 1));

Android Media Framework Extras

Audio Focus

  • This is how the Android framework knows about different applications using audio. If you want your app to fade out when other important notifications (such as navigation) occur, you'll need to learn how your app can "hop in line" to be the one in charge of audio playback, until another app requests focus.

Noisy Intent

  • Imagine you were listening to a song at full volume, and you trip and yank out the headphones from the audio port. The android framework sends out the ACTION_ AUDIO_ BECOMING_ NOISY intent when this occurs. This allows you to register a broadcast receiver and take a specific action when this occurs (like pausing the music).

Audio Stream

  • Android uses separate audio streams for playing music, alarms, notifications, the incoming call ringer, system sounds, in-call volume, and DTMF tones. This allows users to control the volume of each stream independently.

  • By default, pressing the volume control modifies the volume of the active audio stream. If your app isn't currently playing anything, hitting the volume keys adjusts the ringer volume. To ensure that volume controls adjust the correct stream, you should call setVolumeControlStream() passing in AudioManager.STREAM_MUSIC.

ExoPlayer Extras

Subtitle Side Loading

  • Given a video file and a separate subtitle file, MergingMediaSource can be used to merge them into a single source for playback.
MediaSource videoSource = new ExtractorMediaSource(videoUri, ...);
MediaSource subtitleSource = new SingleSampleMediaSource(subtitleUri, ...);
// Plays the video with the sideloaded subtitle.
MergingMediaSource mergedSource =
    new MergingMediaSource(videoSource, subtitleSource);

Looping a video

  • A video can be seamlessly looped using a LoopingMediaSource. The following example loops a video indefinitely. It’s also possible to specify a finite loop count when creating a LoopingMediaSource.
MediaSource source = new ExtractorMediaSource(videoUri, ...);
// Loops the video indefinitely.
LoopingMediaSource loopingSource = new LoopingMediaSource(source);