In this guide, we will learn how to create a responsive VideoView using Jetpack Compose and ExoPlayer. Video playback has become a crucial aspect of many Android applications. With the growing use of Jetpack Compose, integrating video players has become more efficient and modernized. We will explore how to set up an ExoPlayer VideoView that adapts to screen sizes, includes loading animations, and ensures an enhanced user experience.

Setting Up ExoPlayer for Jetpack Compose VideoView

To get started with Jetpack Compose and ExoPlayer, you’ll first need to add the ExoPlayer dependency to your project. The ExoPlayer library is a powerful and flexible media player that can handle different formats and streaming protocols.

implementation("com.google.android.exoplayer:exoplayer-core:2.19.1")
implementation("com.google.android.exoplayer:exoplayer-ui:2.19.1")
implementation("androidx.media3:media3-exoplayer:1.4.1")

After syncing your project with the Gradle files, you’re ready to implement a responsive VideoView.

Responsive Video Playback Layout with Jetpack Compose

The key to a great video player is its responsiveness to different screen sizes and orientations. Using the AndroidView composable in Jetpack Compose, we can easily integrate the StyledPlayerView from ExoPlayer.

@Composable
fun EnhancedVideoView(
    exoPlayer: ExoPlayer,
    modifier: Modifier = Modifier,
    showLoading: Boolean = true,
    responsive: Boolean = false,
    onComplete: () -> Unit = {}
) {
    val isVisible = remember { mutableStateOf(true) }
    val videoHeight = remember { mutableStateOf(0) }
    val videoWidth = remember { mutableStateOf(0) }
    val context = LocalContext.current
    val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)

    // Calculate aspect ratio for responsive layout
    val aspectRatio = if (videoWidth.value != 0 && videoHeight.value != 0) {
        videoWidth.value.toFloat() / videoHeight.value
    } else 1f

    val videoModifier = if (responsive) {
        Modifier
            .fillMaxWidth()
            .aspectRatio(aspectRatio)
            .background(Color.Black)
    } else Modifier.fillMaxSize()

    Surface(modifier = modifier) {
        Box {
            AndroidView(
                modifier = videoModifier,
                factory = {
                    StyledPlayerView(context).apply {
                        resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
                        player = exoPlayer
                        setShowBuffering(StyledPlayerView.SHOW_BUFFERING_WHEN_PLAYING)
                        useController = false // Hide playback controls
                        exoPlayer.addListener(object : Player.Listener {
                            override fun onVideoSizeChanged(videoSize: VideoSize) {
                                videoWidth.value = videoSize.width
                                videoHeight.value = videoSize.height
                            }

                            override fun onPlaybackStateChanged(playbackState: Int) {
                                if (playbackState == Player.STATE_ENDED) {
                                    onComplete()
                                }
                            }
                        })
                    }
                }
            )

            // Show loading animation while buffering
            AnimatedVisibility(visible = isVisible.value) {
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .background(Color(0xAA000000)),
                    contentAlignment = Alignment.Center
                ) {
                    // Custom loading view or animation can be placed here
                    LoadingIndicator()
                }
            }

            DisposableEffect(lifecycleOwner.value) {
                val observer = LifecycleEventObserver { _, event ->
                    when (event) {
                        Lifecycle.Event.ON_RESUME -> {
                        exoPlayer.prepare()
                            exoPlayer.play().apply {
                                isVisible.value = false
                                onComplete()
                            }
                        }
                        Lifecycle.Event.ON_PAUSE -> exoPlayer.pause()
                        Lifecycle.Event.ON_DESTROY -> exoPlayer.release()
                        else -> {}
                    }
                }
                val lifecycle = lifecycleOwner.value.lifecycle
                lifecycle.addObserver(observer)

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

Using the above code, we create a responsive VideoView that adapts to the video’s aspect ratio and fills the screen as needed. Additionally, we show a loading indicator while the video buffers, which enhances user experience.

Custom Loading Indicator

While the video is buffering, we want to show the user that something is happening. We can create a simple circular loading indicator:

@Composable
fun LoadingIndicator() {
    // Replace with your custom loading animation or spinner
    androidx.compose.material3.CircularProgressIndicator(
        color = Color.White,
        modifier = Modifier.size(50.dp)
    )
}

Best Practices for ExoPlayer Video Playback

Lifecycle Awareness

To prevent memory leaks and manage video playback efficiently, you should ensure that ExoPlayer’s lifecycle is properly managed. By observing the lifecycle events, you can control when the video should play, pause, or release resources.

Responsive Design

The aspect ratio of the video is crucial for maintaining a proper display across devices. Using Jetpack Compose’s Modifier.aspectRatio() method ensures the video maintains its natural dimensions.

Creating ExoPlayer with Kotlin

To create an instance of ExoPlayer in Jetpack Compose, you can use the following function:

fun initializeExoPlayer(context: Context, videoUrl: String, isMuted: Boolean): ExoPlayer {
    return ExoPlayer.Builder(context).build().apply {
        setMediaItem(MediaItem.com.google.android.exoplayer2.MediaItem.fromUri(videoUrl))
        playWhenReady = true
        volume = if (isMuted) 0f else 1f
        repeatMode = Player.REPEAT_MODE_ALL
        prepare()
    }
}

With this setup, you can initialize your video player and start playback immediately.

Sample Jetpack Compose Exoplayer Usage Code

Let me show you how you can apply this code to your project with an example. I will show you the example video right after the code.

            ExampleAITheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background,
                ) {
                    Box(contentAlignment = Alignment.TopCenter) {
                        LazyColumn {
                            item {
                                EnhancedVideoView(
                                    exoPlayer = initializeExoPlayer(
                                                LocalContext.current, "https://assets.mixkit.co/videos/51945/51945-720.mp4",
                                                false),
                                   modifier = Modifier.fillMaxWidth(),
                                   responsive = true
                                )
                            }
                            item {
                                EnhancedVideoView(
                                    exoPlayer = initializeExoPlayer(LocalContext.current, "https://assets.mixkit.co/videos/1261/1261-720.mp4", false),
                                    modifier = Modifier.fillMaxWidth(),
                                    responsive = true
                                )
                            }
                        }
                    }
                }
            }

