Screenshot: The YouTube Android app

ExoPlayer is an application level media player built on top of Android's low level media APIs. ExoPlayer has a number of advantages over Android's built-in MediaPlayer. 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.

What are we going to build?

In this codelab you're going to build an app which plays audio and video using ExoPlayer.

What you'll need

This codelab is focused on ExoPlayer. Non-relevant concepts and code blocks are glossed over and are provided for you to simply copy and paste.

Get the Code

To get started, download the code:

Download Zip

Alternatively, you can clone the Github repository for the code:

git clone https://github.com/googlecodelabs/exoplayer-intro.git

Directory structure

Cloning or unzipping will provide you with a root folder (exoplayer-intro), which contains one folder for each step of this codelab, along with all of the resources you will 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-NN folders 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

  1. Start Android Studio
  2. Choose File->New->Import Project.
  3. Import the initial project from exoplayer-codelab-00.

Screenshot: Project structure when importing

After the build has finished you'll 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")
}

Our media player Activity is kept in the player-lib module. The reason for keeping it in a separate library module is so we can share it among APKs targeting different platforms, like mobile and Android TV. It also allows us to take advantage of features like Dynamic Delivery which would allow our 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 via jCenter 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

We can add ExoPlayer to our 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 into modules to allow developers to import only the functionality they need. Read more about ExoPlayer's modular structure here.

  1. Open the build.gradle file of the player-lib module.
  2. 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.10.5'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.10.5'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.10.5'

}

Add the PlayerView

  1. Open the layout resource file activity_player.xml from the player-lib module.
  2. Add a PlayerView element. Place the cursor inside the FrameLayout element and start typing <PlayerView and let Android Studio auto-complete the PlayerView element.
  3. Use match_parent for the width and height.
  4. 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 we will refer to this UI element as the video view.

  1. In the PlayerActivity we now need to find the video view so we can properly set it up in the onCreate method of the activity:

PlayerActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    [...]
   playerView = findViewById(R.id.video_view);
}
  1. Add the playerView member field to your PlayerActivity class. Make sure the type of view is PlayerView.

Pro tip: Use Android Studio's Quick Fix feature to add a member field automatically.

Screenshot: Quick fix menu for making a member field

Create an ExoPlayer

To play streaming media we need an ExoPlayer object. The simplest way of creating one is to call ExoPlayerFactory.newSimpleInstance which gives us a SimpleExoPlayer.

SimpleExoPlayer is a convenient, all-purpose implementation of the ExoPlayer interface.

  1. Add a private method initializePlayer to create our SimpleExoPlayer:

PlayerActivity.java

private SimpleExoPlayer player;
[...]
private void initializePlayer() {
     player = ExoPlayerFactory.newSimpleInstance(this);
     playerView.setPlayer(player);
}

We pass our context to newSimpleInstance which creates our SimpleExoPlayer object. This is then assigned to player which we need to declare as a member field. We then use playerView.setPlayer to bind the player to its corresponding view.

Creating a media source

Our player now needs some content to play. For this we need to create a MediaSource. There are many different types of MediaSource, but we'll start by creating a ProgressiveMediaSource for an MP3 file on the internet.

  1. Create a new private method named buildMediaSource.

PlayerActivity.java

private MediaSource buildMediaSource(Uri uri) {
  DataSource.Factory dataSourceFactory =
       new DefaultDataSourceFactory(this, "exoplayer-codelab");
  return new ProgressiveMediaSource.Factory(dataSourceFactory)
       .createMediaSource(uri);
}

This method takes a Uri as its parameter, containing the URI of a media file. The body of the method does quite a few things so let's break it down.

First, we create a DefaultDataSourceFactory, specifying our context and the user-agent string which will be used when making the HTTP request for the media file.

Next, we pass our newly created dataSourceFactory to ProgressiveMediaSource.Factory. This is, as the name suggests, a factory for creating progressive media data sources.

By default the ProgressiveMediaSource.Factory uses a DefaultExtractorsFactory.

Finally, we call createMediaSource, supplying our uri, to obtain a MediaSource object.

  1. Let's use our new method inside initializePlayer by adding the following code:

PlayerActivity.java

