19. Dec 2022Android

Jetpack Compose Basics - How to use Exoplayer library to play videos

One of the very common requests across various Android applications is video playback. The Exoplayer library is one of the most popular libraries designed to accomplish this task. In this article, we will look at how to use it and implement it in Jetpack Compose.

Paulina SlavikováAndroid developer

Why Exoplayer?

Maybe some of you are shaking your head at the fact that we need to use some external library for such a common task. Well, Android does provide us with the MediaPlayer class, but its options are not sufficient in most cases.

Exoplayer is an open-source library from Google, which, unlike MediaPlayer, is more stable, much more customizable and easier to use.

Jetpack Compose & Exoplayer

The first step we need to do is to add a new library to an existing project. Currently, the latest version of the library is 2.18.1. You can check the latest release version here https://github.com/google/ExoPlayer/releases.

implementation 'com.google.android.exoplayer:exoplayer:2.18.1'

The next step is to create a @Composable function, which will be stretched over the entire screen area and will represent the space for placing the player. In this composable, we will create an Exoplayer object using the ExoPlayer.Builder call, to which we need to send the context of the given composable. Subsequently, we will additionally edit it:

  • The most important step is to call the setMediaItem function, which uses the fromUri function to create a MediaItem object from the videoURL string value that the player can play. Calling this function deletes any previously set playlist and resets the player's position to its original state. We must not forget to add this permission to the manifest file <uses-permission android:name="android.permission.INTERNET"/>
  • By setting playWhenReady to true, the player will start the video automatically.
  • By calling the prepare function, the player will start loading the media and getting the resources needed for playback.
@Composable
fun ExoPlayerComp() {
    Surface(
        modifier = Modifier.fillMaxSize(),
        color = Color.Black
    ) {
        val videoURL = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"
        val context = LocalContext.current

        val exoPlayer = ExoPlayer.Builder(context)
            .build()
            .apply {
                setMediaItem(fromUri(videoURL))
                playWhenReady = true
                prepare()
           }
    }
}

When we have the exoplayer object set up correctly, we need to create some @Composable that will contain the UI of the playing video and its controls. Currently, the Exoplayer library is not yet adapted for Compose, but we can adapt it ourselves 🙂 using @Composable AndroidView, in which we can insert the classic view that we would use in the case of using xml, in our case StyledPlayerView.

AndroidView(
    modifier = Modifier.fillMaxSize(),
    factory = {
        StyledPlayerView(context).apply {
           resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
           player = exoPlayer
    }
})

Using the apply function, we can set the same parameters for this "composed view" as we could set for the view in the xml file. According to the resizeMode parameter, we can set the span of the video. In this case, the size of the video is adjusted to the size of the screen, keeping the original aspect ratio. We insert the exoplayer object we created in the previous step into the player parameter. Now we are all done and ready to start the video!

… but …

… if you go back to the previous screen, you can see that the video is still playing in the background, even though the screen is no longer part of the composition. This is because the player was not properly released and did not release the resources it was using. We can solve this problem using the DisposableEffect effect provided by the Compose API. For our use, all we need to know about this effect is that the body of the effect is executed whenever its key1 parameter changes, and that the callback method onDispose{} is executed whenever the composable leaves the composition. And this feature is key for us. It is in this part that we can call exoPlayer.release().

DisposableEffect(
    key1 = AndroidView(
        modifier = Modifier.fillMaxSize(),
        factory = {
            StyledPlayerView(context).apply {
                resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
                player = exoPlayer
            }
         }),
    effect = {
        onDispose {
            exoPlayer.release()
         }
     }
)

Now when we go back to the previous screen the video stops playing, however if we just put the app in the background the video will continue to play because it is still part of the composition and the onDispose{} callback hasn't been called. To solve this problem, we need to get an instance of the LocalLifecycleOwner class, which we need to listen for activity lifecycle changes.

val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)

Subsequently, we will create a LifecycleEventObserver implementation in DisposableEffect, which will either stop or start the player, depending on whether the application is in the foreground or in the background. We must bind this observer to the life cycle of the activity and we must also remove it when leaving the composition, again in the onDispose{} callback.

The whole code after editing is here 🙂

@Composable
fun ExoPlayerComp() {
    Surface(
        modifier = Modifier.fillMaxSize(),
        color = Color.Black
    ) {
        val videoURL       = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"
        val context        = LocalContext.current
        val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)

        val exoPlayer = ExoPlayer.Builder(context)
            .build()
            .apply {
                setMediaItem(fromUri(videoURL))
                playWhenReady    = true
                prepare()
            }

        DisposableEffect(
            key1 = AndroidView(
                modifier = Modifier.fillMaxSize(),
                factory = {
                    StyledPlayerView(context).apply {
                        resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
                        player = exoPlayer
                    }
                }),
            effect = {
                val observer = LifecycleEventObserver { _, event ->
                    when (event) {
                        Lifecycle.Event.ON_RESUME -> {
                            Log.e("LIFECYCLE", "resumed")
                            exoPlayer.play()
                        }
                        Lifecycle.Event.ON_PAUSE  -> {
                            Log.e("LIFECYCLE", "paused")
                            exoPlayer.stop()
                        }
                    }
                }

                val lifecycle = lifecycleOwner.value.lifecycle
                lifecycle.addObserver(observer)

                onDispose {
                    exoPlayer.release()
                    lifecycle.removeObserver(observer)
                }
            }
        )
    }
}

That is all. The implementation is not completely trivial, but I hope it will help you in your project 🙂

Sources:

Paulina SlavikováAndroid developer

Join us

Jozef Knažko – Head of Android

"GoodRequest is made up of a team of people who are not looking for excuses, but solutions. We have common goals and we don't just look at ourselves, but what we can achieve together."