Share
Embed
I/O 2016: Best Practices in Media Playback on Android
All of the best practices in media playback on Android, whether you are doing audio or video playback
Ian Lake
May 19, 2016
Transcript
43 slides
-
Goal Tell you when to use the right APIs to
build the best audio or video playback app possible -
Media playback and the Android lifecycle Created Playing Paused Stopped
Destroyed Video: Activity (<N) onCreate() onPause() onDestroy() Video: Activity (N+) onCreate() onStop() onDestroy() Audio: Service onCreate() onDestroy() -
Playing media You did go to the ExoPlayer talk, right?
Created Playing Paused Stopped Destroyed MediaPlayer new prepare -> play pause stop release It plays -> we’re done? -
// Request audio focus AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); int
result = audioManager.requestAudioFocus( mOnAudioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { // Proceed with the playing of glorious music } // On stop audioManager.abandonAudioFocus(mOnAudioFocusChangeListener); Audio Focus Ensure apps don’t talk over one another Hold audio focus until we’ve stopped playback -
OnAudioFocusChangeListener ➔ AUDIOFOCUS_LOSS ◆ Stop Playback ➔ AUDIOFOCUS_LOSS_TRANSIENT ◆ Pause
➔ AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK ◆ Lower volume (keep playing) ➔ AUDIOFOCUS_GAIN ◆ Play at full volume (if previously paused) -
private BroadcastReceiver mNoisyReceiver = new BroadcastReceiver() { @Override public void
onReceive(Context context, Intent intent) { // Pause the music } }; // On Play IntentFilter filter = new IntentFilter( AudioManager.ACTION_AUDIO_BECOMING_NOISY)); registerReceiver(mNoisyReceiver, filter); // On Pause unregisterReceiver(mNoisyReceiver); ACTION_AUDIO_BECOMING_NOISY -
Created Playing Paused Stopped Destroyed MediaPlayer new prepare -> play
pause stop release Audio Focus request focus abandon focus BECOMING_ NOISY register unregister Lifecycle of media playback Local playback...for API 7 -
Android system Android Wear Android Auto Your app Your app
MediaSessionCompat: your app’s public face MediaControllerCompat MediaSessionCompat Callback Token -
Role of the Callback class onPlay(), onPause(), etc, etc. The
MediaSessionCompat.Callback instance serves as a single point in your app where all media control callbacks flow into. Can be swapped at runtime to change behavior (Local vs Remote for instance) -
// In onCreate() mMediaSession = new MediaSessionCompat(context, LOG_TAG); mMediaSession.setCallback(new LocalPlaybackCallback());
mMediaSession.setFlags( MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); // Right after audio focus mMediaSession.setActive(true); // On stop mMediaSession.setActive(false); // On destroy mMediaSession.release(); MediaSessionCompat -
PlaybackStateCompat MediaSessionCompat PlaybackStateCompat setState() setState() setActions() It works!* *API 21+
MediaButtonReceiver component may not be null -
Media Button Receiver BroadcastReceiver receiving android.intent.action. MEDIA_BUTTON Pre-Lollipop: how the
system sent media buttons, required (technically a PendingIntent API 18+, but there be dragons) Lollipop+: allows restarting playback after the session is stopped (i.e., setActive(false)), optional -
MediaButtonReceiver In Support v4 Add to manifest to auto forward
to your Service - Has media browser service intent-filter - Has the MEDIA_BUTTON action intent-filter // Translate a MEDIA_BUTTON Intent into Callbacks // Call in onStartCommand(), etc MediaButtonReceiver.handleIntent(mMediaSession, intent); -
// construct a PendingIntent for the media button Intent mediaButtonIntent
= new Intent(Intent.ACTION_MEDIA_BUTTON); mediaButtonIntent.setClass(context, MediaButtonReceiver.class); PendingIntent mbrIntent = PendingIntent.getBroadcast(context, 0, mediaButtonIntent, 0); mMediaSession.setMediaButtonReceiver(mbrIntent); Enabling MediaButtonReceiver 21+ -
MediaMetadataCompat MediaSessionCompat MediaMetadataCompat setMetadata() METADATA_KEY_TITLE METADATA_KEY_ARTIST METADATA_KEY_ALBUM METADATA_KEY_DURATION METADATA_KEY_ART /
METADATA_KEY_ALBUM_ART METADATA_KEY_ART_URI / METADATA_KEY_ALBUM_ART_URI -
Media Notifications NotificationCompat.MediaStyle MediaHelper.from(Context, MediaSessionCompat) - uses MediaMetadataCompat#getDescription() MediaHelper.getActionIntent(Context, int)
Full code at gist.github.com/ianhanniballake/47617ec3488e0257325c -
NotificationCompat.Builder builder = MediaStyleHelper.from(this, mediaSession); builder .setSmallIcon(R.drawable.notification_icon) .setColor(ContextCompat.getColor(this, R.color.primaryDark)); builder
.addAction(new NotificationCompat.Action( R.drawable.pause, getString(R.string.pause), MediaStyleHelper.getActionIntent(this, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))); NotificationCompat.Builder builder = MediaStyleHelper.from(this, mediaSession); builder .setSmallIcon(R.drawable.notification_icon) .setColor(ContextCompat.getColor(this, R.color.primaryDark)); NotificationCompat.Builder builder = MediaStyleHelper.from(this, mediaSession); builder .setSmallIcon(R.drawable.notification_icon) .setColor(ContextCompat.getColor(this, R.color.primaryDark)); builder .addAction(new NotificationCompat.Action( R.drawable.pause, getString(R.string.pause), MediaStyleHelper.getActionIntent(this, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))); builder.setStyle(new NotificationCompat.MediaStyle() .setShowActionsInCompactView(0) .setMediaSession(mediaSession.getSessionToken())); -
Created Playing Paused Stopped Destroyed MediaPlayer new prepare -> play
pause stop release Audio Focus request focus abandon focus BECOMING_ NOISY register unregister MediaSession Compat new set flags set callback setActive(true) set metadata set state set state setActive(false) release Notification show notification update notification clear notification Minimum Viable Product -
// Let’s say I have a MediaSessionCompat.Token token MediaControllerCompat mediaController
= new MediaControllerCompat(this, // Context token); // Part of FragmentActivity setSupportMediaController(mediaController); mButton.setOnClickListener(view -> getSupportMediaController().getTransportControls().pause() ); Getting started -
MediaControllerCompat.Callback callback = new MediaControllerCompat.Callback() { @Override public void onMetadataChanged(MediaMetadataCompat
metadata) {} @Override public void onPlaybackStateChanged(PlaybackStateCompat state) {} }); mediaController.registerCallback(callback); //Make sure to call unregisterCallback(callback)! MediaControllerCompat.Callback -
public class MediaPlaybackService extends MediaBrowserServiceCompat { private MediaSessionCompat mMediaSession; @Override
public void onCreate() { ... setSessionToken(mMediaSession.getSessionToken()); } MediaBrowserServiceCompat -
@Override public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, Bundle rootHints)
{ // This should probably do something useful if (TextUtils.equals(clientPackageName, getPackageName()) { return new BrowserRoot(getString(R.string.app_name), null); } return null; } @Override public void onLoadChildren(String parentId, Result<List<MediaBrowerCompat.MediaItem>> result) { result.sendResult(null); } -
MediaBrowserCompat mediaBrowser = new MediaBrowserCompat(context, new ComponentName(this, MediaPlaybackService.class), mConnectionCallbacks, null);
// optional Bundle mediaBrowser.connect(); // Make sure to disconnect()! Connecting -
new MediaBrowserCompat.ConnectionCallback() { @Override public void onConnected() { MediaSessionCompat.Token token
= mediaBrowser.getSessionToken(); // Set us up the MediaControllerCompat } @Override public void onConnectionSuspended() {} @Override public void onConnectionFailed() {} } ConnectionCallbacks -
An app divided...on purpose Using a separate process for your
playback Service -
Foreground Services Requires a notification Notifications for foreground services are
“ongoing” - they can’t be swiped away Prior to Lollipop, stopForeground(false) didn’t make the notification dismissable. -
Bound vs Started MediaBrowserServiceCompat is bound to your UI startService()
lets you continue playing -
Created Playing Paused Stopped Destroyed MediaPlayer new prepare -> play
pause stop release Audio Focus request focus abandon focus BECOMING_ NOISY register unregister MediaSession Compat new set flags set callback setActive(true) set metadata set state set state setActive(false) release Notification start FG stopFG(false) stopFG(true) Service set token startService stopSelf Lifecycle of media playback -
What’s Next? Check out UAMP github.com/googlesamples/android-UniversalMusicPlayer Watch the talks on
- ExoPlayer - Android Auto - Google Cast and Android TV - Introducing the Cast SDK - Android Wear 2.0: Standalone youtube.com/AndroidDevelopers