Playing a video with ExoPlayer

This recipe shows how to create a video player using ExoPlayer. ExoPlayer is the de-facto video player for Android, and is capable of playing pretty much anything. You can use it to play on or offline files, or streaming formats.

1. Include ExoPlayer

In the root build.gradle:

repositories {
    google()
    jcenter()
}

Then in the module build.gradle:

compileOptions {
  targetCompatibility JavaVersion.VERSION_1_8
}

dependencies {

    ...

    implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
    implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'

    // And for each specific format you want to use:
    implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X'
    implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X'
    implementation 'com.google.android.exoplayer:exoplayer-hls:2.X.X'
    implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.X.X'
}

2. Creating a video player

Creating a video player using a media source is easy if you have a MediaSource:

fun createVideoPlayer(context: Context, uri: String) {
    val player = SimpleExoPlayer.Builder(requireContext())
            .setUseLazyPreparation(true)
            .build()
    
    // Magic... read on below
    val mediaSource = ....?
    
    player.prepare(mediaSource)
    return player
}

How a MediaSource should be created depends on your usecase, and is described a few sections below.

3. Using the video player

In you layout, use something like the snippet below. There are smaller versions (e.g. without the AspectRatioFrameLayout) but this one covers a common usecase where the video must appear in a specifically sized view, similar to the centerCrop scale type for ImageViews.

<com.google.android.exoplayer2.ui.SimpleExoPlayerView
    android:id="@+id/player_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:hide_on_touch="false"
    app:show_timeout="0"
    app:use_artwork="false"
    app:use_controller="false">

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

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

Alternative to a SurfaceView you may also use a TextureView if you need more control over the view.

Next, create a video player using the createVideoPlayer method above, and attach it to the view:

val player = createVideoPlayer(context, uri)

// Configure it, for example:
player_view.useController = false
player_view.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
player_view.player = player
player.setVideoSurfaceView(surface_view)

And start it:

player.playWhenReady = true

You may also use a listener to detect the state of the video, like so:

player.addListener(object : Player.EventListener {
    override fun onLoadingChanged(isLoading: Boolean) {
        // Note: this method may be called many times, every time the buffer
        // is loading
    }

    override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
        if (playbackState == Player.STATE_ENDED) {
            // the video has ended
        } else if (playbackState == Player.STATE_READY) {
            thumbnail_placeholder.visibility = View.GONE
        }
    }
})

4a. Creating a media source from difference sources

The snippets below create a MediaSource from different video file sources. These can be used for either online video files or local ones.

From a URL

private fun getOnlineMediaSource(context: Context, uri: String): ProgressiveMediaSource {
    val factory = DefaultDataSourceFactory(
        context,
        Util.getUserAgent(context, context.getString(R.string.user_agent))
    )
    return ProgressiveMediaSource
        .Factory(factory)
        .createMediaSource(Uri.parse(uri))
}

From the /res/raw folder

private fun createProgressiveMediaSource(
        dataSource: DataSource,
        dataSpec: DataSpec
): ProgressiveMediaSource {
    dataSource.open(dataSpec)
    val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)
    val dataFactory = DataSource.Factory { dataSource }
    return ProgressiveMediaSource
        .Factory(dataFactory)
        .setExtractorsFactory(extractorsFactory)
        .createMediaSource(dataSource.uri)
}
fun createRawMediaSource(
        context: Context,
        @RawRes rawRes: Int
) : ProgressiveMediaSource {
    val dataSpec = DataSpec(RawResourceDataSource.buildRawResourceUri(R.raw.sample_video))
    val dataSource = RawResourceDataSource(context)
    return createProgressiveMediaSource(dataSource, dataSpec)
}

From a local file

This method requries the createProgressiveMediaSource method from the snippets above.

fun createFileMediaSource(
        context: Context,
        @RawRes rawRes: Int
) : ProgressiveMediaSource {
    val dataSpec = DataSpec(localUri)
    val dataSource = FileDataSourceFactory().createDataSource()
    return createProgressiveMediaSource(dataSource, dataSpec)
}

4b. Creating a media source for streaming video

When loading a streaming video instead of a file, you’ll need to create a media source fit for the stream type.

HLS

HLS is a common streaming format. Depending on its configuration it has a 3 to 30 second buffer, which makes it suitable for live streaming videos but less suitable for video calling.

An HLS stream URL typically sends with .m3u8.

private fun getHlsMediaSource(
        context: Context,
        uri: String
) : MediaSource {
    val dataSourceFactory = DefaultHttpDataSourceFactory(
        Util.getUserAgent(
            context,
            context.getString(R.string.user_agent)
        )
    )
    return HlsMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(uri))
}

RTMP

An RTMP stream typically starts with rtmp://.

private fun getRtmpMediaSource(uri: String): MediaSource {
    val rtmpDataSourceFactory = RtmpDataSourceFactory()
    return ProgressiveMediaSource
        .Factory(rtmpDataSourceFactory)
        .createMediaSource(Uri.parse(uri))
}