Best practices in media playback – Google I/O 2016
Articles Blog

Best practices in media playback – Google I/O 2016

IAN LAKE: Hi everyone. Let’s get started. Come on. There’s four more seats
right here in the front. I’m on stage. I’m not going to bite anymore. It’s fine. Hi. My name is Ian Lake. I’m a developer
advocate at Google. I focus primarily on the Android
framework and support library, but more specifically on
the Android media framework. I’m here to talk to you
about best practices in media playback. So a very simple goal
today, and that’s basically just to
tell you when to use the right APIs to build
the best audio or video playback app possible. So throughout this
talk, I’m going to be talking about a lot
of these things in terms of certain events in your
media playback life cycle. So you can imagine
any kind of media app is going to go through all
of these phases at some point. So created is obviously kind
of our one time initialization step, and then we
go into playing. And for playing, what we’re
going to talk about in playing is actually outputting sound–
playing a video, playing audio. That’s the playing state. And on the opposite
side is a paused. So pause is going to be
our shorthand for basically any temporary state where
we’re not actually outputting any audio or any video. And at some point in our
life cycle, sad as it may be, the user may actually
exit out of our app. And at that point, you’re going
to go into the stopped state. That’s kind of the permanent
state where the user has moved on from your app to
maybe a different app, or they’ve just stopped
playback entirely. And of course,
that triggers then onDestroy which would then
clean up all of our resources. Now, I was talking
about media states. All right. And those are slightly different
than our Android lifecycle events. I know. So there’s two cases. One, if you’re a
video playing app– and how many people here
are doing video apps? All right. Quite a few. And audio apps? Nice. Nice. And both? Who’s the overachievers? Yeah. You guys. Nicely done. So for video, you’re
obviously kind of tied to the activity lifecycle. And in this case
of course, onCreate is a great place to do our
one time initialization onDestroy for our clean up. That makes sense. Things have changed slightly
with the introduction of Multi Window for Android N.
Here, previously to N, actually the onStop command
might not be called immediately after your activity ends. So some might hit
the Home button, and you might actually not
get onStop for five seconds afterwards. And if you’re a
video playing app, you may not want to be
playing audio and assuming to play video for five
seconds while your app is in the background. So in this case, we
probably actually want to stop playback in onPause. Right? Because this will be
called immediately when you become not visible
or at least in the background. And plus, obviously that’d be a
disastrous idea because things running side by side, you’ll
be paused but not stopped. Remember stopped is when
you become not visible. So we’ve actually
increased the guarantee that onStop will be
called immediately. So that makes it
the perfect place to actually stop
playback is onStop. Now, for audio apps
or if you’re doing remote playback via
Google Cast, you’re going to be in a service. That’s kind of where we do
background work in Android. And here, we have kind
of onCreate for created, onDestroy for on destroy. So in all these cases, these
are Android lifecycle events that are going to be triggering
a change in your media playback. So you know we didn’t
have anything in the play and paused states for this? That’s because those
aren’t actually tied to Android
lifecycle things. Those are tied to
user interactions. That’s where we’re going
to spend the vast majority of our time here today. So wouldn’t be much
of a media playing app if it didn’t
actually play media. So everyone went to
the ExoPlayer talk. No. The lines was huge. It’s fine. I understand. It’s OK because everything
else in this talk is not going to depend
on you knowing ExoPlayer. Actually everything
here is player agnostic. So from the Android media
framework perspective, it actually doesn’t care if
you’re using ExoPlayer or Media Player or some custom player
that you’re forced to use. It’s fine. They all work. So for example, if we were
using Media Player, mapping this onto kind of our states
for media playback, it’s very simple. onCreate, we’re going to then
create a new media player. Playing, we’re going
to prepare pause. Pause, pause. Stop, stop. Destroy, release. Right? So in this case, it’s
a very simple flow. And it kind of gets us in
the idea of what these media events are actually meaning. So woo. It plays. It paused. We’re done, right? We can all go home. Well, like I said, everything
is player agnostic. So this actually doesn’t
tell anyone anything about what you’re doing. And really, we’re trying
to do this the right way. This isn’t just playing audio. This is making a great user
experience for users, right? We want all of our guys
to be happy like that guy. We don’t want any sad, sunburned
faces here, but happy people. So let’s do a little bit better. And there’s actually a
whole bunch of things that we’re going to cover
today that actually makes for a better user experience. Now, these first
two, Audio Focus and action_becoming_noisy,
are really important for local playback. So if you’re playing a video
or audio on the device itself, these things are
the type of things that are going to make sure that
you’re only actually putting out sound when you mean to. Now, the other two,
MediaSession and Notifications, are something you
should always do. Whether you’re
playing on a device or if you’re using
Google Cast, these are the things that
are actually going to tell the system what’s
going on in your world so it can tell other
apps and other apps can do the type of controls that
Android provides by default. So the first thing
is Audio Focus. It’s kind of the key to good
citizenship in Android when it comes to media playback. And it’s really all about having
apps not talk over one another. If you could imagine
kind of the conch shell that you pass from app to app
saying, you shall now speak, and now you can speak. So this is very much a last
one wins kind of a model. So the last person who
requests audio focus should be the one that the
user is interacting with. Now, this is slightly different
than actually playing audio. We’re actually going to want
to hold onto audio focus through both those playing
and paused state all the way until we’re actually stopped. So really, you think of it
more of as an intent to play versus actually playing. A slight difference here. So I’m going to go
through a lot of code. I hope you’re OK with that. I like code. It’s nice and easy. The slides will be
available afterwards. So don’t worry about
taking pictures. It’s OK. You know. Pictures are great. I’m OK with pictures. But it’ll all be
available online. Don’t worry. And Audio Focus,
pretty straightforward. You call request
Audio Focus when you want to request Audio Focus. And then you’ll really
want to check to make sure it’s actually granted. Now, in 98% of the times
it will just be granted. And you’re fine, and you can go
play music, play your videos. But there are a few special
cases where it actually isn’t granted. So for example, if you
are in a phone call, other apps won’t be
able to get Audio Focus. When you’re in a
phone call, it’s a very specific case
where there’s generally a two way dialogue
going on hopefully the entire time– no awkward
pauses or anything like that. So in that case, you’re
actually not going to be granted Audio Focus. So make sure you actually
check the result at all times. And then of course,
when we’re stopped, we’re going to call
Abandon Audio Focus. Now, there’s an
AudioFocusChangeListener. And the name should be
fairly self-explanatory, but it’s all of the events
that happen around Audio Focus. So this is actually how other
apps tell you what’s going on. So for example, if another
media has requested Audio Focus and said I want to gain
permanent Audio Focus, you’ll actually get an
Audio Focus loss callback. And this is the hammer
that says you’re done. The user has moved on. They’re in a new app. They want to play
audio in that app. So in this case, we’re just
going to stop playback. We’re done. We want to respect the
user’s wish to say, hey, they’ve moved onto a
different app that has said they’re requesting Audio Focus. Now, it’s not always
a permanent loss. Right? There’s more transient losses. So in this case,
transient loss means that another app wants
access to play audio, and they want you to pause. They want you to not
play any more media. And just during
that time, you can imagine they had
something really important to say for a second
or the voice search and things like
use Loss Transient. So then you have kind of a
full dialogue with Google just temporarily without having
to have audio being output at the same time as you’re
trying to give a voice query into Google. Now, the one you’re
probably most familiar with, particularly from
a user perspective, is Transient_Can_Duck. Now, these aren’t the
quack quack ducks. These are the lowering volume,
as in ducking your volume. So in this case,
you’re actually going to just lower your volume,
but you can keep playing. Now, if you’re an app that is
really important, that you’re not missing any words,
say a podcast app. You can actually pause. It’s OK to pause in these
cases– if it’s important, you have spoken words. This is just a suggestion
that you can duck. But in any case, whether
it’s a Loss Transient or Transient_Can_Duck,
you’ll get an Audio Focus gain when the other app
abandons Audio Focus. This kind of brings
you back to where you were, where you
can start playing audio or video at full speed. Now, for video, things are may
be slightly different in that pausing a video
every time you get a notification could be weird. So you might consider actually
just muting rather than pausing. Kind of depends
on your use case. If it’s really important
that the words and audio are in sync, or if it’s OK if
it cuts out for that Loss Transient. So a good way to test this
is say like, your OK Google, to your device while
a video’s playing and try and decide what
makes more sense there. In a lot of cases, it is pause. But if you’re doing
something on like Android TV, maybe muting temporarily
makes more sense. So the other one is
Action_Audio_Becoming_Noisy. And it’s probably one
of my favorite names from a like totally
ridiculous kind of a name but actually is exactly
what’s happening. So these are when you
have headphones plugged in and they become unplugged. And it’s becoming noisy. This actually made sense when
you think about it, right? All of a sudden we’re
switching from headphones to basically the
speaker on your device. Now, in this case, you would
rather not surprise your user. In almost all cases, this maybe
isn’t an intentional thing. You can imagine someone
has it in their pocket, and it gets plugged. It’s not necessarily the
best idea, especially if it’s audio playback. So in this case, we’re
just going to pause, and then they can hit the
Play button if they want. Now in this case, we’re
creating this broadcast receiver programmatically. It’s not in our
manifest, and actually we can’t use manifest
ones for these. And that’s OK because
audio, the Becoming_Noisy, is actually tied to when
you’re outputting sound. So this is only in
the playing state. In this case, as
soon as we pause, we’re not actually playing
any audio or video. Then it’s OK to
unregister, right? Unplugging it when it’s not
actually outputting anything is not something we
actually care about. So now we can update our chart. We have a few more
items in here. So we can see how the
lifecycle is slightly different between
Audio Focus, which goes all the way
until we’re stopped, and Becoming_Noisy, which
is only when we’re actually outputting sound. Now, at this point,
this is everything we need– well, if we’re
running on API 7 devices. Who has a 2.1 device? Yeah. Nobody. We have to do a little
bit more if you actually want to take advantage
of everything that Android’s
offered since 2.1. And a lot of this is just
because user expectations have changed. Android’s changed. Things have changed a lot. You can imagine now– with
Android Wear and Android Auto and Bluetooth headsets–
actually taking your phone out of your pocket to pause might
actually not be the thing that users are used to. If they’re out on
a run, it’s not something they’re going to do. Similarly, if you have controls
on the Android TV remote, they want that Play/Pause
button to actually work. Similarly, they may not actually
want to unlock their device. Aren’t they expecting
controls on their lockscreen? Similarly, do they even need
to enter your app at all? If you have
notifications, there’s actually little reason to go
into your app specifically just to pause or play or
skip to the next track. All right. None of this is even counting
Google Cast and remote playback which wasn’t even close to being
a thing in the 2.1 [INAUDIBLE]. So we have to go a
little bit farther. And in Lollipop– despite
redoing the whole UI thing, which whatever, it’s a minor
thing– also introduced this fancy class
called MediaSession. So MediaSession is kind of the
one stop shop between your app and the rest of the
Android media framework. So it was so useful to have
just a single point for both sending commands to your app
from, say, Bluetooth headsets, as well as giving
information to the system, that we deprecated a
whole bunch of APIs. So no more remote
control client. Yay. All right. No one’s excited about
more remote conrol. All right. Maybe it was before your time. It’s OK. It’s great, but we still
want to support older devices pre-Lollipop. So we built a
MediaSessionCompact. It Backward compatible back to
API 4, before any of this stuff actually existed. You could still
use MediaSession, and it does nothing. That’s OK because it also
does all the translations to the remote
control client, which would be the Ice Cream Sandwich
through KitKat era of Android. It does all that
translation for you. So you get one API to write
across all Android versions, which is pretty sweet. That’s kind of where we
want all of our APIs to be. So when you’re actually
creating a MediaSessionCompat, it’s actually doing a lot more
for you than you might think. So it’s actually creating
what’s called a token, which is actually just a parsable
wrapper around a binder but allows anyone, whether it’s
the Android system or Android Wear or Android Auto or
even your own app, to build a MediaControllerCompats. And these instances
actually allow them to send media buttons to
you, to send controls to you, as well as read what kind of
metadata or playback state– like are you actually
playing something right now or are you buffering? These are the important
things that the system needs to know about and
things like Android Wear need to know about to actually
get your media everywhere. So this is also something
you can use in your own app for building your UI,
and we’ll get back to that in a little bit. So when someone’s
actually sending you a command through
this token, it’s actually going through
your MediaSessionCompat and into your Callback class. Now, you can imagine with
all these events coming in, the Callback class is kind
of a big deal, and it is. It basically has all of the
onPlay, onPause, onSkipToNext, onPlay Track_From_Search. It’s got all of
the methods you’d expect for every one of the
actions that could be sent to your app– as well as even
custom actions– all come in to your Callback class. So it’s actually a really
great place in your app to encapsulate all of
the Media Player calls, all of the ExoPlayer calls,
all of the Google Cast calls, can all be put into
that one Callback class because you know when you get
an onPlay into your callback– no matter where it came
from, whether it was tapping on a button on your
device or hitting a Bluetooth headset– it all is
going to go to that one place. This makes it really powerful
in a way to actually swap between these things. Say you have to support
less than API 16 and you can’t live in the
beautiful ExoPlayer world, you can actually
switch those callbacks or choose at run time what
callback you want to add. So that way, you can use Media
Player or something separate, pre-API 16 and then
ExoPlayer on 16+ and not have to re-architect your whole app
around just swapping between these two and
swapping out calls. So let’s get started
with MediaSessionCompact. So we create it, and one thing
to keep in mind throughout all of MediaSessionCompact is
that it’s very cautious. It assumes you support nothing
until you actually tell it. So we have to set a few flags. You have to say, yes. I actually do want media
buttons from Bluetooth headsets or wired headsets as well
as transport controls. So these would be things like
Android Wear and Android Auto. So we have to actually
tell it every time. Don’t forget these two
lines or else things will magically fail, and
you will not know why. It’s these two lines. And similar to Audio Focus,
this is an intent to play audio. So in this case,
we’re actually going to call setActive,
which says hey! I actually do want to
receive media buttons now when we receive Audio Focus. And then once we’ve stopped,
we can actually say, no. I no longer need
any more events. I’m good. I’m not going to
playback anything else. So we haven’t actually told
the system anything, right? Again, very cautious. So we actually do need to tell
the system what we’re doing. And this comes first
across in PlaybackState. Now, PlaybackState is built
off of two separate pieces. One is the actual
setState itself. So these are like– I am
playing, I am buffering, I am paused. And this is what gives kind
of the control to say, oh. By the way, you should display a
play button rather than a pause button. Or you should display
a buffering circle if your buffering. This all makes a
lot of sense to tell the system what’s going on. Then the other half
of it is setActions. Now, here’s where you
actually declare, yes. I do solemnly swear
I support play/pause or I support skip to next. Not every app needs to
have a Skip to Next button. So in this case, you
need to call setActions for each and every one
of the actions you want. Otherwise, that callback
for that specific class won’t actually ever be called. So if you never say,
I support pausing, there will be no Pause
button event sent to you, which is probably not
actually what you want. But you have to just make
sure that these stay in sync. Now, of course, these are
kind of coupled together, which at first, you’re like. OK. These are all coupled
together for some reason. It turns out that
many times when you’re changing
your state, you’re also changing your actions. Fast forwarding while
you’re buffering probably actually doesn’t make
a whole lot of sense. Maybe it does. But at any point, you can just
create these things together. This is a builder
kind of pattern. So you actually curate a
PlaybackStateCompact builder. So don’t throw away
the builder every time. You can just reuse
the same builder and change just the
one thing you need to. You can just change setState
and not call setActions and then just call
build and call setState. So with this, all of
our media buttons work. Yay! Well, OK. I lied. It works great on API 21+. Turns out it fails
completely on pre-API 21 with a nice handy
error message saying, MediaButtonReceiver
component may not be null. OK. I’ll take your word on it. But what is a media
button receiver component? And why do I need it? Well, it turns out that
media button receivers have a long history in
Android, but it’s something that’s changed a lot. So it’s just a BroadcastReceiver
in your manifest that receives the
media_button action. That’s all it does, but
how it was used changed. Before Lollipop, it
was actually required for basically all of the
Android media framework APIs. So if you didn’t say
I have a media button receiver with a component,
then all of the system wouldn’t actually route
any buttons to you, you wouldn’t show up
on the lockscreen. You basically weren’t
using any of the system. Now, technically you could have
used PendingIntent on API 18 and higher– so like 18 and
19 and then it was all gone. But we actually didn’t
add any CTS tests. So it fails on
certain manufacturers. So from MediaSessionCompat’s
perspective, it’ll always need a component
in your manifest registered if you want to use
MediaSessionCompat prior to API 21. Now, it’s actually still kind of
useful in Lollipop+ even though we have a MediaSession– which
Lollipop is going to be sending you media buttons
directly to your callback. In this case, the
actual component in your manifest kind of has
a secondary optional aspect where it’s only used
to restart playback. So if you’ve ever listened
to Google Play Music and then stopped playback
and then got into your car and hit the Play button,
you’ll note that Google Play Music was the one to start. It knew that it would
get a start command and actually start playback. And that’s because it had
registered a media button receiver. So I really strongly
suggest if you’re doing anything with background
playback or something that should restart,
you should still register a media button
receiver on Lollipop and higher. Because otherwise, it
will be Google Play Music that starts up
even though it may have been your app that the
user was last listening to. So just keep that in mind. So we want to make
this really easy. So we built a class for it
called MediaButtonReceiver, fancy enough. Again, also in the Support
v4 support library. And it has kind of
two main purposes. The first is kind
of specifically around that
background case where if you have a service
that’s handling your MediaSession–
which is, of course, very common for audio apps–
it will auto forward any media button intents
onto your service where you can actually
do something with it. And it’ll choose
between two things. One, if you have a media
browser service in your app, it’ll choose that first. And if you don’t
have one of those, you can actually add the same
media button intent filter to your service, and
MediaButtonReceiver will automatically route it to that. The nice part about this
is that you don’t actually have to write any
MediaButtonReceiver code. You just add it to your
manifest, and that’s it. You’re done. That’s pretty nice because
who wants to write code? Everyone. OK. But the important
part is once you get that intent in your service
or in your activity if you’re doing video playback– wherever
your actual MediaSession is– then we have a simple
one line command to actually take in
your MediaSessionCompat and the intent and
then go through all of the process of
extracting the key event, figuring out what event
that is, and then passing it on to your callback for you. So with just adding it to your
manifest and doing one line of handle intent, all of
a sudden you can now use your Callback class– normally
something that would be API 21+– now for all API levels. So whether you’re using
MediaButtonReceiver on older versions or the native
MediaSession stuff on 21+. Now, it’s actually more code
to enable it on API 21+. So the 23 versions of
the support library actually did this for you. But as of the 24 versions of
support library, the Android N versions, you’ll have to do this
manually if you want to do it. It’s pretty straightforward. You create a PendingIntent
to your action MediaButtonReceiver. Then you call said
MediaButtonReceiver fancy enough. So now all of our
commands work, but there’s more to just commands. There’s another part
of MediaSessionCompat, and that’s around metadata. Now, metadata comes in a
lot of different formats, but it’s basically the what’s
playing aspect of telling the system what’s going on. So this is how
Android Wear gets all of your information on their
device or the lockscreen gets the background art. So at the bare minimum,
I’d suggest a few fields. First, title, artist, album if
you have albums, the duration. If it’s a fixed length
track, having the duration actually allows it to display
how far along you are. And then of course, a bitmap
for the art or the album art. And now, this bitmap shouldn’t
necessarily be too large. This is being sent across
processes and to the Android system. So no 4,000 by 4,000
pixel bitmaps please. But it is the only way to get
lockscreen backgrounds working. So probably should include
a smaller size like 640 by 640 size, somewhere in that
kind of range or lower, just to get something on there. We are looking at a
future version of Android where the lockscreen stuff won’t
actually need a bitmap itself, but you’ll be able to use the
other part, which is a URI. So these allow you to give
a content URI to the app. So then instead of having
to keep that bitmap and send it around through
across multiple processes, you can just send a simple URI. And then this is what
things like Android Auto already use to get
really high quality artwork onto those devices. Now, while that’s really nice
for a lockscreen background art, we don’t actually have
lockscreen controls on Lollipop anymore. That all comes from
media notifications. Notifications are the
new hotness, guys. And it turns out
that you probably should have had a
notification all along because they’re kind
of really useful. Now, they’ve come a long way
since the very beginning. If you can imagine like
back in the 2.1 era, you couldn’t actually have
buttons in your notification. We take that so for granted
now that you can actually have multiple actions. But that only was
actually added in API 14. So there’s a lot of kind
of compactness to this where you’re like, all right. Well, which versions support
what things for media buttons and that type of thing? But you don’t have
to do any of that because
NotificationCompat.MediaStyle does it all for you. Now, it’s still more
boilerplate than I’d like. So I’ve created a
MediaHelper class that I personally like
quite a bit, which takes a context and
your MediaSession and then builds out all
of the things that you’d need from a notification. I’m going to post this link
in these spaces after the talk here. So keep an eye on that if
you want the link itself. But it actually is
using the getDescription from the MediaMetadata,
and this is actually the exact same thing
the Android Wear uses to figure out what text to
display on your device there. So it’s a really nice
way of staying in sync and keeps the 27-ish fields
that are in MediaMetadata down to a much more
manageable number, which you can then use
for your notification. And then of course, the
other part that’s sometimes really tricky for
notifications is actually getting the PendingIntents
for the Play/Pause button, for the Next button. And we still want to reuse all
of the things we’ve already built. We don’t want yet another
custom path of PendingIntends to your callbacks. So in this case, we can
actually build these intents pretty simply just by using this
getActionIntent method which is actually going to reuse our
MediaButtonReceiver that we included in our app. We’re going to reuse
that and kind of fake it so that it’s like a
media button came in, but its in fact a notification. So this way, we actually get
all of the stuff already there. So we create our builder
using the MediaStyle helper. And then we use setSmallIcon
because we kind of need those for notifications. And then we need
to call setColor. Now, I’m not sure if you noticed
the Play Music and stuff did not actually use a bright
orange for their color because it turns out that on
the Lollipop through Marshmallow devices, this was
the full background for your notification. So the giant bright
pink that you use for your branding for
your app, probably not the best color to use here. You’ll note in Android N it’s
actually much more aligned with all of the other
styles where it’s just used as an accent color, not
actually as the giant garish background for the notification. So baby steps. But keep in mind
for a color, this might actually be
something you want to check your API version on. And it wouldn’t be a very
useful media notification if it didn’t have any actions. So in this case, we’re
adding a Pause button to our notification and using
getActionIntent– in this case, just passing in the Play/Pause
key event, just to say, hey, this is a
Play/Pause button. And now this will trigger
all of the same logic as if someone had hit
the Play/Pause button on their headset so they don’t
actually need to build any more code to support these actions. And then of course, we want to
our actual MediaStyle itself. So MediaStyle has
this great property where it supports both an
expanded style– the two line style, where it has larger album
art and up to five actions– as well as the compact
view where you could get only a max of two
actions on that compact view. But it still works. And so you’ll need
to actually tell which actions you want to
support in that compact view. You can imagine if you had
Next Track and then Play/Pause and then Previous Track. You’d probably want to
support the Play/Pause action as the highest most action
because that’s probably the one the users will
want to use the most. It’s a zero base based on the
order you called addAction. Now, the other thing we
really– really important. Don’t forget. Again, this is another
this will break you if you forget it– is
calling setMediaSession. Now, this adds our SessionToken
that we talked about before to the notification. So this is what things
like Android Wear are actually using to get
a reference to your token. So if you forget
this all of a sudden, you’ll get things like, well,
the notification shows up on Wear because we added
the metadata and all that. But then you hit the button, and
it doesn’t actually Play/Pause, which is probably the most
frustrating thing in the world. So don’t forget this line again. So at this point,
we actually have what I would call the
minimum viable product. So this is kind of what I
would consider something that I won’t yell at you for. So if you actually
do all these things, I’ll be reasonably happy. And I would say this
applies to all apps. Whether you’re video
or audio, these are things that you’re
going to need to do. But of course, we’re
much farther along, and we don’t even know
how to build a Pause button for our UI. So we have to build a UI. It’s kind of a
big deal for apps, and we mentioned
MediaControllerCompat is actually really
useful for this because it has access to all
of your metadata and everything else. So we can actually create
a media controller compact either from the token or
from the session itself, and FragmentActivity actually
has some really nice methods that make it easy to set and
then get the MediaController at some point. So here, we can use
just TransportControls and say pause. And we’re using lambdas here. So it looks a little weird,
but it’s a lot shorter on slides, which is awesome. But the TransportControls
is basically the one-to-one with your callbacks. So you can imagine
there’s a Play, a Pause, and a Skip to Next,
which then triggers onPlay, onPause onSkiptoNext. One-to-one. But of course, just
having a Pause button doesn’t actually do a
whole lot for your UI. You want to keep
these things in sync. So you can imagine
in the old days where you had a service
that was doing things and you had to create
some custom situation to actually pass
information back and forth. Now you don’t need
to do that anymore because MediaControllerCompat
gives you all of the things you’d need. So get metadata to
get the metadata, getPlaybackState to
get the playback state. But all of these
things are one shot. So you probably also
want to use a callback. So here, you get a
onMetadataChanged onPlaybackStateChanged,
and this actually allows you to stay perfectly
in sync even across processes. So you’ll actually be sent
the information immediately when you call setState or
when you call setMetadata, it comes across
on the other side. Now, all of what
I’ve talked about has been great for
video and audio. But if we’re doing background
playback with a service, that adds some extra
complexity because we don’t have our
MediaSession.Token in our UI. So we need to do
a little bit more, and we still need that token. Oh. OK. So we built a class for that to. Actually
MediaBrowserServiceCompat does a lot of this stuff for
you besides the actual browsing part– which if you go to any
of the Android Auto talks, will be like the key for them. But we can actually do
this in our own app. So to actually build a
MediaBrowserServiceCompat, it’s pretty straightforward. You add an intent action
to MediaBrowserService. This is what calls you
out as a media browser. And then you extend the class. And then there’s one
method, which again, if you don’t call it,
it breaks everything. That’s setSessionToken
where, again, we kind of give the token
to MediaBrowserService. And there’s two other methods,
onGetRoute and onLoadChildren, which are required
to be implemented. These are really important
for Android Auto. But for us, we just
need to know that you need to return
something not null in onGetRoute for any
connections to succeed. So in this case,
I’m just saying, am I calling myself good? All right. Great. Here’s a route that gives
you no children back, and we’re on with our life. So if MediaBrowserServiceCompat
was one side, MediaBrowserCompat
is the other side. So this is what you’d
actually use in your UI to connect to your service. So in this case, we pass
on the component name, and we just call connect. And it’s actually
binding to your service and doing all that
stuff for you. But the important
part is when you get that connection callback,
it will call onConnected, and it will be able to
call getSessionToken and go about our wonderful
ways of hooking our whole UI up and getting
all of our own information and setting up our callback
and on and on and on and on. And like I mentioned, all of
this works across processes. So you have not had to touch
Android IPC or AIDLs or any of that fun stuff. But it turns out
that you can actually support multiple processes. And if you’re doing audio
playback in a service, it’s actually probably
a fairly good idea to have a separate process. So when you have very different
parts of your life cycle, it’s really important to
keep the long-term memory usage as low as possible. If someone’s going to be playing
audio for multiple hours, you don’t want to be
taking up extra resources. And Android only kills
things at the process level. So if they ever open
your UI, that UI piece is going to stay in memory
as long as your service is. So using a separate service
is actually a really good idea for many reasons, including
things like the WebView process being updated. If you’re using WebView
in your UI process, that’s going to be
killed automatically. And we don’t really want
to pause our music just because WebView updated. It seems kind of silly. And actually, we
never really want to pause our music if
it’s actually playing. We want it to
continue on forever. So to do this on Android, we’re
using a foreground service. Now, foreground services
have one property in that they require
notification, but that’s OK because we
just built our notification. So for media, it actually
makes a lot of sense to use these things together. But there were some problems. The notification use
for foreground services are ongoing, i.e. they
can’t be swiped away. So you can imagine how
frustrating it is to a user. You’re trying to swipe
away that notification. I just want to stop playback. It’s fine. I’m good. But they can’t if you’re
using a foreground service. And it turns out there’s been
a bug for like six years where stopForeground(false) didn’t
actually make it dismissible. Even though that was no
longer our foreground service and the notification
was still there, it would still be ongoing. And you still couldn’t
swipe it away. So we fixed it as
part of a MediaStyle basically by adding a
little x in the corner. Very low tech
solution, but it allows users to hit that x button and
allows them to actually delete the service. In this case, we’re just
sending a stop command. Now, on Lollipop
and higher, once you call stopForeground(false),
you can’t actually swipe to dismiss
the notification. So this basically does nothing
on Lollipop and higher devices. Now, I also mentioned
that MediaBrowserService and MediaBrowserCompat are
binding to the service, which is great because that means that
your service and your process is already up when someone
hits the Play button. So you can instantaneously
use start playback, but it also means as
soon as your UI dies, your service is going to
die with it because it’ll be unbound when there’s no
unbound to it, and you’re dead. But that’s OK. We’ll just need to
call StartService when we start playback. And that way, we make sure
our service will continue even when our activity dies. So you can see our
updated lifecycle of media playback specifically
for the service case. So in this case,
our notifications have been updated slightly
to start foreground when we hit Play,
stop foreground when we hit Pause
because we do want people to be able
to swipe it away when we’re actually paused. And then on Stop, we’re going
to remove our notification. And similarly for the
actual service itself, we really want to make sure
we call setSessionToken. We need to call StartService
when we start playback so we live as long as possible. But of course, when the user
hits the Stop button or x’s out our notification,
we actually do want to call StopSelf
so the system can clean up our service. So what’s next? There is a great
example app called UAMP, our Universal
Android Music Player, which is up on GitHub,
which uses basically all of these technologies all in
one including many, many more. It’s a great app that you
can run on your devices. There’s also a whole
bunch of other talks which I would suggest–
the ExoPlayer talk, which I believe
is already online. If you’re free tonight,
after an after party. The Android Auto talks–
they’re right out this way if you want to get hands on
with Android Auto– as well as Google Cast, the Cast
SDK, and Android Wear for Standalone. So thanks for coming. I’ll be out here for
questions afterwards. Thanks again. [MUSIC PLAYING]

