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 and supports many of the same media formats as MediaPlayer plus the adaptive formats DASH and SmoothStreaming. ExoPlayer is highly customizable and extendable and can be adapted closely to specific use cases. It is used by Google apps like YouTube and Google Play Movies, and is developed as an open source project hosted on GitHub.

What are we going to build?

In this codelab, you're going to build an Activity for media playback 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 from Github

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

Directory structure

Cloning or unzipping will will provide you with a root folder (exoplayer-codelab-io17), which contains one folder for each step of this codelab, along with all of the resources you will need:

/PATH/TO/YOUR/FOLDER/exoplayer-codelab-io17/exoplayer-codelab-00
/PATH/TO/YOUR/FOLDER/exoplayer-codelab-io17/exoplayer-codelab-01
/PATH/TO/YOUR/FOLDER/exoplayer-codelab-io17/exoplayer-codelab-02
/PATH/TO/YOUR/FOLDER/exoplayer-codelab-io17/exoplayer-codelab-03

The exoplayer-codelab-NN folders contain the desired end state of each step of this codelab at the end. These are Android Studio projects which can be each imported with Android Studio as it's own project. Start with exoplayer-codelab-00 with the initial state of the project which is an empty Activity in a standard fullscreen theme.

Import the project

Start Android Studio and choose File - New - Import Project.. Import the initial project from exoplayer-codelab-00. After the build has finished you'll see two modules: the app module (of type application aka apk module) and the player-lib module (of type library). The app module is actually empty; having only a manifest and merges in everything from the player-lib module by a simple gradle dependency.

It makes sense to have our media player Activity separated in a library project so we can share it among different apks like for mobile or Android TV. Specifically with Android Instant Apps (AIA) it will soon become a common requirement for Android apps to be modularized in so called feature splits or atoms. So we are better off with a little bit of more complexity to start with.

Deploy and run the app to check everything is fine. It's a black nothing in fullscreen.

Add ExoPlayer dependency

ExoPlayer is an open source project hosted on Github. Each release is distributed via jCenter repositories which are leveraged by gradle and Android Studio:

com.google.android.exoplayer:exoplayer:rX.X.X

So we can simply add a dependency to ExoPlayer to import player classes and UI components. It's pretty small and has pro-guarded a footprint of about 70 to 300 Kb depending on the use case and how many formats must be supported by an app. The ExoPlayer library is split into modules to allow developers to import only a subset of the functionality provided by the full library. Read this blog post to learn more about the modularization.

Open the build.gradle file of the player-lib module and add the following line to the dependencies section and sync the project like Android Studio suggests after editing gradle files:

dependencies {
   [...]
   compile 'com.google.android.exoplayer:exoplayer:r2.4.0'
}

Add the SimpleExoPlayerView

Open the layout resource file activity_player.xml from the player-lib module and add a SimpeExoPlayerView element. Place the cursor inside the FrameLayout element and start typing <SimpleExoPlayerView or <SEPV, confirm the offered option to let the IDE auto-complete the element. Then use match_parent for width and height of the video view. Further we declare the id video_view:

<com.google.android.exoplayer2.ui.SimpleExoPlayerView
   android:id="@+id/video_view"
   android:layout_width="match_parent"
   android:layout_height="match_parent"/>

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

@Override
protected void onCreate(Bundle savedInstanceState) {
    [...]
   playerView = (SimpleExoPlayerView) findViewById(R.id.video_view);
}

This addition requires us to add the simpleExoPlayerView member field to our PlayerActivity class. Use your IDE to complete this semi-automatically.

Ladies and gentleman! Start your engine!

Creating a player instance and a media source is all we need for streaming media. In the initializePlayer method of the PlayerActivity we create a new instance of SimpleExoPlayer. It's a very convenient default, all-purpose implementation of the ExoPlayer interface which is the public interface of the actual player in the ExoPlayer library. Let's create our private method initializePlayer to get such an instance:

private void initializePlayer() {
     player = ExoPlayerFactory.newSimpleInstance(
         new DefaultRenderersFactory(this),
         new DefaultTrackSelector(), new DefaultLoadControl());

     playerView.setPlayer(player);

     player.setPlayWhenReady(playWhenReady);
     player.seekTo(currentWindow, playbackPosition);
}

As the snippet above shows, we just pass the default components DefaultRenderersFactory, DefaultTrackSelector and DefaultLoadControl to the factory method newSimpleInstance, assign the returned instance to the member field player which you must declare, and pass the player instance to the playerView.

