Slide 1

Slide 1 text

Media playback the right way Ian Lake Developer Advocate, Google

Slide 2

Slide 2 text

About me ~5 years of Android Development experience Previously at Phunware, Facebook Developer Advocate at Google ● Advanced Android Udacity Course ● Android Development Patterns

Slide 3

Slide 3 text

Media playback Specifically audio Video: github.com/googlecast/CastCompanionLibrary-android

Slide 4

Slide 4 text

User expectations

Slide 5

Slide 5 text

Background playback = Service

Slide 6

Slide 6 text

Important events in media playback Created Playing Paused Stopped Destroyed

Slide 7

Slide 7 text

Playing audio MediaPlayer* *Or ExoPlayer: github.com/google/ExoPlayer Created Playing Paused Stopped Destroyed MediaPlayer new prepare -> play pause stop release Mission Accomplished!

Slide 8

Slide 8 text

Media playback the right way What do we still need?

Slide 9

Slide 9 text

Audio Focus // 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); Ensure apps don’t talk over one another Hold audio focus until we’ve stopped playback

Slide 10

Slide 10 text

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)

Slide 11

Slide 11 text

Lifecycle of media playback Created Playing Paused Stopped Destroyed MediaPlayer new prepare -> play pause stop release Audio Focus request focus abandon focus Done! Not quite...

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Lifecycle of media playback Created Playing Paused Stopped Destroyed MediaPlayer new prepare -> play pause stop release Audio Focus request focus abandon focus NOISY register unregister Minimum viable product?

Slide 14

Slide 14 text

Controls everywhere

Slide 15

Slide 15 text

Headphones and bluetooth controls ‘Media buttons’ android.intent.action.MEDIA_BUTTON broadcasts sent by the system Use Support Library provided MediaButtonReceiver

Slide 16

Slide 16 text

But … not working?

Slide 17

Slide 17 text

Preferred media button receiver Becoming the chosen one

Slide 18

Slide 18 text

Your app <--> media APIs Callbacks MediaSessionCompat // In onCreate() mMediaSession = new MediaSessionCompat( context, LOG_TAG); mMediaSession.setFlags( MediaSessionCompat. FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat. FLAG_HANDLES_TRANSPORT_CONTROLS); mMediaSession.setCallback( new ExampleCallbacks());

Slide 19

Slide 19 text

// Right after audio focus mMediaSession.setActive(true); // On stop mMediaSession.setActive(false); // In onStartCommand() MediaButtonReceiver.handleIntent( mMediaSession, intent); setActive(boolean) setActive(boolean) handleIntent() links media buttons to Callback methods MediaSessionCompat

Slide 20

Slide 20 text

PlaybackStateCompat MediaSessionCompat PlaybackStateCompat setState() setActions() setState()

Slide 21

Slide 21 text

Lock screen controls Requires an image Set via setMetadata(MediaMetadataCompat) Metadata isn’t just for the lock screen - Android Wear also uses it!

Slide 22

Slide 22 text

Important Metadata Fields Bitmap ● METADATA_KEY_ART ● METADATA_KEY_ALBUM_ART Uri ● METADATA_KEY_ART_URI ● METADATA_KEY_ALBUM_ART_URI Also consider METADATA_KEY_USER_RATING Text ● METADATA_KEY_TITLE ● METADATA_KEY_ARTIST ● METADATA_KEY_ALBUM ● METADATA_KEY_ALBUM_ARTIST Long ● METADATA_KEY_DURATION

Slide 23

Slide 23 text

Lifecycle of media playback Created Playing Paused Stopped Destroyed MediaPlayer new prepare -> play pause stop release Audio Focus request focus abandon focus NOISY register unregister MediaSession Compat new set flags set callback setActive(true) set metadata set state set state setActive(false) release Lock screen on 5.0+?

Slide 24

Slide 24 text

Notifications and media controls NotificationCompat.MediaStyle ●

Slide 25

Slide 25 text

public static NotificationCompat.Builder from( Context context, MediaSessionCompat mediaSession) { } public static NotificationCompat.Builder from( Context context, MediaSessionCompat mediaSession) { MediaControllerCompat controller = mediaSession.getController(); MediaMetadataCompat mediaMetadata = controller.getMetadata(); MediaDescriptionCompat description = mediaMetadata.getDescription(); } public static NotificationCompat.Builder from( Context context, MediaSessionCompat mediaSession) { MediaControllerCompat controller = mediaSession.getController(); MediaMetadataCompat mediaMetadata = controller.getMetadata(); MediaDescriptionCompat description = mediaMetadata.getDescription(); NotificationCompat.Builder builder = new NotificationCompat.Builder(context); builder .setContentTitle(description.getTitle()) .setContentText(description.getSubtitle()) .setSubText(description.getDescription()) .setLargeIcon(description.getIconBitmap()) .setContentIntent(controller.getSessionActivity()) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setDeleteIntent(getActionIntent(context, KeyEvent.KEYCODE_MEDIA_STOP)); return builder; }

Slide 26

Slide 26 text

public static PendingIntent getActionIntent( Context context, int mediaKeyEvent) { Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, mediaKeyEvent)); return PendingIntent.getBroadcast(context, mediaKeyEvent, intent, 0); } Full code at gist.github.com/ianhanniballake/47617ec3488e0257325c

Slide 27

Slide 27 text

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()));

Slide 28

Slide 28 text

Lifecycle of media playback Created Playing Paused Stopped Destroyed MediaPlayer new prepare -> play pause stop release Audio Focus request focus abandon focus 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

Slide 29

Slide 29 text

Foreground Services Caveat: always ongoing pre-Lollipop

Slide 30

Slide 30 text

Handling ongoing pre-Lollipop builder.setStyle(new NotificationCompat.MediaStyle() .setShowActionsInCompactView(0) .setMediaSession(mediaSession.getSessionToken()) .setShowCancelButton(true) .setCancelButtonIntent(MediaStyleHelper.getActionIntent(context, KeyEvent.KEYCODE_MEDIA_STOP));

Slide 31

Slide 31 text

Lifecycle of media playback Created Playing Paused Stopped Destroyed MediaPlayer new prepare -> play pause stop release Audio Focus request focus abandon focus 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)

Slide 32

Slide 32 text

Service <-> UI

Slide 33

Slide 33 text

MediaControllerCompat getMetadata() -> MediaMetadataCompat getPlaybackState() -> PlaybackStateCompat getTransportControls() -> 1:1 with Callback MediaControllerCompat.Callback

Slide 34

Slide 34 text

MediaControllerCompat requires a Token

Slide 35

Slide 35 text

MediaBrowserService / MediaBrowser API to connect and retrieve a Token New APIs for browsing available media items Required for Android Auto integration Adds browse option on Android Wear notification

Slide 36

Slide 36 text

Implementing MediaBrowserService Call setSessionToken() in onCreate() New methods: ● onGetRoot() ● onLoadChildren() ● onLoadItem() See example Universal Android Music Player (UAMP) github.com/googlesamples/android-UniversalMusicPlayer

Slide 37

Slide 37 text

Using MediaControllerCompat Bind to Service, build API to retrieve Token ● Pros: ensures Service will be alive and ready while UI is up ● Cons: complicated Static getSessionToken() method in Service, send a local broadcast on change ● Pros: simple ● Cons: no multi-process, still need to start the Service

Slide 38

Slide 38 text

Lifecycle of media playback Created Playing Paused Stopped Destroyed MediaPlayer new prepare -> play pause stop release Audio Focus request focus abandon focus 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) MediaBrowser Service set session token

Slide 39

Slide 39 text

Google+ +IanLake Twitter @ianhlake Contact