12 thoughts on “Best practices in media playback – Google I/O 2016

  1. Hi Ian! Can you please explain about MediaControllerCompat.Callback's onQueueChanged and onExtrasChanged callbacks? I was playing around media playback stuff for a while and I notice that theese callbacks are never called on my MediaControllerCompat. Few days ago I found this issue – Is this really just a bug in support library or maybe theese callbacks were deprecated from API 21? Can you suggest some workaround before fix?

  2. An app that I'm working on doesn't seem to be receiving the media buttone vents. I've been using the universal media player example as a guide. ( I noticed that it doesn't have the setMediaButtonReceiver in the code mentioned at 27:48. Could this be the reason I am not getting the button events?

  3. I'm now getting problems with notifications being dismissed and reappearing, but then going away permanently. (IT goes away because of a delayedStopHandler activates and kills the notification)
    When I pause audio, I'm calling stopForeground(false) so that the notification remains. If I dismiss the app from the recent list, the onCreate and onStart are called in my MediaBrowserService which causes a "ghost" notification to reappear. Any ideas how to keep this from happening? If I change the server to START_NOT_STICKY I don't get the "ghost" notification on 6.0.1 but still get it on 5.1.1.

  4. Can I use MediaBrowserServiceCompat to develop video playback application with an audio only background (Youtube Red) ?
    My questions are following :
    1) Is it possible to get media metadata and playback position details in other activity to show minicontroller at bottom?

    Please let me know, If I can achieve this using MediaBrowserServiceCompat then I will invest time into it.

  5. This has been really helpful, but a GitHub example to follow along with would be amazing. I'm having trouble attaching all of these different classes together in my head and having code to look at would be super helpful.

  6. Thank you for the lecture. I find MediaButtonReceiver a little bit intricate. I like your "Media playback the right way (Big Android BBQ 2015)" lecture which tell about how to make it works on prior-Lollipop.

  7. how can I handle queue manager in media application. Is it good to manage queue using methods provided by "mediasession" such as "setqueue" or "setqueueitem" etc. Is there any advantages using it or we have to implement own logic to handle song queue manager.

  8. Good stuff ! Can someone explain how internally mediasession communicate between two android devices? Is it wifi ? Is it taken care by Andriod itself?

Leave a Reply

Your email address will not be published. Required fields are marked *

Back To Top