I am building a video player library with jetpack compose and jetpack media3 exoplayer. I have to make it work both on phones and TV. Is there any official sample of media3 exoplayer and leanback integrated together to refer? I gone through many docs, i didnt find any useful resources for TV. I had added a simple video player. But the issue is that the control buttons loosing focus sometimes. When the remotes buttons are pressed nothing happens some times.I cant use custom controls. I have to use the defalult control from media3 only.This is my implementation.
@OptIn(UnstableApi::class)
@Composable
fun VideoPlayer(
videoUrl: String,
modifier: Modifier = Modifier,
title: String = "Bugs Bunny",
description: String = "",
artistName: String = "",
artworkUrl: String = "",
playWhenReady: Boolean = true,
seekBackSeconds: Int = 5,
seekForwardSeconds: Int = 10,
isInListItem: Boolean = false,
resizeMode: Int = VideoResizeKeys.RESIZE_MODE_FIT,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onFullScreenToggle: (Boolean) -> Unit = {},
onIsPlayingChanged: (Boolean) -> Unit = {},
onPlayerError: (Int) -> Unit = {},
onKeyEvent: (NativeKeyEvent) -> Unit = {}
) {
val context = LocalContext.current
val focusRequester = remember { FocusRequester() }
val mediaMetadata = remember {
MediaMetadata.Builder()
.setArtworkUri(Uri.parse(artworkUrl))
.setTitle(title)
.setDisplayTitle(title)
.setDescription(description)
.setAlbumArtist(artistName)
.build()
}
val mediaItem = remember {
mutableStateOf(
MediaItem.Builder()
.setUri(videoUrl)
.setMediaMetadata(mediaMetadata)
.build()
)
}
val exoPlayerListener = remember {
object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
onIsPlayingChanged(isPlaying)
Log.d(LogTag, "onIsPlayingChanged: $isPlaying")
}
override fun onPlayerError(error: PlaybackException) {
onPlayerError(error.errorCode)
val cause = error.cause
Log.e(LogTag, "PlaybackException Occurred: ${error.message} caused by $cause")
}
override fun onMetadata(metadata: Metadata) {
super.onMetadata(metadata)
Log.d(LogTag, "onMetadata: ${metadata}")
}
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
super.onMediaMetadataChanged(mediaMetadata)
Log.d(LogTag, "onMediaMetadataChanged: ${mediaMetadata}")
}
}
}
val exoPlayer = remember {
ExoPlayer.Builder(context)
.setSeekBackIncrementMs(seekBackSeconds * 1000L)
.setSeekForwardIncrementMs(seekForwardSeconds * 1000L)
.build().apply {
this.setMediaItem(mediaItem.value)
this.prepare()
this.playWhenReady = playWhenReady
this.addListener(exoPlayerListener)
}
}
val playerView = remember {
PlayerView(context)
}
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
if (exoPlayer.isPlaying.not()) {
exoPlayer.play()
}
} else if (event == Lifecycle.Event.ON_STOP) {
exoPlayer.pause()
} else if (event == Lifecycle.Event.ON_DESTROY) {
exoPlayer.removeListener(exoPlayerListener)
exoPlayer.release()
Log.i(LogTag, "ON_DESTROY release player")
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
Log.i(LogTag, "Dispose release lifecycle observer")
}
}
val interactionSource = remember { MutableInteractionSource() }
var isControllerViible by remember {
mutableStateOf(false)
}
Box(
modifier = modifier.onNotVisible {
Log.i(LogTag, "onNotVisible on screen called")
if (isInListItem) {
exoPlayer.stop()
exoPlayer.removeListener(exoPlayerListener)
exoPlayer.release()
}
}
) {
AndroidView(
factory = {
playerView.also {
it.requestFocus()
}
},
update = {
it.player = exoPlayer
it.setShowBuffering(PlayerView.SHOW_BUFFERING_WHEN_PLAYING)
it.setControllerAnimationEnabled(true)
it.setShowFastForwardButton(true)
it.setShowRewindButton(true)
it.setShowNextButton(false)
it.setShowPreviousButton(false)
it.showController()
it.setFullscreenButtonClickListener(onFullScreenToggle)
it.setResizeMode(resizeMode.asAspectRatioFrameLayoutResizeMode())
it.setAspectRatioListener { targetAspectRatio, naturalAspectRatio, aspectRatioMismatch ->
Log.i(
LogTag,
"onAspectRatioUpdated: $targetAspectRatio, $naturalAspectRatio, $aspectRatioMismatch"
)
}
it.setControllerVisibilityListener(PlayerView.ControllerVisibilityListener {
isControllerViible = it == View.VISIBLE
})
it.controllerAutoShow = true
if (DeviceType.isTv(context)) {
val playPauseButton =
it.findViewById<ImageButton>(androidx.media3.ui.R.id.exo_play_pause)
val rewindButton =
it.findViewById<Button>(androidx.media3.ui.R.id.exo_rew_with_amount)
val forwardButton =
it.findViewById<Button>(androidx.media3.ui.R.id.exo_ffwd_with_amount)
val settingsButton =
it.findViewById<ImageButton>(androidx.media3.ui.R.id.exo_settings)
listOf(
playPauseButton,
rewindButton,
forwardButton,
settingsButton
).setFocusedBackground()
}
},
modifier = Modifier
.fillMaxSize()
.focusable(
enabled = true,
interactionSource = interactionSource
)
.onFocusChanged {
Log.i(LogTag, "onFocusChanged: ${it.hasFocus}")
}
.onKeyEvent {
if (isControllerViible)
playerView.dispatchKeyEvent(it.nativeKeyEvent)
else {
playerView.showController()
playerView.dispatchKeyEvent(it.nativeKeyEvent)
}
}
.focusRequester(focusRequester),
)
}
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
I have to integrate leanback library please refer any official docs with exoplayer and media3 leanback.
Thanks in advance