I/O 2016: Best Practices in Media Playback on Android

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

44dac57d3bb65805bc3df4409d019c83?s=128

Ian Lake

May 19, 2016
Tweet

Transcript

  1. Media playback Best practices +IanLake @ianhlake

  2. Goal Tell you when to use the right APIs to

    build the best audio or video playback app possible
  3. Important events in media playback Created Playing Paused Stopped Destroyed

  4. 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()
  5. 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?
  6. Media playback the right way

  7. The Right Way Audio Focus ACTION_BECOMING_NOISY MediaSession Notifications Local Only

    Always
  8. // 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
  9. 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)
  10. 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
  11. 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
  12. User Expectations

  13. MediaSession Or really, MediaSessionCompat

  14. Android system Android Wear Android Auto Your app Your app

    MediaSessionCompat: your app’s public face MediaControllerCompat MediaSessionCompat Callback Token
  15. 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)
  16. // 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
  17. PlaybackStateCompat MediaSessionCompat PlaybackStateCompat setState() setState() setActions() It works!* *API 21+

    MediaButtonReceiver component may not be null
  18. 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
  19. 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);
  20. // 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+
  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
  22. Media Notifications NotificationCompat.MediaStyle MediaHelper.from(Context, MediaSessionCompat) - uses MediaMetadataCompat#getDescription() MediaHelper.getActionIntent(Context, int)

    Full code at gist.github.com/ianhanniballake/47617ec3488e0257325c
  23. 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()));
  24. 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
  25. Building your UI With MediaControllerCompat

  26. // 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
  27. Keeping your UI in sync getMetadata() -> MediaMetadataCompat getPlaybackState() ->

    PlaybackStateCompat
  28. 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
  29. Background playback With a Service

  30. Background playback With a MediaBrowserServiceCompat

  31. <service android:name=”.MediaPlaybackService” android:exported=”true”> <intent-filter> <action android:name=”android.media.browse.MediaBrowserService” /> </intent-filter> </service> MediaBrowserServiceCompat

  32. public class MediaPlaybackService extends MediaBrowserServiceCompat { private MediaSessionCompat mMediaSession; @Override

    public void onCreate() { ... setSessionToken(mMediaSession.getSessionToken()); } MediaBrowserServiceCompat
  33. @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); }
  34. MediaBrowserCompat mediaBrowser = new MediaBrowserCompat(context, new ComponentName(this, MediaPlaybackService.class), mConnectionCallbacks, null);

    // optional Bundle mediaBrowser.connect(); // Make sure to disconnect()! Connecting
  35. 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
  36. An app divided...on purpose Using a separate process for your

    playback Service
  37. 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.
  38. builder.setStyle(new NotificationCompat.MediaStyle() .setShowActionsInCompactView(0) .setMediaSession(mediaSession.getSessionToken()) .setShowCancelButton(true) .setCancelButtonIntent(MediaStyleHelper.getActionIntent(context, KeyEvent.KEYCODE_MEDIA_STOP))); Handling ongoing Pre-Lollipop

  39. Bound vs Started MediaBrowserServiceCompat is bound to your UI startService()

    lets you continue playing
  40. 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
  41. 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
  42. #mediaplayback +IanLake @ianhlake Thank You!

  43. None