Simple and Sane Video Playback in Android

This month's blog post is a little late so November will hopefully end up with two posts :).

Let's look at how quickly and easily we can build a minimum viable video playing app in Android using the ExoPlayer library.

ExoPlayer is an open source media player that was built on top of the low level media codec APIs released in Android 4.1. It was built by the YouTube team at Google and is an excellent choice if your app needs a feature rich video player.
We'll cover a simple use case for streaming videos in your app and correctly handle the complexities of the Android activity life cycle.

To begin we'll need to add ExoPlayer as a dependency to our build.gradle file.

    implementation 'com.google.android.exoplayer:exoplayer-core:r2.5.4'
    implementation 'com.google.android.exoplayer:exoplayer-ui:r2.5.4'
    implementation "com.google.android.exoplayer:extension-okhttp:r2.5.4"

We'll make use of the core library as well as the UI extension library to give us easy to use views that plug right into our ExoPlayer instance.
I'm also a big fan of OkHttp and if you are too, then you can use the OkHttp extension to provide ExoPlayer with your app's OkHttp client instance.

You can find the latest release at the ExoPlayer GitHub page.

Let's start our implementation by taking advantage of the SimpleExoPlayerView widget that we get from the ui extension library. We'll go ahead and make a new activity named VideoActivity and place a SimpleExoPlayerView in the layout of our activity

Next we'll work on constructing an instance of SimpleExoPlayer.

In order to create an instance of SimpleExoPlayer we'll need to first create a TrackSelector. The TrackSelector will be responsible for selecting tracks from a media source that will then be consumed by one of ExoPlayer's renderers.

The library provides a DefaultTrackSelector and so we'll use it since it will work for our simple video player.

The DefaultTrackSelector will take an AdaptiveTrackSelectionFactory as a dependency. This factory is in charge of producing "track selections" for ExoPlayer. The AdaptiveTrackSelectionFactory that we will use is capable of producing TrackSelections that can be updated based on the network conditions and the state of the buffer.

Finally since we are using the AdaptiveTrackSelectionFactory we will need a BandwidthMeter which is simply a component that can estimate the available bandwidth. Here again we will use DefaultBandwidthMeter since it should do the job for us just fine.

SimpleExoPlayer will also expect a context as a constructor argument. We are scoping our player to our VideoService so we will use it as our context. This is just good android practice since you should generally prefer to use the narrowest scoped Context possible to get the job done.

Once we have an instance of SimpleExoPlayer we can look at how we can tell it to play a video.

We will need to provide a MediaSource to SimpleExoPlayer via the prepare() method. If our player called setPlayWhenReady() passing true then once the MediaSource has been prepared playback will begin. How do we get a MediaSource? With a Factory of course :)

Specifically, we will want a DefaultDataSourceFactory. We'd also really like it if our DefaultDataSourceFactory used the same http client as the rest of our app.

This is especially helpful when you need to communicate with an API that expects you to pass along auth headers when making requests. If you are already using something like OkHttp then ExoPlayer makes it easy to plug in your exist http client instance.

We'll create an OkHttpDataSourceFactory and then pass that in when we create our DefaultDataSourceFactory. Whenever we create a MediaSource using our factory our http client will be used to make the streaming request.

Where does all of this code live? Well in our sample project we are using dagger2 for dependency injection so we create all of these objects in a module that we scope to our VideoService.

When we want to use our ExoPlayer instance at runtime we will wrap it in a controller class that is service scoped along with our DefaultDataSourceFactory. That way the rest of our app can simply provide the controller with a URI and all of the magic that turns that URI into an ExoPlayer MediaSource is wrapped up and hidden.

So now that we have ExoPlayer ready to stream our videos, how do we get them to the screen so that the user can see them?

We'll just need to make a connection between our SimpleExoPlayerView in our VideoActivity and our service scoped instance of SimpleExoPlayer.

Once our VideoActivity has connected to our VideoService, it can get our SimpleExoPlayer instance and set that to our SimpleExoPlayerView. The ExoPlayer library takes over after that and handles playback and playback controls for us!

It can be a little bit of a pain to work with a bound service in Android but the advantage of doing it this way is that our video playback is not tied to a specific android activity instance and therefore our user's video playback will not be interrupted if they rotate the device to view in landscape.

At this point we have a very simple video playing app. There are many other things we would want to add before we released this app to users.

In this blog post I just wanted to show how easy it is to get up and running with ExoPlayer so I will mention some of the other things you should consider adding only briefly and provide you with some resources to find out more.

The most important thing any media playing app will need to do on Android is set up a MediaSession. This is how your app can interact with playback controls from android wear or Bluetooth devices. It also allows you to publish information about what you are playing so that those devices can display that information to the user. It is basically the gateway between the media you are playing and the rest of the Android ecosystem.

Another important part of being a responsible video playing citizen on Android is to handle audio ducking. You'll want to register with the system so that you can reduce volume or even pause playback when the user's device receives a notification or interacts with Google Assistant.

In that same vein you will also want to register for the "becoming noisy" broadcast so that your app can know when some headphones or Bluetooth speakers have disconnected and you can react accordingly and not end up blasting your audio and perhaps embarrassing your user.

You'll probably also want to provide some kind of notification that allows the user to control playback without having to open your app's VideoActivity. Android provides a very easy to use media notification api that also integrates nicely with the MediaSession you should have already set up.

If you'd like to dive even deeper into android media playback I highly recommend the following videos:

Ian Lake has done a far better job than I could ever do in explaining all of the intricacies of media playback on Android.

I would also suggest reading through and becoming familiar with the Universal Music Player example provided by Google.

It is a great example of everything you want your media playing app to do and it even makes use of ExoPlayer so now that you've read this post you'll be that much further ahead when you start browsing the source code.

In addition to the above, here are some more links that I have found helpful in my journey through Android media playback.

https://google.github.io/ExoPlayer/