Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 3 additions & 75 deletions app/src/main/java/com/mun/bonecci/exoplayer/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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()
}
}
19 changes: 19 additions & 0 deletions app/src/main/java/com/mun/bonecci/exoplayer/data/VideoMockData.kt
Original file line number Diff line number Diff line change
@@ -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"
)
}
Original file line number Diff line number Diff line change
@@ -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<String>) {
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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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.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
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 lifecycle = LocalLifecycleOwner.current.lifecycle

val exoPlayer = remember {
ExoPlayer.Builder(context).build()
}

val mediaSource = remember(videoUrl) {
MediaItem.fromUri(videoUrl)
}

LaunchedEffect(mediaSource) {
exoPlayer.setMediaItem(mediaSource)
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()
}
}

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
)
}