[MUSIC PLAYING] NEVIN MITAL: Hi, everyone. Welcome to the media streaming
with ExoPlayer workshop. My name is Nevin, and I'm a
developer relations engineer on the Android team. ExoPlayer is an open
source media player that you can include in your app
for audio and video playback. Unlike Android's built-in
media player class, it's easy to
customize and extend and supports features such as
adaptive streaming, playlists, and DRM-protected playback. ExoPlayer is
available in Jetpack as part of the
Media3 library, which is a collection of support
libraries for implementing media use cases on Android. Today, I'd like to show
you how simple it can be to add ExoPlayer to your app. If you'd like to follow along
in Android Studio yourself, you can search for media
streaming with ExoPlayer to find the Codelab
I'll be going through. Or head to developer.Androi
d.com/codelabs/ExoPlayer-intro to get started. To get the most out
of this work-through, you'll want to be familiar
with the basics of Android development. But otherwise, we're starting
today's app from scratch. That being said,
let's get into it. We've prepared a project on
our Codelabs GitHub repository that you can use as
a starting point. Cloning this repo will get you
set up with a blank project. And it also includes
folders with completed code at various steps
during the Codelab. Switching over to
Android Studio, I've already
imported the project. And I'm ready to
start with step 00. The application module
named app is actually empty aside from an
Android manifest file and only has a Gradle
dependency on the currently specified ExoPlayer-codelab
library module. Setting up separate
modules like this is a great way to share
code among different APKs. For example, if you're
targeting different platforms like mobile and Android TV. I've also got an emulator
running so I can build and test my app as we go. If I run the project
right off the bat, the app should launch
and fill the screen with a blank background. Looks like we're ready to go. First things first, we need to
add ExoPlayer to the project. Let's navigate to
the build.gradle file in the ExoPlayer-codelab-00
module. As of this recording, the latest
release of the Media3 library is 1.0.0-alpha03. The Media3 library is split
into several different modules so you can import only the
functionality that you need. For this workshop, we'll include
the following three modules-- Media3-ExoPlayer, which adds
the actual player implementation called ExoPlayer that
we'll use, Media3-UI, which adds the UI element called
player view that we'll use to add a playback
view to our activity, and Media3-ExoPlayer-- which adds support for dynamic
adaptive streaming for later on in the workshop. Make sure to sync your
project after adding these dependencies. Next, let's add the
player view to our UI. Open the layout resource file
activity underscore player.xml. Here, inside the frame layout
is where the player view element will go. For both the height and the
width, I'm using match parent. And I've also set the ID
to video underscore view. Is the ID that I'll use to
refer to the UI element going forward. Jumping into the
player activity now, we can get a
reference to the view tree created by the XML file. This will initialize the view
binding variable the first time it is used, which in our case
will be the on create lifecycle callback for the activity. Speaking of which, in
the on create callback, we can set the root
of the view tree as the content view
of the activity. To double-check things,
we can make sure that the video view property
is available from the view binding. Looks like the type
is player view. So everything looks good so far. Now that our UI
is set up, we need to create an ExoPlayer
object to actually play a streaming media. I'll start by declaring a
member field called player. Let's create a private helper
method called initialize player to build our ExoPlayer instance. In this method, we'll use
the ExoPlayer.builder class to create an ExoPlayer object
using the activities context. Don't forget to call
the build method. We're assigning this newly
created ExoPlayer object to our player member field. But additionally,
we'll bind this object to the player property
of our video view. And now that we
have a player, we need to give it some
content to play. For this, we have to
build a media item. Let's start with
creating a media item for an MP3 file that's
available on the internet. We've already provided
a URL for this file in the strings.xml
resource file. Back inside the
initialized player method, we can create the media item
using the fromUri method. Once it's created, we
can add it to the player by calling set media item. This player object
has the potential to use up a lot of device
resources, including memory, CPU, network connections, and
hardware codecs, many of which are in short supply. To make sure that we're
being a good Android citizen, we should tie the
player's life cycle to the life cycle of our app
so that the player's resources are released to other apps
when we're not using them. There are four life cycle
methods of the player activity for us to override. Let's start with the on start
and on resume methods Android API level 24 and up
support multiple Windows, which means your app can
be visible but not active. In these cases,
your player should be initialized in on start. So we'll call our initialized
player helper function if the API level
is higher than 23. Prior to API level 24, you
should wait as long as possible to initialize the player. So for that case,
we'll use on resume. We'll also add a
helper method called hide system UI to enable
a full-screen experience. Now, we'll turn our attention
to on pause and on stop. Similar to before, with
API level 24 and up, your app might be paused
but still visible. So we'll only release the
player's resources in on stop. Prior to API level 24,
there's no guarantee that on stop will be called. So we release resources
in on pause instead. We'll add a release
player helper method to take care of freeing up
resources and destroying our player. But before releasing
the resources, we'll want to record three
pieces of information. Play when ready, which
stores the play pause state of the player;
current item, which stores the index of
the current media item; and playback position, which
stores the current playback position within the
current media item. These three items allow
us to resume playback from where the user
left off when they put the app into the background. Up top, we'll initialize
these variables so that by default, the
player will automatically start playback from the
beginning of the first media item. We can now go back to the
initialized player method to use the state information
that we've stored. We'll instruct
the player whether or not to start playing
immediately once all required resources have been
acquired, which media item to start with and
at what position within it, and finally, to acquire
all the required resources. And that's it. We have everything we
need for playback to work. We can launch the app
to play the MP3 file and see the embedded artwork. This is a good
opportunity for you to test that the app
works as expected in all the different
activity lifecycle states. [MUSIC PLAYING] But what if we wanted
to play a video instead? This can be as easy as
modifying the media item Uri to an MP4 file
instead of an MP3 file. We've already provided a
URL to try this out as well in the strings.xml file. If we start up the
app again, you'll see that the player view
is handling everything. And instead of the
artwork, the video is being rendered
in full screen mode. [MUSIC PLAYING] OK. So we have a single
media file playing now. But ExoPlayer also makes it easy
to play multiple media files one after the other. We can create a playlist
by adding multiple media items to the player
using add media item. ExoPlayer will handle
buffering in the background to allow for seamless playback. The player also
includes move media item and remove media
item methods for you to modify the
playlist on the fly. Now if we restart
the app, we can use the next and
previous buttons provided by the player view
to navigate between our media items. [MUSIC PLAYING] Next, I'd like to talk
about adaptive streaming. Adaptive streaming refers to
a technique in streaming media where the player
varies the quality of the stream based on the
available network bandwidth. Adaptive media content generally
consists of several tracks with different qualities such
as bit rates and resolutions. Each track is also split into
smaller chunks, typically between two and 10 seconds. ExoPlayer player can
choose one of these tracks based on the available
network bandwidth. The smaller subdivisions
of each track also allow ExoPlayer to quickly
switch between these tracks as the amount of bandwidth
available changes. ExoPlayer will handle
stitching the audio together for seamless playback even as
it performs these switches. Back in our initialized
player method, let's start by creating
a default track selector, which is in
charge of choosing tracks within media items. We'll also instruct
it to only pick tracks of standard
definition or lower. This is one option at your
disposal for saving your users' data at the expense of quality. Lastly, we'll pass
the track selector into the builder for creating
an ExoPlayer instance. We also need to
provide the player with a media item in an
adaptive streaming format. Dynamic adaptive streaming over
HTTP, otherwise known as DASH, is one such popular format. Instead of using
media item.fromUri, we'll need to use a
media item builder. This will give us more
freedom to specify the format of the media item. Some properties that we can
set using the builder class include the MIME
type of the content, subtitle files to
use during playback, and protected content
properties such as the DRM type. Same as before,
we've provided a URL to content in the dash
format for you to use, so we can update our
initializer player method with the media item
that uses the new URL and has a MIME type of
MPD, which tells ExoPlayer that the content is
in the DASH format. With that done,
all we have to do is restart the app to see
this new file being played. We can also see the adaptive
streaming in action. I'll double-tap shift to open
the search everywhere bar. I'm looking for the adaptive
track selection class, and specifically the update
selected track method. This is the method
that's called when the track is being updated. I'll put a breakpoint
towards the end and restart the
app in debug mode. This way, you can see each
time the track selector needs to decide which track to use. Of course, DASH is only one
adaptive streaming format. Other commonly used formats
are HLS and smooth streaming, both of which are also
supported by ExoPlayer. So far, we've learned
that ExoPlayer is handling a lot
of things for us behind the scenes, such as
allocating memory, downloading the media content and
extracting its metadata, decoding the media file, and
rendering it on the device. But you might find it
helpful to understand what ExoPlayer player is doing
at runtime so you can improve the experience accordingly. For example, you may want to
display a loading spinner when the player is buffering or
display a watch next overlay when the media item has ended. Luckily, ExoPlayer provides
a listener interface that we can use to get
callbacks for useful events. In the player activity, we
need a private player.listener member variable. And I'll create a tag constant
outside the player activity that we can use for
logging in a moment. Now, also outside the
player activity class, let's implement the
player.listener interface inside a factory function. And we'll override
the on playback state changed callback. This callback will trigger
any time the playback state changes. So we can use it to print the
current state of the player. The player can be in
one of four states. Idol, meaning that
the player has been created, but not prepared. Buffering, meaning
that the player hasn't buffered enough
data yet and can't play from the current position. Ready, meaning that the
player can immediately play from the current
position once the play when ready property
is set to true. And ended, which means that
the player has finished playing the provided media. But how would you know if
your player is actually playing media? There are a few factors
that must be true. First, playback
state must be ready. Second, the play when ready
property must be true. And lastly, playback
must not be suppressed for some other reason, such
as loss of audio focus. To make this easier
to know, ExoPlayer provides both an
is playing method and an on is playing callback. We can update our
listener to also log changes in the playing state. Now that we've defined what
our callback should do, we need to register the
listener with our player. Back in the initialized
player method, we'll add our new listener
prior to the prepare call. And remember, that we'll need
to free up the listener's resources as well. So in our released
player helper method, we can remove the listener
before releasing the player. If we rerun the app with logcat
open in a separate window, we can use the UI controls
to navigate playback and see the relevant playback
state changes in the logs. [MUSIC PLAYING] SPEAKER 1: OK class,
record a video. NEVIN MITAL: ExoPlayer
player offers a number of other callbacks
for specific audio and video events. For example, on
rendered first frame is called when the first
frame of a video is rendered. This can be useful
in calculating how long the user had to
wait until playback started. Another callback worth
calling out is on events. This callback reports
all state changes that happen in a
single iteration together and also gives
you access to the player object within the callback. This can be useful if you
want to use multiple state values together or if you
need the player object to use a getter method or
to trigger further events. For example, if I
wanted to update the UI whether the playback state
changed, or the is playing property changed, we could
override on events callback as follows. As another example, if we wanted
to skip ahead by five seconds any time the media
item transitions, we could issue a seek command
to the player when the desired event is detected. I'll also add our
second media item back in so we can see this
new callback in action. Now when we switch
media items, notice that the timestamp
starts at five seconds, since we're skipping ahead as
soon as we get a media item transition event. [MUSIC PLAYING] And with that, I've shared
all I have for you today. We covered a lot of
ground, including adding ExoPlayer to your
project, creating media items and taking advantage
of adaptive streaming, and responding to playback
events to improve the user experience on the fly. But of course, this is just
the tip of the iceberg. Head over to the ExoPlayer
docs in our Android Media Docs to learn more. Thanks for tuning in, and
have a great Google IO. [MUSIC PLAYING]