From a2aaa171d7f7aefaf474a538aef29cdc04478a57 Mon Sep 17 00:00:00 2001 From: Tony Yang Date: Thu, 5 Dec 2024 16:21:37 +0800 Subject: [PATCH 1/2] refactor: restructure ExoPlayer demo with multiple video support --- .../com/mun/bonecci/exoplayer/MainActivity.kt | 78 +--------------- .../bonecci/exoplayer/data/VideoMockData.kt | 19 ++++ .../bonecci/exoplayer/ui/VideoPlayerScreen.kt | 89 +++++++++++++++++++ .../exoplayer/ui/component/ExoPlayerView.kt | 73 +++++++++++++++ 4 files changed, 184 insertions(+), 75 deletions(-) create mode 100644 app/src/main/java/com/mun/bonecci/exoplayer/data/VideoMockData.kt create mode 100644 app/src/main/java/com/mun/bonecci/exoplayer/ui/VideoPlayerScreen.kt create mode 100644 app/src/main/java/com/mun/bonecci/exoplayer/ui/component/ExoPlayerView.kt diff --git a/app/src/main/java/com/mun/bonecci/exoplayer/MainActivity.kt b/app/src/main/java/com/mun/bonecci/exoplayer/MainActivity.kt index e04104b..9e61388 100644 --- a/app/src/main/java/com/mun/bonecci/exoplayer/MainActivity.kt +++ b/app/src/main/java/com/mun/bonecci/exoplayer/MainActivity.kt @@ -3,25 +3,12 @@ package com.mun.bonecci.exoplayer import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.annotation.OptIn import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.media3.common.MediaItem -import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.ui.PlayerView +import com.mun.bonecci.exoplayer.data.VideoMockData +import com.mun.bonecci.exoplayer.ui.VideoPlayerScreen import com.mun.bonecci.exoplayer.ui.theme.ExoPlayerTheme class MainActivity : ComponentActivity() { @@ -34,68 +21,9 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - ExoPlayerView() + VideoPlayerScreen(VideoMockData.sources) } } } } -} - -/** - * Composable function that displays an ExoPlayer to play a video using Jetpack Compose. - * - * @OptIn annotation to UnstableApi is used to indicate that the API is still experimental and may - * undergo changes in the future. - * - * @see EXAMPLE_VIDEO_URI Replace with the actual URI of the video to be played. - */ -@OptIn(UnstableApi::class) -@Composable -fun ExoPlayerView() { - // Get the current context - val context = LocalContext.current - - // Initialize ExoPlayer - val exoPlayer = ExoPlayer.Builder(context).build() - - // Create a MediaSource - val mediaSource = remember(EXAMPLE_VIDEO_URI) { - MediaItem.fromUri(EXAMPLE_VIDEO_URI) - } - - // Set MediaSource to ExoPlayer - LaunchedEffect(mediaSource) { - exoPlayer.setMediaItem(mediaSource) - exoPlayer.prepare() - } - - // Manage lifecycle events - DisposableEffect(Unit) { - onDispose { - exoPlayer.release() - } - } - - // Use AndroidView to embed an Android View (PlayerView) into Compose - AndroidView( - factory = { ctx -> - PlayerView(ctx).apply { - player = exoPlayer - } - }, - modifier = Modifier - .fillMaxWidth() - .height(200.dp) // Set your desired height - ) -} - - -const val EXAMPLE_VIDEO_URI = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - ExoPlayerTheme { - ExoPlayerView() - } } \ No newline at end of file diff --git a/app/src/main/java/com/mun/bonecci/exoplayer/data/VideoMockData.kt b/app/src/main/java/com/mun/bonecci/exoplayer/data/VideoMockData.kt new file mode 100644 index 0000000..1b5f403 --- /dev/null +++ b/app/src/main/java/com/mun/bonecci/exoplayer/data/VideoMockData.kt @@ -0,0 +1,19 @@ +package com.mun.bonecci.exoplayer.data + +object VideoMockData { + val sources = listOf( + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4", + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4", + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4", + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4", + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4", + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4", + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4", + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/VolkswagenGTIReview.mp4", + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WeAreGoingOnBullrun.mp4", + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WhatCarCanYouGetForAGrand.mp4" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/mun/bonecci/exoplayer/ui/VideoPlayerScreen.kt b/app/src/main/java/com/mun/bonecci/exoplayer/ui/VideoPlayerScreen.kt new file mode 100644 index 0000000..b909259 --- /dev/null +++ b/app/src/main/java/com/mun/bonecci/exoplayer/ui/VideoPlayerScreen.kt @@ -0,0 +1,89 @@ +package com.mun.bonecci.exoplayer.ui + +import androidx.annotation.OptIn +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.media3.common.util.UnstableApi +import com.mun.bonecci.exoplayer.data.VideoMockData +import com.mun.bonecci.exoplayer.ui.component.ExoPlayerView + +@OptIn(UnstableApi::class) +@Composable +fun VideoPlayerScreen(videoUrls: List) { + var currentVideoIndex by remember { mutableIntStateOf(0) } + + Column( + modifier = Modifier.fillMaxSize() + ) { + ExoPlayerView( + videoUrl = videoUrls[currentVideoIndex], + modifier = Modifier + .fillMaxWidth() + .weight(1f), + onReadyCallback = { + it.playWhenReady = true + }, + onEndedCallback = { + currentVideoIndex++ + } + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = { + if (currentVideoIndex > 0) { + currentVideoIndex -= 1 + } + }, + enabled = currentVideoIndex > 0 + ) { + Text("Previous") + } + + Text( + text = "Now Playing: ${currentVideoIndex + 1}/${videoUrls.size}", + modifier = Modifier.padding(horizontal = 8.dp), + style = MaterialTheme.typography.bodyMedium + ) + + Button( + onClick = { + if (currentVideoIndex < videoUrls.size - 1) { + currentVideoIndex += 1 + } + }, + enabled = currentVideoIndex < videoUrls.size - 1 + ) { + Text("Next") + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewVideoPlayerScreen() { + VideoPlayerScreen(VideoMockData.sources) +} \ No newline at end of file diff --git a/app/src/main/java/com/mun/bonecci/exoplayer/ui/component/ExoPlayerView.kt b/app/src/main/java/com/mun/bonecci/exoplayer/ui/component/ExoPlayerView.kt new file mode 100644 index 0000000..42f32b6 --- /dev/null +++ b/app/src/main/java/com/mun/bonecci/exoplayer/ui/component/ExoPlayerView.kt @@ -0,0 +1,73 @@ +package com.mun.bonecci.exoplayer.ui.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.PlayerView + +@Composable +fun ExoPlayerView( + videoUrl: String, + modifier: Modifier = Modifier, + onIdleCallback: (ExoPlayer) -> Unit = {}, + onBufferingCallback: (ExoPlayer) -> Unit = {}, + onReadyCallback: (ExoPlayer) -> Unit = {}, + onEndedCallback: (ExoPlayer) -> Unit = {} +) { + val context = LocalContext.current + + val exoPlayer = remember { + ExoPlayer.Builder(context).build() + } + + val mediaSource = remember(videoUrl) { + MediaItem.fromUri(videoUrl) + } + + LaunchedEffect(mediaSource) { + exoPlayer.setMediaItem(mediaSource) + exoPlayer.prepare() + } + + DisposableEffect(Unit) { + onDispose { + exoPlayer.release() + } + } + + DisposableEffect(Unit) { + val listener = object : Player.Listener { + override fun onPlaybackStateChanged(playbackState: Int) { + when (playbackState) { + Player.STATE_IDLE -> onIdleCallback(exoPlayer) + Player.STATE_BUFFERING -> onBufferingCallback(exoPlayer) + Player.STATE_READY -> onReadyCallback(exoPlayer) + Player.STATE_ENDED -> onEndedCallback(exoPlayer) + } + } + } + + exoPlayer.addListener(listener) + + onDispose { + exoPlayer.removeListener(listener) + exoPlayer.release() + } + } + + AndroidView( + factory = { ctx -> + PlayerView(ctx).apply { + player = exoPlayer + } + }, + modifier = modifier + ) +} \ No newline at end of file From d075d8b84da1393f9263350166e80ab46dac8a8a Mon Sep 17 00:00:00 2001 From: Tony Yang Date: Fri, 6 Dec 2024 18:06:16 +0800 Subject: [PATCH 2/2] feat: auto-pause video playback when app enters background --- .../exoplayer/ui/component/ExoPlayerView.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/src/main/java/com/mun/bonecci/exoplayer/ui/component/ExoPlayerView.kt b/app/src/main/java/com/mun/bonecci/exoplayer/ui/component/ExoPlayerView.kt index 42f32b6..7dd460f 100644 --- a/app/src/main/java/com/mun/bonecci/exoplayer/ui/component/ExoPlayerView.kt +++ b/app/src/main/java/com/mun/bonecci/exoplayer/ui/component/ExoPlayerView.kt @@ -6,7 +6,10 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer @@ -22,6 +25,7 @@ fun ExoPlayerView( onEndedCallback: (ExoPlayer) -> Unit = {} ) { val context = LocalContext.current + val lifecycle = LocalLifecycleOwner.current.lifecycle val exoPlayer = remember { ExoPlayer.Builder(context).build() @@ -36,6 +40,20 @@ fun ExoPlayerView( exoPlayer.prepare() } + DisposableEffect(lifecycle) { + val lifecycleObserver = object : DefaultLifecycleObserver { + override fun onPause(owner: LifecycleOwner) { + exoPlayer.pause() + } + } + + lifecycle.addObserver(lifecycleObserver) + + onDispose { + lifecycle.removeObserver(lifecycleObserver) + } + } + DisposableEffect(Unit) { onDispose { exoPlayer.release()