The last two lines is about the meat of a proper setup and of playback resumption: declare the member fields playWhenReady, currentWindow and playbackPosition and use these to resume the playback state with setPlayWhenReady() and seekTo(). Obviously we need to set these members later when playback is interrupted. We are well prepared!

We're almost there. An ExoPlayer instance is waiting for media. We need to create a MediaSource. That's the next step in which we create an ExtractorMediaSource for an MP3 file with an embedded artwork in it's ID3 tags. Complete the initializePlayer method by adding these lines at the end:

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

The first line creates a hardcoded Uri pointing to an mp3 file on the internet. Then a MediaSource is created which we just delegate to a method buildMediaSource(uri). Finally the player is prepared with the media source at the last line. The boolean flags passed to prepare along with the source indicate whether to reset position and state of the player.

We've separated building the MediaSource into it's own method buildMediaSource:

private MediaSource buildMediaSource(Uri uri) {
  return new ExtractorMediaSource(uri,
      new DefaultHttpDataSourceFactory("ua"),
      new DefaultExtractorsFactory(), null, null);
}

The method constructs and returns a ExtractorMediaSource for the given uri. We simply use a new DefaultHttpDataSourcFactory which only needs a user agent string. The third parameter is the DefaultExtractorFactory which supports almost all non-adaptive audio and video formats supported on Android. It will recognize our mp3 file and play it nicely.

Blending into the activity life cycle

We're all set now, but we need to call our initializePlayer method properly within the life-cycle of our activity. It's important to acquire and release codec resources shared with other apps properly. Nicely blending into the Activity lifecycle is key for media apps to work properly.

For this, override the four methods onStart, onResume, onPause and onStop of PlayerActivity (Android Studio: Code - Override methods...). The IDE creates the methods by just calling the super implementation.

We initialize the player either in the onStart or onResume callback according to API level:

@Override
public void onStart() {
 super.onStart();
 if (Util.SDK_INT > 23) {
   initializePlayer();
 }
}

