Upgrade to Pro — share decks privately, control downloads, hide ads and more …

I/O 2016: Best Practices in Media Playback on A...

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
Tweet

More Decks by Ian Lake

Other Decks in Programming

Transcript

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

    build the best audio or video playback app possible
  2. 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()
  3. 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?
  4. // 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
  5. 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)
  6. 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
  7. 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
  8. Android system Android Wear Android Auto Your app Your app

    MediaSessionCompat: your app’s public face MediaControllerCompat MediaSessionCompat Callback Token
  9. 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)
  10. // 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
  11. 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
  12. 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);
  13. // 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+
  14. 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()));
  15. 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
  16. // 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
  17. 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
  18. public class MediaPlaybackService extends MediaBrowserServiceCompat { private MediaSessionCompat mMediaSession; @Override

    public void onCreate() { ... setSessionToken(mMediaSession.getSessionToken()); } MediaBrowserServiceCompat
  19. @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); }
  20. 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
  21. 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.
  22. 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
  23. 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