private void initializePlayer() {
     [...]
     Uri uri = Uri.parse(getString(R.string.media_url_mp3));
     MediaSource mediaSource = buildMediaSource(uri);
}

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 we release those resources for other apps to use when we're not using them, such as when our app is put into the background.

Put another way, our player's lifecycle should be tied to the lifecycle of our app. To implement this, you'll need to override the four methods onStart, onResume, onPause and onStop of PlayerActivity.

  1. With PlayerActivity open, go to the Code menu -> Override methods...
  2. Select onStart, onResume, onPause and onStop.
  3. We initialize the player in the onStart or onResume 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();
 }
}

Starting with API level 24 Android supports multiple windows. As our app can be visible but not active in split window mode, we need to initialize the player in onStart. Before API level 24 we wait as long as possible until we grab resources, so we wait until onResume before initializing the player.

  1. 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 us to have a full screen experience.

  1. Release resources with releasePlayer (which we'll create shortly) in onPause and onStop:

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

Before API Level 24 there is no guarantee of onStop being called. So we have to release the player as early as possible in onPause. Starting with API Level 24 (which brought multi and split window mode) onStop is guaranteed to be called. In the paused state our activity is still visible so we wait to release the player until onStop.

We now need to create a releasePlayer method which frees up the player's resources and destroys it.

  1. 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 we release and destroy the player we store the following information:

This will allow us to resume playback from where the user left off. All we need to do is supply this state information when we initialize our player.

Final preparation

All we need to do now is to supply the state information we saved in releasePlayer to our player during initialization.

  1. Add the following to initializePlayer:

PlayerActivity.java

private void initializePlayer() {
    [...]
    player.setPlayWhenReady(playWhenReady);
    player.seekTo(currentWindow, playbackPosition);
    player.prepare(mediaSource, false, false);
}

Here's what's happening:

Play audio!

Finally, we are done! Start the app to play the MP3 file and see the embedded artwork:

Screenshot: The application playing a single track

Test the activity lifecycle!

Let's test whether the app works in all the different states of the activity lifecycle:

  1. Start another app and put your app into the foreground again. Does it resume at the correct position?
  2. Pause the application and move it to the background and then the foreground again. Does it stick to paused state when backgrounded in paused state?
  3. Rotate the app. How does it behave if you change the orientation from portrait to landscape and back?

Play video!

Know what? If you want to play video, it's as easy as modifying the uri to an mp4 file.

  1. Change the uri in the initializePlayer to R.string.media_url_mp4
  2. Start the app again and test the behaviour after being backgrounded with video playback as well:

PlayerActivity.java

private void initializePlayer() {
  [...]
  Uri uri = Uri.parse(getString(R.string.media_url_mp4));
  [...]
}

The PlayerView does it all. Instead of the artwork the video is rendered full screen:

Screenshot: The application 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!

Our current app plays a single media file, but what if we wanted to play more than media file one after the other? For that we need a playlist.

Playlists can be created using a ConcatenatingMediaSource. This class allows us to combine media sources and play them seamlessly. It takes care of buffering in the background so the user doesn't see a buffering spinner when changing sources.

Using ConcatenatingMediaSource

Let's concatenate two media sources - the video and audio file from the previous step. They will be played one after the other.

  1. Copy this code and replace the old buildMediaSource method:

PlayerActivity.java

private MediaSource buildMediaSource(Uri uri) {
 // These factories are used to construct two media sources below
 DataSource.Factory dataSourceFactory =
         new DefaultDataSourceFactory(this, "exoplayer-codelab");
 ProgressiveMediaSource.Factory mediaSourceFactory =
         new ProgressiveMediaSource.Factory(dataSourceFactory);

 // Create a media source using the supplied URI
 MediaSource mediaSource1 = mediaSourceFactory.createMediaSource(uri);

 // Additionally create a media source using an MP3
 Uri audioUri = Uri.parse(getString(R.string.media_url_mp3));
 MediaSource mediaSource2 = mediaSourceFactory.createMediaSource(audioUri);

 return new ConcatenatingMediaSource(mediaSource1, mediaSource2);
}

Check how the player controls behave. You can use the previous and next buttons (to the far left and far right of the controls) to navigate in the sequence of concatenated media sources:

Screenshot: Playback controls showing a next and previous button

That's pretty handy! Experiment with it and read more about MediaSource composition and playlists here.

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 will allow.

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. Let's update our app to play an adaptive streaming source by using adaptive track selection.

  1. Update initializePlayer with the following code:

PlayerActivity.java

private void initializePlayer() {

  if (player == null) {
    DefaultTrackSelector trackSelector = new DefaultTrackSelector();
    trackSelector.setParameters(
       trackSelector.buildUponParameters().setMaxVideoSizeSd());
    player = ExoPlayerFactory.newSimpleInstance(this, trackSelector);
  }
  // Remove or comment out
  // player = ExoPlayerFactory.newSimpleInstance(this);
  [...]
}

First, we create a DefaultTrackSelector which is responsible for choosing tracks in the media source. Then we tell our trackSelector to only pick tracks of standard definition or lower - a good way of saving our user's data at the expense of quality. Lastly, we use our trackSelector to create a SimpleExoPlayer instance.

Building an adaptive MediaSource

DASH is a widely used adaptive streaming format. To stream DASH content we need to create a DashMediaSource.

  1. Update buildMediaSource as follows:

PlayerActivity.java

private MediaSource buildMediaSource(Uri uri) {
  DataSource.Factory dataSourceFactory =
          new DefaultDataSourceFactory(this, "exoplayer-codelab");
  DashMediaSource.Factory mediaSourceFactory = new DashMediaSource.Factory(dataSourceFactory);
  return mediaSourceFactory.createMediaSource(uri);
}

By now you should be familiar with this process. We create a factory for data sources, then a factory for DASH media sources. Finally we create the media source using the given URI.

  1. Now all we need is to change our URI to one which points to a DASH media source:
private void initializePlayer() {
  [...]
  Uri uri = Uri.parse(getString(R.string.media_url_dash));
  [...]
}
  1. Restart the app and see adaptive video streaming with DASH in action. It's pretty easy with ExoPlayer!

Other adaptive streaming formats

HLS and SmoothStreaming are other commonly used adaptive streaming formats, both of which are supported by ExoPlayer using HlsMediaSource and SsMediaSource respectively. Check the ExoPlayer demo app for more guidance on how to construct other adaptive media sources.

In the previous steps we learned how to stream progressive and adaptive media streams. ExoPlayer is doing a lot of work for us behind the scenes, including:

Sometimes it's useful to know what ExoPlayer is doing at run-time in order to understand and improve the playback experience for our users.

For example, we might want to reflect playback state changes in the user interface by:

ExoPlayer offers several listener interfaces which provide callbacks for useful events. We'll use a listener to log what state the player is in.

Listen up!

  1. Declare a private member of type PlaybackStateListener in the PlayerActivity.
  2. Also create a TAG constant which we'll use for logging later on.

PlayerActivity.java

private PlaybackStateListener playbackStateListener;
private static final String TAG = PlayerActivity.class.getName();
  1. Instantiate the playbackStateListener at the beginning of onCreate (it won't compile yet but we'll fix this shortly).

PlayerActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_player);

 playbackStateListener = new PlaybackStateListener();