@Override
public void onResume() {
 super.onResume();
 hideSystemUi();
 if ((Util.SDK_INT <= 23 || 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.

BTW: hideSystemUi called in onResume is just an implementation detail to have a pure full screen experience:

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

Accordingly we need to release resources with the yet to be created releasePlayer method in onPause and onStop:

@Override
public void onPause() {
 super.onPause();
 if (Util.SDK_INT <= 23) {
   releasePlayer();
 }
}

@Override
public void onStop() {
 super.onStop();
 if (Util.SDK_INT > 23) {
   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 and in the paused mode our activity is eventually still visible. Hence we need to wait releasing until onStop.

Tidy up!

It's not compiling now! We need to create the releasePlayer method to tidy up nicely:

private void releasePlayer() {
  if (player != null) {
    playbackPosition = player.getCurrentPosition();
    currentWindow = player.getCurrentWindowIndex();
    playWhenReady = player.getPlayWhenReady();
    player.release();
    player = null;
  }
}

To be able to resume properly when our app for instance comes to foreground again, we need to store the playback state in playbackPosition, currentWindow and playWhenReady. We already have member variables for these and update these now. Then release the player and null the player member. Done! With this we released framework resources like hardware codecs which we share with other apps and are now available for them to use. For some of these codecs it may have only one instance so not releasing it would harm other apps. Wait until users find about the culprit! Your app is uninstalled quickly. So being a good citizen is not altruistic behaviour but for our very own benefit.

Play audio!

We are done! Start the app to play the mp3 file and see the embedded artwork.

Test the activity lifecycle! Start another app and put your app into the foreground again: does it resume at the correct position? Does it stick to pause state when backgrounded in paused state? And how does it behave if you change orientation from portrait to landscape and back?

Play video!

Know what? We are done with this as well. Change the uri in the initializePlayer to R.string.media_url_mp4 instead of R.string.media_url_mp3, start the app again and test the behaviour after being backgrounded with video playback as well:

Uri uri = Uri.parse(getString(R.string.media_url_mp4));

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

You rock! You just managed to have a rock solid full screen activity for proper media streaming on Android!

We just learned about the MediaSource which we create in the buildMediaSource method and pass it to the prepare method of ExoPlayer. We used an ExtractorMediaSource for our first steps with ExoPlayer. ExtractorMediaSource is suitable for regular media files (mp4, webm, mkv etc). The ExoPlayer library also provides MediaSource implementations for adaptive formats like DASH (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource).

We'll look into adaptive streaming later. For now we want to explore MediaSources composition. The ExoPlayer library also provides LoopingMediaSource and ConcatenatingMediaSource with which we can combine media sources and play them seamlessly in nifty ways.

Given we want to play our mp3 file endlessly,

Using ConcatenatingMediaSource

There's more. Let's concatenate two media sources (a video and an audio file) to be played in sequence like in a play list (and totally without buffering!):

private MediaSource buildMediaSource(Uri uri) {
  // these are reused for both media sources we create below
  DefaultExtractorsFactory extractorsFactory =
      new DefaultExtractorsFactory();
  DefaultHttpDataSourceFactory dataSourceFactory =
      new DefaultHttpDataSourceFactory( "user-agent");

  ExtractorMediaSource videoSource =
      new ExtractorMediaSource(uri, dataSourceFactory,
      extractorsFactory, null, null);

  Uri audioUri = Uri.parse(getString(R.string.media_url_mp3));
  ExtractorMediaSource audioSource =
      new ExtractorMediaSource(audioUri, dataSourceFactory,
      extractorsFactory, null, null);

  return new ConcatenatingMediaSource(audioSource, videoSource);
}

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

That's pretty handy! Experiment with it and read more about MediaSource composition in our blog on Medium.

In a nutshell adaptive video playback cuts video and audio files into multiple chunks of a given duration. A player then links them together for playback. The chunks are available in different qualities (size or bitrate). With this a player can choose the quality of each chunk according to the network bandwidth available. After starting with a low quality chunk to be able to render the first frame as quickly as possible, the player switches to a better quality for the second chunk if sufficient bandwidth is available (for instance on wifi vs. mobile).

Adaptive track selection

At the heart of adaptive streaming is selecting the most appropriate track to adapt to the environment in which streaming happens. Let's enable adaptive track selection.

Adaptive playback implies estimating available network bandwidth based on measured download speed. Let's declare a constant DefaultBandwidthMeter in the PlayerActivity for this:

private static final DefaultBandwidthMeter BANDWIDTH_METER =
    new DefaultBandwidthMeter();

The task of actually selecting a track is with the DefaultTrackSelector. We're already passing it to the newSimpleInstance method in our initializePlayer method. We now wire up an AdaptiveTrackSelection.Factory in inititalizePlayer and pass it to the constructor of the DefaultTrackSelector. This makes our track selection adaptive:

private void initializePlayer() {
  if (player == null) {
    // a factory to create an AdaptiveVideoTrackSelection
    TrackSelection.Factory adaptiveTrackSelectionFactory =
        new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);

    player = ExoPlayerFactory.newSimpleInstance(
        new DefaultRenderersFactory(this),
        new DefaultTrackSelector(adaptiveTrackSelectionFactory), 
        new DefaultLoadControl());
    [...]

Building an adaptive MediaSource

DASH is a widely used adaptive streaming format. Streaming DASH with ExoPlayer, means building an appropriate adaptive MediaSource. To switch our app to DASH we build a DashMediaSource by changing our buildMediaSource method as follows:

private MediaSource buildMediaSource(Uri uri) {
  DataSource.Factory dataSourceFactory =
      new DefaultHttpDataSourceFactory("ua", BANDWIDTH_METER);
  DashChunkSource.Factory dashChunkSourceFactory =
      new DefaultDashChunkSource.Factory(dataSourceFactory);

  return new DashMediaSource(uri, dataSourceFactory,
      dashChunkSourceFactory, null, null);
}

Note the BANDWIDTH_METER which is passed to the constructor of DefaultHttpDataSourceFactory. This makes sure the bandwidth meter is informed about downloaded bytes and enables to collect data for bandwidth estimation.

Of course we now need a uri pointing to a DASH manifest instead of to an mp4 file. We again change the url. This time we use R.string.media_url_dash:

 Uri uri = Uri.parse(getString(R.string.media_url_dash));

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 also supported by ExoPlayer. There is an HlsMediaSource and a SsMediaSource which can be constructed pretty similarly. Check the demo app to find sample code on how to construct all sorts of adaptive MediaSources.

In the previous steps we learned how to stream conventional and adaptive media streams with ExoPlayer. Under the hood a lot is going on. Starting from memory allocation, downloading container files, extracting media data from container, decoding data and in the end rendering video, audio and text media onto a user interface like screen and loudspeakers.

Mostly we don't need to know in detail what is going on under the hood but in some cases we wish to. Maybe we want to reflect state changes in the user interface in some ways. For instance we want to display a loading spinner when the player goes into buffering state or show an overlay with ‘watch next' options just when the track has ended.

For being informed SimpleExoPlayer offers a couple of listener interfaces which we can implement and register to get the callbacks called in which we are interested.

A brief thought on style

As mentioned we need to implement multiple interfaces. To not pollute the public API of our class we declare an inner member class which then will implement these interfaces. That's a nice little coding pattern for the sake of a clean API.

Declare a private member of the yet not existing type ComponentListener in the PlayerActivity and instantiate the componentListener at the beginning of onCreate. Just like so (yes, it's not yet compiling):

private ComponentListener componentListener;

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

 componentListener = new ComponentListener();
 [...]

Let's now use the power of the IDE to create the inner class ComponentListener automatically:

Screenshot: Create an inner member class ComponentListener.

Implement an ExoPlayer.EventListener

It's a member class, so it has no static keyword. Now we make the ComponentListener implement the ExoPlayer.EventListener interface which is the most important listener. Then use the power of the IDE again. This time to provide a no-op implementation for all the callbacks required by the interface:

Screenshot: Implement ExoPlayer.EventListener with no-op callbacks.

The most interesting callback is the onPlayerStateChanged callback. It's the only callback which is implemented in the code snippet below. Change your implementation of onPlayerStateChanged to be the same:

private class ComponentListener implements ExoPlayer.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);
}

 @Override
 public void onTimelineChanged(Timeline timeline, Object manifest) {}
 @Override
 public void onTracksChanged(TrackGroupArray trackGroups,
     TrackSelectionArray trackSelections) {}
 @Override
 public void onLoadingChanged(boolean isLoading) {}
 @Override
 public void onPlayerError(ExoPlaybackException error) {}
 @Override
 public void onPositionDiscontinuity() {}
 @Override
 public void onPlaybackParametersChanged(PlaybackParameters
     playbackParameters) {}
}

onPlayerStateChanged is called when the player either transitions from one playback state to another or if playback is paused or set to play.

Paused/play is expressed with the boolean playWhenReady parameter passed to onPlayerStateChanged. The boolean flag indicates a paused or player player (true for playing). This corresponds with the method ExoPlayer.setPlayWhenReady(boolean playWhenReady) which actually is the ExoPlayer API for play and pause.

The playbackState parameter reports what it says: the state of the playback. There are four of them:

State

Description

ExoPlayer.STATE_IDLE

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

ExoPlayer.STATE_BUFFERING

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

ExoPlayer.STATE_READY

The player is able to immediately play from the current position. This means the player does actually play media when playWhenReady is true. If it is false the player is paused.

ExoPlayer.STATE_ENDED

The player has finished playing the media.

Register an ExoPlayer.EventListener

To have our callbacks called, we need to add our componentListener to the player. That's simple and we do it in the initializePlayer method:

private void initializePlayer() {
 if (player == null) {
   [...]
   player = ExoPlayerFactory.newSimpleInstance(this,
       new DefaultTrackSelector(adaptiveVideoTrackSelectionFactory),
       new DefaultLoadControl());
   
   player.addListener(componentListener); 
   [...]
}

Again we need to tidy up to avoid unwanted references from the player which might run us into memory leak problems. So we remove the listener in releasePlayer:

private void releasePlayer() {
 if (player != null) {
   [...]
   player.removeListener(componentListener);
   player.release();
   player = null;
 }
}

Now we can start and observe logcat when we start the app and use the UI like for example seeking to various positions, pausing and resuming playback. It all goes to our component listener which logs state changes.

Using VideoRendererEventListener and AudioRendererEventListener

Besides state changes we are mostly interested in the Quality of Experience (QoE) of our users. This means getting information about the rendering of video and audio. Worst thing which can happen are dropped video frames and audio underruns. Both mean a significant loss of QoE for our users and we want to know when this is happening.

SimpleExoPlayer is delegating VideoRendererEventListener and AudioRendererEventListener to it's internal renderers. Let's register our component listener as such:

private void initializePlayer() {
  [...]
  player.addListener(componentListener);
  player.setVideoDebugListener(componentListener);
  player.setAudioDebugListener(componentListener);
  [...]
}

You know the drill. First, make ComponentListener implement VideoRendererEventListener:

And then let ComponentListener implement AudioRendererEventListener:

Connecting to for instance Firebase Analytics or Google Analytics is now as easy as creating AnalyticsListener class which implements those 4 listener interfaces like the ComponentListener does.

Know what? Clean up nicely! As always we do this in the releasePlayer method of the PlayerActivity:

private void releasePlayer() {
 if (player != null) {
   [...]
   player.removeListener(componentListener);
   player.setVideoListener(null);
   player.setVideoDebugListener(null);
   player.setAudioDebugListener(null);
   player.release();
   player = null;
 }
}

Collecting signals of negative impact on Quality of Experience (QoE)

With all these listeners we are well armed to collect enough signals to monitor the quality of experience we are delivering. Here are a couple of listener callbacks which are useful for this purpose:

Start thinking about how you want to measure now - in terms of implementation you are on it already with finishing this step. Congratulations!

The UI of ExoPlayer is kind of minimal but being the controller of the player for the user, it is super important. The component used by ExoPlayer is the PlaybackControlView which is highly customizable:

The first simple customization is whether to not use the controller at all. This can be done easily by using the use_controller attribute on the SimpleExoPlayerView element. Set it to false and the control does not show up anymore:

<com.google.android.exoplayer2.ui.SimpleExoPlayerView
   [...]
   app:use_controller="false"/>

Try it out or continue with the next step.

Customizing the behaviour of PlaybackControlView

PlaybackControlView has a couple of attributes to determine its behaviour. These attributes can be set by declaring attributes of the SimpleExoPlayerView in the xml layout file. Let's do a first customization and use the attributes show_timeout, fastforward_increment and rewind_increment and set it to some custom values like in the example here:

<com.google.android.exoplayer2.ui.SimpleExoPlayerView
   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 the SimpleExoPlayerView the time after which the control is hidden after the user used it. The config above sets this value to 10000 milliseconds. With this the controls are hidden 10 seconds after the user interacted with the control. If you set the value to 0 (zero), the control is never hidden automatically but by user interaction only.

The increment values we've set above determine the time in millisecond ExoPlayer seeks forward of backwards when the user hits the fast forward or the rewind button. Just set it to some value which make sense for you.

The Java Doc of the PlaybackControlView explains in detail what other options are available. Certainly these attributes can be set programmatically as well. Check it out.

Customizing the appearance of PlaybackControlView

Well, that's a good start. But what if you want to have the PlaybackControlView look different than by default? Or what if you don't want all the buttons which are provided by default or you want to add your own buttons? The implementation of PlaybackControlView does not assume the existence of any button. So with removing stuff - anything goes.

Let's look at how we can customize the PlaybackControlView specifically for a given instance of the SimpleExoplayerView. 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':

Screenshot: Custom playback control layout file has been created


The easiest way is to start with is to just copy the original layout file. Get it from Github here and replace the content of custom_playback_control.xml. Then remove the ImageButton elements with the id @id/exo_prev and @id/exo_next.

To make ExoPlayer use our custom layout we need to set the attribute app:controller_layout_id of the SimpleExoPlayerView element in the activity_player.xml file. Use the layout id of our custom file like in the snippet below:

<com.google.android.exoplayer2.ui.SimpleExoPlayerView android:id="@+id/video_view"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   app:controller_layout_id="@layout/custom_playback_control_view"/>

Start the app again. The playback control now has three buttons only.

You can apply whatever change 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 or the CI of your company. Add a android:tint attribute to each ImageButton element:

<ImageButton android:id="@id/exo_rew"
   android:tint="#FF00A6FF"
   style="@style/ExoMediaButton.Rewind"/>

Then change all android:textColor attributes you find in your custom file to the same color #FF00A6FF.

<TextView android:id="@id/exo_position"
   [..]
   android:textColor="#FF00A6FF"
   [..]
<TextView android:id="@id/exo_duration"
   [..]
   android:textColor="#FF00A6FF"/>

Lovely!

Screenshot: Tinted buttons and text view as applied above.

Don't play around with this too long! We are almost done with our codelab!

We just customized the element in the layout file activity_player.xml. Another approach is to just override the default layout file the PlaybackControlView is using. If we look into the source code of PlaybackControlView, we find that the layout file R.layout.exo_playback_control_view is used. The trick is simple. We can override the default layout throughout our app by creating our own layout file exo_playback_control_view.xml in the res folder of our library module player-lib. All instances of PlayerControlView of our app will then use our default which can still again be overridden specifically for each SimpleExoPlayerView element with the app:controller_layout_id attribute..

First remove the controller_layout_id attribute you just added and delete the file custom_playback_control.xml. After removing the attribute you have the SimpleExoPlayerView element look like this again:

<com.google.android.exoplayer2.ui.SimpleExoPlayerView
   android:id="@+id/video_view"
   android:layout_width="match_parent"
   android:layout_height="match_parent"/>

If you want you can restart to verify if we really have the default look and feel again. But go ahead and create a file exo_playback_control_view.xml in the res folder of our library module:

Screenshot: Overriding the default layout file of the PlaybackControlView.

We only put the play and the pause button into this layout file and below the button row an ImageView with a logo:

<?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_2015_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 code lab. You learned a lot about integrating ExoPlayer properly in your app:

You can find the source code of ExoPlayer on Github. The ExoPlayer team blogs on Medium and you certainly want to have a look into the ExoPlayer Developer Guide.