External Resources

For further information on Jetpack Compose and its video capabilities, check out the official documentation:

Interested in learning more about Jetpack Compose and Android development? Check out these related articles on my blog:

Full Code

Here is the complete code for the responsive VideoView with Jetpack Compose and ExoPlayer:

import android.content.Context
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem.fromUri
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.video.VideoSize

@Composable
fun EnhancedVideoView(
    exoPlayer: ExoPlayer,
    modifier: Modifier = Modifier,
    responsive: Boolean = false,
    onComplete: () -> Unit = {}
) {
    val isVisible = remember { mutableStateOf(true) }
    val videoHeight = remember { mutableStateOf(0) }
    val videoWidth = remember { mutableStateOf(0) }
    val context = LocalContext.current
    val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)

    // Calculate aspect ratio for responsive layout
    val aspectRatio = if (videoWidth.value != 0 && videoHeight.value != 0) {
        videoWidth.value.toFloat() / videoHeight.value
    } else 1f
    Log.e("aspectRatio", aspectRatio.toString())
    val videoModifier = if (responsive) {
        Modifier
            .fillMaxWidth()
            .aspectRatio(aspectRatio)
            .background(Color.White)
    } else Modifier.fillMaxSize()

    Surface(modifier = modifier) {
        Box {
            AndroidView(
                modifier = videoModifier,
                factory = {
                    StyledPlayerView(context).apply {
                        resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
                        player = exoPlayer
                        setShowBuffering(StyledPlayerView.SHOW_BUFFERING_WHEN_PLAYING)
                        useController = false // Hide playback controls
                        exoPlayer.addListener(object : Player.Listener {
                            override fun onVideoSizeChanged(videoSize: VideoSize) {
                                videoWidth.value = videoSize.width
                                videoHeight.value = videoSize.height
                            }

                            override fun onPlaybackStateChanged(playbackState: Int) {
                                if (playbackState == Player.STATE_ENDED) {
                                    onComplete()
                                }
                            }
                        })
                    }
                }
            )

            DisposableEffect(lifecycleOwner.value) {
                val observer = LifecycleEventObserver { _, event ->
                    when (event) {
                        Lifecycle.Event.ON_RESUME -> {
                            exoPlayer.prepare()
                            exoPlayer.play().apply {
                                isVisible.value = false
                                onComplete()
                            }
                        }
                        Lifecycle.Event.ON_PAUSE -> exoPlayer.pause()
                        Lifecycle.Event.ON_DESTROY -> exoPlayer.release()
                        else -> {}
                    }
                }
                val lifecycle = lifecycleOwner.value.lifecycle
                lifecycle.addObserver(observer)

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

fun initializeExoPlayer(context: Context, videoUrl: String, isMuted: Boolean): ExoPlayer {
    return ExoPlayer.Builder(context).build().apply {
        setMediaItem(fromUri(videoUrl))
        playWhenReady = true
        repeatMode = Player.REPEAT_MODE_ALL
        volume = if (isMuted) 0f else 1f
        prepare()
    }
}

This example demonstrates how you can leverage Jetpack Compose and ExoPlayer for a modern and responsive video playback experience in your Android app.

Did you like this article?
You can subscribe to my newsletter below and get updates about my new articles.

Shares:
Leave a Reply

Your email address will not be published. Required fields are marked *