Cover image for ExoPlayer in Android Jetpack Compose

ExoPlayer in Android Jetpack Compose

Balaji Ramasamy's profile pic
Balaji Ramasamy Full Stack Developer

Oct 28, 2021

This is just a small introduction that I wanted to share with someone. You can skip if you don’t wanna read this. In my college days, I tried to learn android application development. But at that time I was still learning Java and jumped straight into it. So everything seemed overwhelming to me and I gave up on it. Then I got into REST API development and front application development with React and Angular. So I kind of forgot about MAD. Then I came back to it when I found flutter. Flutter was awesome. It was easy it took away everything those I found overwhelming about android app development in college. So I thought to myself I might never go back to native code. But just recently I came across the Jetpack Compose and I have to say it got me hooked up into native android application. Lately I have been trying out different functionalities and I wanted to share some of it.

What is ExoPlayer?

ExoPlayer is an application level media player for Android. It provides an alternative to Android’s MediaPlayer API for playing audio and video both locally and over the Internet. ExoPlayer supports features not currently supported by Android’s MediaPlayer API, including DASH and SmoothStreaming adaptive playbacks. Unlike the MediaPlayer API, ExoPlayer is easy to customize and extend, and can be updated through Play Store application updates.

Visit official documentation of ExoPlayer for more info.

  1. Add the dependency of ExoPlayer in app/build.gradle
ext {
    verion = '2.15.0'
}dependencies {
    implementation "com.google.android.exoplayer:exoplayer:$version"
}

2. Key parts we require for a video to play using ExoPlayers are,

1. DataSourceFactory
2. MediaItem
3. MediaSource
4. SimpleExoPlayer
5. PlayerView

3. In order to create a DataSourceFactory and SimpleExoPlayer, we need context of the current UI tree. We can get the Context inside the compose functions using the LocalContext value.

We can create a compose function with name ExoVideoPlayer and it will take a file as an arugment. This file corresponds to the video that we wanna play.

@Composable
fun ExoVideoPlayer(file: File) {
    val context = LocalContext.current
}

4. We can create SimpleExoPlayer using SimpleExoPlayer.Builder method with the context as argument. We will call the Builder function inside the remember function to cache the SimpleExoPlayer object. It will prevent new SimpleExoPlayer object getting whenever the compose function commits.

@Composable
fun ExoVideoPlayer(file: File) {
    val context = LocalContext.current
    val exoPlayer = remember {
        SimpleExoPlayer.Builder(context).build()
    }
}

5. We need to create and add a MediaSource to the exo player object. We can do this using the apply extension function that kotlin provides with all the types.

We will create a DataSourcesFactory with the context and package name. Then will create MediaSource using the MediaItem created with the file.

We can add multiple MediaSource to the ExoPlayer.

Lastly we will call prepare method for initializing the player with the media source added.

@Composable
fun ExoVideoPlayer(file: File) {
    val context = LocalContext.current
    val exoPlayer = remember {getSimpleExoPlayer(context, file)}
}private fun getSimpleExoPlayer(context: Context, file: File): SimpleExoPlayer {
    return SimpleExoPlayer.Builder(context).build().apply {
        val dataSourceFactory = DefaultDataSourceFactory(
            context,
            Util.getUserAgent(context, context.packageName)
        )
        //local video
        val localVideoItem = MediaItem.fromUri(file.toUri())
        val localVideoSource = ProgressiveMediaSource
            .Factory(dataSourceFactory)
            .createMediaSource(localVideoItem)
        this.addMediaSource(localVideoSource)

        // streaming from internet
        val internetVideoItem = MediaItem.fromUri(VIDEO_URL)
        val internetVideoSource = ProgressiveMediaSource
            .Factory(dataSourceFactory)
            .createMediaSource(internetVideoItem)
        this.addMediaSource(internetVideoSource)
        // init
        this.prepare()
    }
}

There are different implementations of MediaSource implementation for handling different formats. ProgressiveMediaSource is for handling commonly used media types such as mp3, mp4, fav and more.

6. Now we will have to pass this exoPlayer on to PlayerView. PlayerView is built with AndroidView FrameLayout. So we cannot directly use it like a compose function. But Jetpack Compose provides interoperability APIs to use android views directly in compose functions. We can use AndroidView to include any layout into compose UI hierarchy. This approach is particularly useful if you want to use UI elements that are not yet available in Compose, like AdView and MapView. Visit Jetpack Docs for more examples and information on interoperability.

@Composable
fun <T : View> AndroidView(
    factory: (Context) -> T,
    modifier: Modifier = Modifier,
    update: (T) -> Unit = NoOpUpdate
) {} 

AndroidView can take any layout which extends from the View class, a factory method to build our layout.

@Composable
fun ExoVideoPlayer(file: File) {
    val context = LocalContext.current
    val exoPlayer = remember {getSimpleExoPlayer(context, file)}
    AndroidView(
        modifier = Modifier
            .fillMaxSize()
            .padding(bottom = 20.dp),
        factory = context1 ->
            PlayerView(context1).apply {
                player = exoPlayer
            }
        },
    )
}

Whole Code:

import android.content.Context
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.net.toUri
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.ui.PlayerView
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.Util
import java.io.File

const val TAG = "StatusSaverApp"

const val VIDEO_URL =
    "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"

@Composable
fun ExoVideoPlayer(file: File) {
    val context = LocalContext.current
    val exoPlayer = remember { getSimpleExoPlayer(context, file) }
    AndroidView(
        modifier = Modifier
            .fillMaxSize()
            .padding(bottom = 20.dp),
        factory = { context1 ->
            PlayerView(context1).apply {
                player = exoPlayer
            }
        },
    )
}

private fun getSimpleExoPlayer(context: Context, file: File): SimpleExoPlayer {
    return SimpleExoPlayer.Builder(context).build().apply {
        val dataSourceFactory = DefaultDataSourceFactory(
            context,
            Util.getUserAgent(context, context.packageName)
        )
        //local video
        val localVideoItem = MediaItem.fromUri(file.toUri())
        val localVideoSource = ProgressiveMediaSource
            .Factory(dataSourceFactory)
            .createMediaSource(localVideoItem)
        this.addMediaSource(localVideoSource)

        // streaming from internet
        val internetVideoItem = MediaItem.fromUri(VIDEO_URL)
        val internetVideoSource = ProgressiveMediaSource
            .Factory(dataSourceFactory)
            .createMediaSource(internetVideoItem)
        this.addMediaSource(internetVideoSource)
        // init
        this.prepare()
    }
}

Thank you for reading👍.

1_L49fGnBU1Wi8hRjb7H4oPA.png