Media streaming with ExoPlayer

4ec261ee5a0bd0cc.png

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

Download zip

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

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

f2e5feb9ade6c7f7.png

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.

  1. Deploy and run the app to check everything is fine. The app should fill the screen with a black background.

21c0dae6245fbd31.png

Screenshot: Blank app running

Add ExoPlayer dependency

ExoPlayer is an open source project hosted on GitHub. Each release is distributed through 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

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.

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

  1. Open the layout resource file activity_player.xml from the player-lib module.
  2. Place the cursor inside the FrameLayout element.
  3. Start typing <PlayerView and let Android Studio autocomplete the PlayerView element.
  4. Use match_parent for the width and height.
  5. 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.

  1. In the PlayerActivity, you now need to find the video view so you 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.

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

.

b0f7f70115bb90e5.png

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.

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

  1. With PlayerActivity open, click Code menu > Override methods....
  2. Select onStart, onResume, onPause, and onStop.
  3. 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();
 }
}

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.

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

  1. Release resources with releasePlayer (which you 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();
 }
}

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.

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

  1. 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. Because playWhenReady is initially true, playback starts automatically the first time the app is run.
  • seekTo tells the player to seek to a certain position within a specific window. Both currentWindow and playbackPosition 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.

1d049aead0483777.png

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.

  1. Start another app and put your app in the foreground again. Does it resume at the correct position?
  2. 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?
  3. 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.

  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() {
  [...]
     MediaItem mediaItem = MediaItem.fromUri(getString(R.string.media_url_mp3));
  [...]
}

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

4ebd0b0f98593691.png

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

  1. 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 1f79fee4d082870f.pngand 39627002c03ce320.png to navigate the sequence of media items.

7b5c034dafabe1bd.png

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.

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

  1. 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);
}
  1. 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

  1. Declare a private member of type PlaybackStateListener in the PlayerActivity.
  2. Create a TAG constant, which you use for logging later.

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

b59ce69a22595ba7.png

Screenshot: Quick Fix menu for creating a missing class

  1. Implement the Player.EventListener interface. This is used to inform you about important player events, including errors and playback state changes.
  2. 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

ExoPlayer.STATE_IDLE

The player has been instantiated, but has not yet been prepared.

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 the player's playWhenReady property is true. If it is false, the player is paused.

ExoPlayer.STATE_ENDED

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.

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

  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.

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.

bcfe17eebcad9e13.png

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.

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

  1. Remove app:use_controller="false".
  2. Change the player view to use show_timeout, fastforward_increment and rewind_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 the PlayerView the delay in milliseconds before the control is hidden after the user last interacted with it.
  • The fastforward_increment and rewind_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.

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

b18d1e7f7f48be59.png

Screenshot: Custom playback control layout file has been created.

  1. Copy the original layout file from here into custom_playback_control.xml.
  2. 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.

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

89e6535a22c8e321.png

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. 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 you have beautiful, colored UI components!

e9835d65d6dd0634.png

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_playback_control_view for layout. If you create our own layout file with the same filename, the PlayerControlView uses your file instead.

  1. Remove the controller_layout_id attribute you just added.
  2. Delete the file custom_playback_control.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"/>
  1. Create a file named exo_playback_control_view.xml in the res/layout folder of your 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 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.