Screenshot: The YouTube Android app
ExoPlayer is an app-level media player built on top of low-level media APIs in Android. ExoPlayer has a number of advantages over the built-in MediaPlayer in Android. It supports many of the same media formats as MediaPlayer, plus adaptive formats, such as DASH and SmoothStreaming. ExoPlayer is highly customizable and extensible, making it capable of many advanced use cases. It is an open source project used by Google apps, including YouTube and Google Play Movies & TV.
Prerequisites
- Moderate knowledge of Android development and Android Studio
What you'll do
- Create a
SimpleExoPlayer
instance, which prepares and plays media from a variety of sources. - Integrate ExoPlayer with the app's activity lifecycle to support backgrounding, foregrounding, and playback resumption in a single or multi-window environment.
- Use
MediaItem
s to create a playlist. - Play adaptive video streams, which adapt the media quality to the available bandwidth.
- Register event listeners to monitor playback state and show how listeners can be used to measure the quality of playback.
- Use standard ExoPlayer UI components, then customize them to your app's style.
What you'll need
- Android Studio version 3.5 or higher
- An Android device with JellyBean (4.1) or higher, ideally with Nougat (7.1) or higher as it supports multiple windows.
Get the code
To get started, download the Android Studio project:
Alternatively, you can clone the GitHub repository:
git clone https://github.com/googlecodelabs/exoplayer-intro.git
Directory structure
Cloning or unzipping provides you with a root folder (exoplayer-intro
), which contains one folder for each step of this codelab, along with all the resources you need:
/PATH/TO/YOUR/FOLDER/exoplayer-intro/exoplayer-codelab-00
/PATH/TO/YOUR/FOLDER/exoplayer-intro/exoplayer-codelab-01
/PATH/TO/YOUR/FOLDER/exoplayer-intro/exoplayer-codelab-02
/PATH/TO/YOUR/FOLDER/exoplayer-intro/exoplayer-codelab-03
/PATH/TO/YOUR/FOLDER/exoplayer-intro/exoplayer-codelab-04
The exoplayer-codelab-N
folders (where N
is 00
to 04
) contain the desired end state of each step of this codelab. These are standalone Android Studio projects, which can each be imported.
Import the initial project
- Start Android Studio.
- Choose File > New > Import Project*.*
- Import the initial project from
exoplayer-codelab-00
.
Screenshot: Project structure when importing
After the build finishes, you see two modules: the app
module (of type application) and the player-lib
module (of type library). The app
module is actually empty, having only a manifest. Everything from the player-lib
module is merged when the app is built using a gradle dependency in app/build.gradle
.
app/build.gradle
dependencies {
implementation project(":player-lib")
}
Your media player Activity is kept in the player-lib
module. The reason for keeping it in a separate library module is so you can share it among APKs targeting different platforms, such as mobile and Android TV. It also allows you to take advantage of features, such as Dynamic Delivery, which allow your media playback feature to be installed only when the user needs it.
- Deploy and run the app to check everything is fine. The app should fill the screen with a black background.
Screenshot: Blank app running
Add ExoPlayer dependency
ExoPlayer is an open source project hosted on GitHub. Each release is distributed through Google's Maven repository, which is one of the default package repositories used by Android Studio and Gradle. Each release is uniquely identified by a string with the following format:
com.google.android.exoplayer:exoplayer:rX.X.X
You can add ExoPlayer to your project simply by importing its classes and UI components. It's pretty small, having a shrunken footprint of about 70-to-300 kB depending on the included features and supported formats. The ExoPlayer library is split in to modules to allow developers to import only the functionality they need. For more information about ExoPlayer's modular structure, see Add ExoPlayer modules.
- Open the
build.gradle
file of theplayer-lib
module. - Add the following lines to the
dependencies
section and sync the project.
player-lib/build.gradle
dependencies {
[...]
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.0'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.0'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.0'
}
Add the PlayerView element
- Open the layout resource file
activity_player.xml
from theplayer-lib
module. - Place the cursor inside the
FrameLayout
element. - Start typing
<PlayerView
and let Android Studio autocomplete thePlayerView
element. - Use
match_parent
for thewidth
andheight
. - Declare the id as
video_view
.
activity_player.xml
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
Going forward, you refer to this UI element as the video view.
- In the
PlayerActivity
, you now need to find the video view so you can properly set it up in theonCreate
method of the activity.
PlayerActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
[...]
playerView = findViewById(R.id.video_view);
}
- Add the
playerView
member field to yourPlayerActivity
class. Make sure the type of view isPlayerView
.
Note: Use the Quick Fix feature in Android Studio to add a member field automatically. Remember to set the type to
PlayerView
rather than the default
View
.
Screenshot: Quick fix menu for making a member field
Create an ExoPlayer
To play streaming media, you need an ExoPlayer
object. The simplest way of creating one is to use the SimpleExoPlayer.Builder
class. As the name suggests, this uses the builder pattern to build a SimpleExoPlayer
instance.
SimpleExoPlayer
is a convenient, all-purpose implementation of the ExoPlayer
interface.
Add a private method initializePlayer
to create your SimpleExoPlayer
.
PlayerActivity.java
private SimpleExoPlayer player;
[...]
private void initializePlayer() {
player = new SimpleExoPlayer.Builder(this).build();
playerView.setPlayer(player);
}
Create a SimpleExoPlayer.Builder
using your context, then call build
to create your SimpleExoPlayer
object. This is then assigned to player
, which you need to declare as a member field. You then use playerView.setPlayer
to bind the player
to its corresponding view.
Create a media item
Your player
now needs some content to play. For this, you create a MediaItem
. There are many different types of MediaItem
, but you start by creating one for an MP3 file on the internet.
The simplest way to create a MediaItem
is to use MediaItem.fromUri
, which accepts the URI of a media file. Add the MediaItem
to the player
using player.setMediaItem
.
- Add the following code to
initializePlayer
:
PlayerActivity.java
private void initializePlayer() {
[...]
MediaItem mediaItem = MediaItem.fromUri(getString(R.string.media_url_mp3));
player.setMediaItem(mediaItem);
}
Note that R.string.media_url_mp3
is defined as https://storage.googleapis.com/exoplayer-test-media-0/play.mp3 in strings.xml
.
Playing nice with the Activity lifecycle
Our player
can hog a lot of resources including memory, CPU, network connections and hardware codecs. Many of these resources are in short supply, particularly for hardware codecs where there may only be one. It's important that you release those resources for other apps to use when you're not using them, such as when your app is put into the background.
Put another way, your player's lifecycle should be tied to the lifecycle of your app. To implement this, you need to override the four methods of PlayerActivity
: onStart
, onResume
, onPause
, and onStop
.
- With
PlayerActivity
open, click Code menu > Override methods.... - Select
onStart
,onResume
,onPause
, andonStop
. - Initialize the player in the
onStart
oronResume
callback depending on the API level.
PlayerActivity.java
@Override
public void onStart() {
super.onStart();
if (Util.SDK_INT >= 24) {
initializePlayer();
}
}
@Override
public void onResume() {
super.onResume();
hideSystemUi();
if ((Util.SDK_INT < 24 || player == null)) {
initializePlayer();
}
}
Android API level 24 and higher supports multiple windows. As your app can be visible, but not active in split window mode, you need to initialize the player in onStart
. Android API level 24 and lower requires you to wait as long as possible until you grab resources, so you wait until onResume
before initializing the player.
- Add the
hideSystemUi
method.
PlayerActivity.java
@SuppressLint("InlinedApi")
private void hideSystemUi() {
playerView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
}
hideSystemUi
is a helper method called in onResume
, which allows you to have a full-screen experience.
- Release resources with
releasePlayer
(which you create shortly) inonPause
andonStop
.
PlayerActivity.java
@Override
public void onPause() {
super.onPause();
if (Util.SDK_INT < 24) {
releasePlayer();
}
}
@Override
public void onStop() {
super.onStop();
if (Util.SDK_INT >= 24) {
releasePlayer();
}
}
With API Level 24 and lower, there is no guarantee of onStop
being called, so you have to release the player as early as possible in onPause
. With API Level 24 and higher (which brought multi- and split-window mode), onStop
is guaranteed to be called. In the paused state, your activity is still visible, so you wait to release the player until onStop
.
You now need to create a releasePlayer
method, which frees the player's resources and destroys it.
- Add the following code to the activity:
PlayerActivity.java
private boolean playWhenReady = true;
private int currentWindow = 0;
private long playbackPosition = 0;
[...]
private void releasePlayer() {
if (player != null) {
playWhenReady = player.getPlayWhenReady();
playbackPosition = player.getCurrentPosition();
currentWindow = player.getCurrentWindowIndex();
player.release();
player = null;
}
}
Before you release and destroy the player, store the following information:
- Play/pause state using
getPlayWhenReady
. - Current playback position using
getCurrentPosition
. - Current window index using
getCurrentWindowIndex
. For more information about windows, see Timeline.
This allows you to resume playback from where the user left off. All you need to do is supply this state information when you initialize your player.
Final preparation
All you need to do now is to supply the state information you saved in releasePlayer
to your player during initialization.
- Add the following to
initializePlayer
:
PlayerActivity.java
private void initializePlayer() {
[...]
player.setPlayWhenReady(playWhenReady);
player.seekTo(currentWindow, playbackPosition);
player.prepare();
}
Here's what's happening:
setPlayWhenReady
tells the player whether to start playing as soon as all resources for playback have been acquired. BecauseplayWhenReady
is initiallytrue
, playback starts automatically the first time the app is run.seekTo
tells the player to seek to a certain position within a specific window. BothcurrentWindow
andplaybackPosition
are initialized to zero so that playback starts from the very start the first time the app is run.prepare
tells the player to acquire all the resources required for playback.
Play audio
Finally, you are done! Start the app to play the MP3 file and see the embedded artwork.
Screenshot: The app playing a single track.
Test the activity lifecycle
Test whether the app works in all the different states of the activity lifecycle.
- Start another app and put your app in the foreground again. Does it resume at the correct position?
- Pause the app, and move it to the background and then the foreground again. Does it stick to a paused state when backgrounded in paused state?
- Rotate the app. How does it behave if you change the orientation from portrait to landscape and back?
Play video
If you want to play video, it's as easy as modifying the media item URI to an MP4 file.
- Change the URI in the
initializePlayer
toR.string.media_url_mp4
. - Start the app again and test the behaviour after being backgrounded with video playback as well.
PlayerActivity.java
private void initializePlayer() {
[...]
MediaItem mediaItem = MediaItem.fromUri(getString(R.string.media_url_mp4));
[...]
}
The PlayerView
does it all. Instead of the artwork, the video is rendered full screen.
Screenshot: The app playing video.
You rock! You just created an app for full-screen media streaming on Android, complete with lifecycle management, saved state, and UI controls!
Your current app plays a single media file, but what if you want to play more than one media file, one after the other? For that, you need a playlist.
Playlists can be created by adding more MediaItem
s to your player
using addMediaItem
. This allows seamless playback and buffering is handled in the background so the user doesn't see a buffering spinner when changing media items.
- Add the following code to
initializePlayer
:
PlayerActivity.java
private void initializePlayer() {
[...]
player.setMediaItem(mediaItem); // Existing code
MediaItem secondMediaItem = MediaItem.fromUri(getString(R.string.media_url_mp3));
player.addMediaItem(secondMediaItem);
[...]
}
Check how the player controls behave. You can use and
to navigate the sequence of media items.
Screenshot: Playback controls showing a next and previous button
That's pretty handy! For more information, see the developer documentation on Media Items and Playlists, and this article about the Playlist API.
Adaptive streaming is a technique for streaming media by varying the quality of the stream based on the available network bandwidth. This allows the user to experience the best-quality media that their bandwidth allows.
Typically, the same media content is split into multiple tracks with different qualities (bit rates and resolutions). The player chooses a track based on the available network bandwidth.
Each track is split into chunks of a given duration, typically between 2 and 10 seconds. This allows the player to quickly switch between tracks as available bandwidth changes. The player is responsible for stitching these chunks together for seamless playback.
Adaptive track selection
At the heart of adaptive streaming is selecting the most appropriate track for the current environment. Update your app to play adaptive streaming media by using adaptive track selection.
- Update
initializePlayer
with the following code:
PlayerActivity.java
private void initializePlayer() {
if (player == null) {
DefaultTrackSelector trackSelector = new DefaultTrackSelector(this);
trackSelector.setParameters(
trackSelector.buildUponParameters().setMaxVideoSizeSd());
player = new SimpleExoPlayer.Builder(this)
.setTrackSelector(trackSelector)
.build();
}
// Remove or comment out.
// player = new SimpleExoPlayer.Builder(this).build();
[...]
}
First, create a DefaultTrackSelector
, which is responsible for choosing tracks in the media item. Then, tell your trackSelector
to only pick tracks of standard definition or lower—a good way of saving your user's data at the expense of quality. Lastly, pass your trackSelector
to your builder so that it is used when building the SimpleExoPlayer
instance.
Build an adaptive MediaItem
DASH is a widely used adaptive streaming format. To stream DASH content, you need to create a MediaItem
as before. However, this time, we must use a MediaItem.Builder
rather than fromUri
.
This is because fromUri
uses the file extension to determine the underlying media format but our DASH URI does not have a file extension so we must supply a MIME type of APPLICATION_MPD
when constructing the MediaItem
.
- Update
initializePlayer
as follows:
PlayerActivity.java
private void initializePlayer() {
[...]
// Replace this line
MediaItem mediaItem = MediaItem.fromUri(getString(R.string.media_url_mp4));
// With this
MediaItem mediaItem = new MediaItem.Builder()
.setUri(getString(R.string.media_url_dash))
.setMimeType(MimeTypes.APPLICATION_MPD)
.build();
// Also remove the following lines
MediaItem secondMediaItem = MediaItem.fromUri(getString(R.string.media_url_mp3));
player.addMediaItem(secondMediaItem);
}
- Restart the app and see adaptive video streaming with DASH in action. It's pretty easy with ExoPlayer!
Other adaptive streaming formats
HLS (MimeTypes.APPLICATION_M3U8
) and SmoothStreaming (MimeTypes.APPLICATION_SS
) are other commonly used adaptive streaming formats, both of which are supported by ExoPlayer. For more information about constructing other adaptive media sources, see the ExoPlayer demo app.
In the previous steps, you learned how to stream progressive and adaptive media streams. ExoPlayer is doing a lot of work for you behind the scenes, including the following:
- Allocating memory
- Downloading container files
- Extracting metadata from the container
- Decoding data
- Rendering video, audio, and text to the screen and loudspeakers
Sometimes, it's useful to know what ExoPlayer is doing at runtime in order to understand and improve the playback experience for your users.
For example, you might want to reflect playback state changes in the user interface by doing the following:
- Displaying a loading spinner when the player goes into a buffering state
- Showing an overlay with "watch next" options when the track has ended
ExoPlayer offers several listener interfaces that provide callbacks for useful events. You use a listener to log what state the player is in.
Listen up
- Declare a private member of type
PlaybackStateListener
in thePlayerActivity
. - Create a
TAG
constant, which you use for logging later.
PlayerActivity.java
private PlaybackStateListener playbackStateListener;
private static final String TAG = PlayerActivity.class.getName();
- Instantiate the
playbackStateListener
at the beginning ofonCreate
(it won't compile yet, but you fix this shortly).
PlayerActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_player);
playbackStateListener = new PlaybackStateListener();
[...]
- Use Quick Fix to create the inner class
PlaybackStateListener
automatically.
Screenshot: Quick Fix menu for creating a missing class
- Implement the
Player.EventListener
interface. This is used to inform you about important player events, including errors and playback state changes. - Override
onPlaybackStateChanged
by adding the following code:
PlayerActivity.java
private class PlaybackStateListener implements Player.EventListener {
@Override
public void onPlaybackStateChanged(int playbackState) {
String stateString;
switch (playbackState) {
case ExoPlayer.STATE_IDLE:
stateString = "ExoPlayer.STATE_IDLE -";
break;
case ExoPlayer.STATE_BUFFERING:
stateString = "ExoPlayer.STATE_BUFFERING -";
break;
case ExoPlayer.STATE_READY:
stateString = "ExoPlayer.STATE_READY -";
break;
case ExoPlayer.STATE_ENDED:
stateString = "ExoPlayer.STATE_ENDED -";
break;
default:
stateString = "UNKNOWN_STATE -";
break;
}
Log.d(TAG, "changed state to " + stateString);
}
}
onPlaybackStateChanged
is called when the playback state changes. The new state is given by the playbackState
parameter.
The player can be in one of the following four states:
State | Description |
| The player has been instantiated, but has not yet been prepared. |
| The player is not able to play from the current position because not enough data has been buffered. |
| The player is able to immediately play from the current position. This means the player will start playing media automatically if the player's playWhenReady property is |
| The player has finished playing the media. |
Register your listener
To have your callbacks called, you need to register your playbackStateListener
with the player. Do that in initializePlayer
.
- Register the listener before the play is prepared.
PlayerActivity.java
private void initializePlayer() {
if (player == null) {
[...]
player.addListener(playbackStateListener);
player.prepare();
}
Again, you need to tidy up to avoid dangling references from the player which could cause a memory leak.
- Remove the listener in
releasePlayer
:
PlayerActivity.java
private void releasePlayer() {
if (player != null) {
[...]
player.removeListener(playbackStateListener);
player.release();
player = null;
}
}
- Open logcat and run the app.
- Use the UI controls to seek, pause, and resume playback. You should see the playback state change in the logs.
Go deeper
ExoPlayer offers a number of other listeners, which are useful in understanding the user's playback experience. There are listeners for audio and video, as well as an AnalyticsListener
, which contains the callbacks from all the listeners. Some of the most important methods are the following:
onRenderedFirstFrame
is called when the first frame of a video is rendered. With this, you can calculate how long the user had to wait to see meaningful content on the screen.onDroppedVideoFrames
is called when video frames have been dropped. Dropped frames indicate that playback is janky and the user experience is likely to be poor.onAudioUnderrun
is called when there has been an audio underrun. Underruns cause audible glitches in the sound and are more noticeable than dropped video frames.
AnalyticsListener
can be added to the player
with addAnalyticsListener
. There are corresponding methods for the audio and video listeners as well.
Think about what events are important to your app and your users. For more information, see Listening for player events. That's it for event listeners!
So far, you've been using ExoPlayer's PlayerControlView
to display a playback controller to the user.
Screenshot: Default playback controller
What if you want to change the functionality or look and feel of these controls? Luckily, these controls are highly customizable.
The first simple customization is to not use the controller at all. This can be done easily by using the use_controller
attribute on the PlayerView
element inside activity_player.xml
.
- Set
use_controller
tofalse
and the control does not show up anymore:
activity_player.xml
<com.google.android.exoplayer2.ui.PlayerView
[...]
app:use_controller="false"/>
- Add the following namespace to your
FrameLayout
:
activity_player.xml
<FrameLayout
[...]
xmlns:app="http://schemas.android.com/apk/res-auto">
Try it now.
Customize the behaviour
PlayerControlView
has several attributes which affect its behaviour. Use show_timeout
, fastforward_increment
, and rewind_increment
to customize the controller's behaviour.
- Remove
app:use_controller="false"
. - Change the player view to use
show_timeout
,fastforward_increment
andrewind_increment
:
activity_player.xml
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:show_timeout="10000"
app:fastforward_increment="30000"
app:rewind_increment="30000"/>
- The
show_timeout
value tells thePlayerView
the delay in milliseconds before the control is hidden after the user last interacted with it. - The
fastforward_increment
andrewind_increment
values tell the player the time in milliseconds to jump forward or backward when the user taps the fast-forward or rewind buttons.
The attributes of the PlayerControlView
can also be set programmatically.
Customize the appearance
Well, that's a good start. But what if you want to have the PlayerControlView
look different or change which buttons are displayed? The implementation of PlayerControlView
does not assume that any buttons exist, so it's easy to remove them and add new ones.
Look at how you can customize the PlayerControlView
.
- Create a new layout file
custom_player_control_view.xml
in the folderplayer-lib/res/layout/
. - From the context menu of the layout folder, choose New - Layout resource file and name it
custom_player_control_view.xml
.
Screenshot: The layout file for the player control view has been created.
- Copy the original layout file from here into
custom_player_control_view.xml
. - Remove the
ImageButton
elements with the id@id/exo_prev
and@id/exo_next
.
To use your custom layout, you need to set the attribute app:controller_layout_id
of the PlayerView
element in the activity_player.xml
file.
- Use the layout ID of your custom file like in the following code snippet:
activity_player.xml
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:controller_layout_id="@layout/custom_player_control_view"/>
- Start the app again. The player control view no longer has the previous and next buttons.
Screenshot: Custom player control view with no previous or next buttons
You can apply whatever changes you like in the layout file. By default, the colors of the Android theme are chosen. You can override this to match the design of your app.
- Add a
android:tint
attribute to eachImageButton
element:
custom_player_control_view.xml
<ImageButton android:id="@id/exo_rew"
android:tint="#FF00A6FF"
style="@style/ExoMediaButton.Rewind"/>
- Change all
android:textColor
attributes you find in your custom file to the same color:#FF00A6FF
.
custom_player_control_view.xml
<TextView android:id="@id/exo_position"
[...]
android:textColor="#FF00A6FF"/>
<TextView android:id="@id/exo_duration"
[...]
android:textColor="#FF00A6FF"/>
- Run the app. Now you have beautiful, colored UI components!
Screenshot: Tinted buttons and text view
Override the default style
You just created a custom layout file and referenced it using controller_layout_id
in activity_player.xml
.
Another approach is to override the default layout file which PlayerControlView
uses. The source code of PlayerControlView
tells us that it uses R.layout.exo_player_control_view
for layout. If you create our own layout file with the same filename, the PlayerControlView
uses your file instead.
- Remove the
controller_layout_id
attribute you just added. - Delete the file
custom_player_control_view.xml
.
The PlayerView
in activity_player.xml
should now look like this:
activity_player.xml
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
- Create a file named
exo_player_control_view.xml
in theres/layout
folder of your library moduleplayer-lib
. - Insert the following code into
exo_player_control_view.xml
to add a play button, a pause button, and anImageView
with a logo:
exo_player**_control_view.xml**
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layoutDirection="ltr"
android:background="#CC000000"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="4dp"
android:orientation="horizontal">
<ImageButton android:id="@id/exo_play"
style="@style/ExoMediaButton.Play"/>
<ImageButton android:id="@id/exo_pause"
style="@style/ExoMediaButton.Pause"/>
</LinearLayout>
<ImageView
android:contentDescription="@string/logo"
android:src="@drawable/google_logo"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
This demonstrates how you can add your own elements here and mix them with standard control elements. ExoPlayerView
now uses your custom control and all logic for hiding and showing when interacting with the control is preserved.
Congratulations! You learned a lot about integrating ExoPlayer with your app.
Learn more
To learn more about ExoPlayer check out the developer guide and source code, and subscribe to the ExoPlayer blog.