55
66package com.auramusic.app.ui.player
77
8+ import android.view.TextureView
89import android.view.ViewGroup
10+ import androidx.compose.animation.core.animateFloatAsState
11+ import androidx.compose.animation.core.tween
912import androidx.compose.foundation.layout.Box
1013import androidx.compose.foundation.layout.fillMaxSize
1114import androidx.compose.runtime.Composable
@@ -16,15 +19,17 @@ import androidx.compose.runtime.mutableStateOf
1619import androidx.compose.runtime.remember
1720import androidx.compose.runtime.setValue
1821import androidx.compose.ui.Modifier
22+ import androidx.compose.ui.draw.alpha
1923import androidx.compose.ui.platform.LocalContext
2024import androidx.compose.ui.viewinterop.AndroidView
2125import androidx.media3.common.MediaItem
26+ import androidx.media3.common.PlaybackException
2227import androidx.media3.common.Player
2328import androidx.media3.common.util.UnstableApi
2429import androidx.media3.exoplayer.ExoPlayer
2530import androidx.media3.ui.AspectRatioFrameLayout
26- import androidx.media3.ui.PlayerView
2731import com.auramusic.app.playback.AuraCanvasRepository
32+ import timber.log.Timber
2833
2934/* *
3035 * Looping, muted MP4 overlay that renders the matching AuraCanvas video
@@ -42,10 +47,12 @@ fun AuraCanvasOverlay(
4247) {
4348 val context = LocalContext .current
4449 var canvasUrl by remember { mutableStateOf<String ?>(null ) }
50+ var isVideoReady by remember { mutableStateOf(false ) }
4551
4652 // Resolve the URL for the current (title, artist). Cached in the repo.
4753 LaunchedEffect (title, artist) {
4854 canvasUrl = null
55+ isVideoReady = false
4956 canvasUrl = runCatching {
5057 AuraCanvasRepository .findCanvasUrl(title, artist)
5158 }.getOrNull()
@@ -61,34 +68,70 @@ fun AuraCanvasOverlay(
6168 }
6269 }
6370
71+ // Attach listener for errors and first frame
72+ DisposableEffect (exoPlayer) {
73+ val listener = object : Player .Listener {
74+ override fun onPlayerError (error : PlaybackException ) {
75+ Timber .e(" AuraCanvas playback error: ${error.errorCodeName} - ${error.message} " )
76+ isVideoReady = false
77+ }
78+
79+ override fun onRenderedFirstFrame () {
80+ isVideoReady = true
81+ }
82+ }
83+ exoPlayer.addListener(listener)
84+ onDispose {
85+ exoPlayer.removeListener(listener)
86+ }
87+ }
88+
6489 // Swap the source whenever the resolved URL changes.
6590 LaunchedEffect (url) {
91+ isVideoReady = false
92+ exoPlayer.stop()
6693 exoPlayer.setMediaItem(MediaItem .fromUri(url))
6794 exoPlayer.prepare()
6895 exoPlayer.playWhenReady = true
6996 }
7097
71- DisposableEffect (Unit ) {
72- onDispose { exoPlayer.release() }
98+ DisposableEffect (exoPlayer) {
99+ onDispose {
100+ exoPlayer.release()
101+ }
73102 }
74103
104+ val alpha by animateFloatAsState(
105+ targetValue = if (isVideoReady) 1f else 0f ,
106+ animationSpec = tween(durationMillis = 250 ),
107+ label = " canvasFade"
108+ )
109+
75110 Box (modifier = modifier.fillMaxSize()) {
76111 AndroidView (
77112 factory = { ctx ->
78- PlayerView (ctx).apply {
79- player = exoPlayer
80- useController = false
81- resizeMode = AspectRatioFrameLayout .RESIZE_MODE_ZOOM
82- setBackgroundColor(android.graphics.Color .TRANSPARENT )
83- setShutterBackgroundColor(android.graphics.Color .TRANSPARENT )
113+ AspectRatioFrameLayout (ctx).apply {
84114 layoutParams = ViewGroup .LayoutParams (
85115 ViewGroup .LayoutParams .MATCH_PARENT ,
86- ViewGroup .LayoutParams .MATCH_PARENT ,
116+ ViewGroup .LayoutParams .MATCH_PARENT
87117 )
118+ resizeMode = AspectRatioFrameLayout .RESIZE_MODE_ZOOM
119+
120+ val textureView = TextureView (ctx).apply {
121+ layoutParams = ViewGroup .LayoutParams (
122+ ViewGroup .LayoutParams .MATCH_PARENT ,
123+ ViewGroup .LayoutParams .MATCH_PARENT
124+ )
125+ }
126+ addView(textureView)
127+ exoPlayer.setVideoTextureView(textureView)
128+ setBackgroundColor(android.graphics.Color .TRANSPARENT )
88129 }
89130 },
90- modifier = Modifier .fillMaxSize(),
91- update = { it.player = exoPlayer },
131+ modifier = Modifier
132+ .fillMaxSize()
133+ .alpha(alpha),
134+ update = { /* no-op */ }
92135 )
93136 }
94137}
0 commit comments