Introduction to Media Playback using ExoPlayer
- 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
Components of a Media Application can be implemented in 1 Activity:
- Media Controller hooked to the UI
- Media Session which controls the Player
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
- 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
- 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
- If you are playing videos specifically from Youtube, check the YouTube Developer APIs
- You can build a Custom Media Player from the low level Media API like MediaCodec, AudioTrack, and MediaDrm
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
- 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.
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;
}
- 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.
- SimpleExoPlayerView has a layout called: exo_simple_player_view.xml
- This layout file includes a PlayBackControlView which also uses its own layout file: exo_playback_control_view.xml
- Use of standard ids is required so that child views can be identified, bound to the player and updated in an appropriate way.
- A full list of the standard ids for each view can be found in the Javadoc for PlaybackControlView and SimpleExoPlayerView.
- 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
Creating a MediaSession
- Create a MediaSessionCompat object (when the activity is created to initialize the MediaSession)
mMediaSession = new MediaSessionCompat(this, TAG);
- Set the Flags using the features you want to support
mMediaSession.setFlags(
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
- 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
- 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());
- Set the callbacks
mMediaSession.setCallback(new MySessionCallback());
- Start the session
mMediaSession.setActive(true);
- 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.
- 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());
-
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
-
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;
}
- 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);
}
}
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.media.app.NotificationCompat.MediaStyle;
import android.support.v7.app.NotificationCompat;
implementation ‘com.android.support:support-media-compat:26.+’
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);
}
// 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));
- 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.
- 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).
-
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.
- 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);
- 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);