[...]
  1. Use Quick Fix to create the inner class PlaybackStateListener automatically.

Screenshot: Quick Fix menu for creating a missing class

  1. Now have our PlaybackStateListener implement the Player.EventListener interface. This is used to inform us about important player events including errors and playback state changes.
  2. Override onPlayerStateChanged by adding the following code:

PlayerActivity.java

private class PlaybackStateListener implements Player.EventListener {

 @Override
 public void onPlayerStateChanged(boolean playWhenReady,
    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 
       + " playWhenReady: " + playWhenReady);
  }
}

onPlayerStateChanged is called when:

The player can be in one of the following 4 states:

State

Description

ExoPlayer.STATE_IDLE

The player has been instantiated but has not yet been prepared with a MediaSource.

ExoPlayer.STATE_BUFFERING

The player is not able to play from the current position because not enough data has been buffered.

ExoPlayer.STATE_READY

The player is able to immediately play from the current position. This means the player will start playing media automatically if playWhenReady is true. If it is false the player is paused.

ExoPlayer.STATE_ENDED

The player has finished playing the media.

Register our listener

To have our callbacks called we need to register our playbackStateListener with the player. Let's do that in initializePlayer.

  1. Register the listener before the play is prepared:

PlayerActivity.java

private void initializePlayer() {
  if (player == null) {
    [...]
    player.addListener(playbackStateListener); 
    player.prepare(mediaSource, false, false);
}

Again, we need to tidy up to avoid dangling references from the player which could cause a memory leak.

  1. Remove the listener in releasePlayer:

PlayerActivity.java

private void releasePlayer() {
 if (player != null) {
   [...]
   player.removeListener(playbackStateListener);
   player.release();
   player = null;
 }
}
  1. Open logcat and run the app.
  2. Use the UI controls to seek, pause and resume playback. You should see the playback state change in the logs.

Going 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:

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 check out the documentation on listening for player events. That's it for event listeners!

So far we've been using ExoPlayer's PlayerControlView to display a playback controller to the user.

Screenshot: Default playback controller

But what if we 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.

  1. Set use_controller to false and the control does not show up anymore:

activity_player.xml

<com.google.android.exoplayer2.ui.PlayerView
   [...]
   app:use_controller="false"/>
  1. You'll also need to add the following namespace to your FrameLayout:

activity_player.xml

<FrameLayout
  [...] 
  xmlns:app="http://schemas.android.com/apk/res-auto">

Try it out now.

Customizing the behaviour

PlayerControlView has several attributes which affect its behaviour. Let's use show_timeout, fastforward_increment and rewind_increment to customize the controller's behaviour.

  1. Remove app:use_controller="false"
  2. Change the player view to use show_timeout, fastforward_increment and rewind_increment as seen below:

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 attributes of the PlayerControlView can also be set programmatically.

Customizing 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.

Let's look at how we can customize the PlayerControlView.

Create a new layout file custom_playback_control.xml in the folder player-lib/res/layout/. From the context menu of the layout folder choose ‘New - Layout resource file' and name it custom_playback_control.xml.

Screenshot: Custom playback control layout file has been created

  1. The easiest way to get started is to copy the original layout file from here into custom_playback_control.xml.
  2. Now remove the ImageButton elements with the id @id/exo_prev and @id/exo_next.

To use our custom layout we need to set the attribute app:controller_layout_id of the PlayerView element in the activity_player.xml file.

  1. Use the layout id of our custom file like in the snippet below:

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_playback_control"/>
  1. Start the app again. The playback control no longer has the previous and next buttons:

Screenshot: Custom playback controls 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.

  1. Add a android:tint attribute to each ImageButton element:

custom_playback_control.xml

<ImageButton android:id="@id/exo_rew"
   android:tint="#FF00A6FF"
   style="@style/ExoMediaButton.Rewind"/>
  1. Then change all android:textColor attributes you find in your custom file to the same color #FF00A6FF.

custom_playback_control.xml

<TextView android:id="@id/exo_position"
   [...]
   android:textColor="#FF00A6FF"/>
<TextView android:id="@id/exo_duration"
   [...]
   android:textColor="#FF00A6FF"/>
  1. Run the app. Now we have beautiful colored UI components!

Screenshot: Tinted buttons and text view as applied above.

Overriding the default style

We 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_playback_control_view for layout. If we create our own layout file with the same filename the PlayerControlView will use our file instead.

  1. First remove the controller_layout_id attribute you just added.
  2. Delete the file custom_playback_control.xml.
  3. 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"/>
  1. Now create a file named exo_playback_control_view.xml in the res/layout folder of our library module player-lib.
  2. Insert the following code into exo_playback_control_view.xml to add a play button, a pause button and an ImageView with a logo:

exo_playback_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 our custom control and all logic for hiding and showing when interacting with the control is preserved.

Wooow! Great job! You just finished the ExoPlayer codelab. You learned a lot about integrating ExoPlayer into your app:

To learn more about ExoPlayer check out the developer guide, the source code and be sure to subscribe to the ExoPlayer blog.