diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt index 3d288fa82..264a86ecf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt @@ -57,7 +57,6 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -91,6 +90,8 @@ import androidx.media3.session.MediaController import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView import com.linc.audiowaveform.infiniteLinearGradient +import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache +import com.vitorpamplona.amethyst.commons.compose.produceCachedState import com.vitorpamplona.amethyst.service.playback.PlaybackClientController import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon import com.vitorpamplona.amethyst.ui.note.LyricsIcon @@ -328,6 +329,43 @@ fun VideoViewInner( } } +val mediaItemCache = MediaItemCache() + +@Immutable +data class MediaItemData( + val videoUri: String, + val authorName: String? = null, + val title: String? = null, + val artworkUri: String? = null, +) + +class MediaItemCache() : GenericBaseCache(20) { + override suspend fun compute(data: MediaItemData): MediaItem? { + return MediaItem.Builder() + .setMediaId(data.videoUri) + .setUri(data.videoUri) + .setMediaMetadata( + MediaMetadata.Builder() + .setArtist(data.authorName?.ifBlank { null }) + .setTitle(data.title?.ifBlank { null } ?: data.videoUri) + .setArtworkUri( + try { + if (data.artworkUri != null) { + Uri.parse(data.artworkUri) + } else { + null + } + } catch (e: Exception) { + if (e is CancellationException) throw e + null + }, + ) + .build(), + ) + .build() + } +} + @Composable fun GetMediaItem( videoUri: String, @@ -336,51 +374,15 @@ fun GetMediaItem( authorName: String?, inner: @Composable (State) -> Unit, ) { - val mediaItem = - produceState( - initialValue = null, - key1 = videoUri, - ) { - this.value = - MediaItem.Builder() - .setMediaId(videoUri) - .setUri(videoUri) - .setMediaMetadata( - MediaMetadata.Builder() - .setArtist(authorName?.ifBlank { null }) - .setTitle(title?.ifBlank { null } ?: videoUri) - .setArtworkUri( - try { - if (artworkUri != null) { - Uri.parse(artworkUri) - } else { - null - } - } catch (e: Exception) { - if (e is CancellationException) throw e - null - }, - ) - .build(), - ) - .build() - } + val data = remember(videoUri) { MediaItemData(videoUri, title, artworkUri, authorName) } + val mediaItem by produceCachedState(cache = mediaItemCache, key = data) - mediaItem.value?.let { + mediaItem?.let { val myState = remember(videoUri) { mutableStateOf(it) } inner(myState) } } -@Immutable -sealed class MediaControllerState { - @Immutable object NotStarted : MediaControllerState() - - @Immutable object Loading : MediaControllerState() - - @Stable class Loaded(val instance: MediaController) : MediaControllerState() -} - @Composable @OptIn(UnstableApi::class) fun GetVideoController( @@ -394,12 +396,11 @@ fun GetVideoController( val controller = remember(videoUri) { - val globalMutex = keepPlayingMutex - mutableStateOf( - if (videoUri == globalMutex?.currentMediaItem?.mediaId) { - MediaControllerState.Loaded(globalMutex) + mutableStateOf( + if (videoUri == keepPlayingMutex?.currentMediaItem?.mediaId) { + keepPlayingMutex } else { - MediaControllerState.NotStarted + null }, ) } @@ -407,7 +408,7 @@ fun GetVideoController( val keepPlaying = remember(videoUri) { mutableStateOf( - keepPlayingMutex != null && controller.value == keepPlayingMutex, + keepPlayingMutex != null && controller == keepPlayingMutex, ) } @@ -419,9 +420,7 @@ fun GetVideoController( DisposableEffect(key1 = videoUri) { // If it is not null, the user might have come back from a playing video, like clicking on // the notification of the video player. - if (controller.value == MediaControllerState.NotStarted) { - controller.value = MediaControllerState.Loading - + if (controller.value == null) { scope.launch(Dispatchers.IO) { Log.d("PlaybackService", "Preparing Video $videoUri ") PlaybackClientController.prepareController( @@ -432,29 +431,27 @@ fun GetVideoController( ) { scope.launch(Dispatchers.Main) { // REQUIRED TO BE RUN IN THE MAIN THREAD - - val newState = MediaControllerState.Loaded(it) - if (!it.isPlaying) { if (keepPlayingMutex?.isPlaying == true) { // There is a video playing, start this one on mute. - newState.instance.volume = 0f + it.volume = 0f } else { // There is no other video playing. Use the default mute state to // decide if sound is on or not. - newState.instance.volume = if (defaultToStart) 0f else 1f + it.volume = if (defaultToStart) 0f else 1f } } - newState.instance.setMediaItem(mediaItem.value) - newState.instance.prepare() + it.setMediaItem(mediaItem.value) + it.prepare() - controller.value = newState + controller.value = it } } } - } else if (controller.value is MediaControllerState.Loaded) { - (controller.value as? MediaControllerState.Loaded)?.instance?.let { + } else { + // has been loaded. prepare to play + controller.value?.let { scope.launch(Dispatchers.Main) { if (it.playbackState == Player.STATE_IDLE || it.playbackState == Player.STATE_ENDED) { if (it.isPlaying) { @@ -466,7 +463,10 @@ fun GetVideoController( it.volume = if (defaultToStart) 0f else 1f } - it.setMediaItem(mediaItem.value) + if (mediaItem.value != it.currentMediaItem) { + it.setMediaItem(mediaItem.value) + } + it.prepare() } } @@ -477,11 +477,11 @@ fun GetVideoController( GlobalScope.launch(Dispatchers.Main) { if (!keepPlaying.value) { // Stops and releases the media. - (controller.value as? MediaControllerState.Loaded)?.instance?.let { + controller.value?.let { it.stop() it.release() Log.d("PlaybackService", "Releasing Video $videoUri ") - controller.value = MediaControllerState.NotStarted + controller.value = null } } } @@ -496,12 +496,9 @@ fun GetVideoController( if (event == Lifecycle.Event.ON_RESUME) { // if the controller is null, restarts the controller with a new one // if the controller is not null, just continue playing what the controller was playing - scope.launch(Dispatchers.IO) { - if (controller.value == MediaControllerState.NotStarted) { - controller.value = MediaControllerState.Loading - + if (controller.value == null) { + scope.launch(Dispatchers.IO) { Log.d("PlaybackService", "Preparing Video from Resume $videoUri ") - PlaybackClientController.prepareController( uid, videoUri, @@ -510,25 +507,22 @@ fun GetVideoController( ) { scope.launch(Dispatchers.Main) { // REQUIRED TO BE RUN IN THE MAIN THREAD - - val newState = MediaControllerState.Loaded(it) - // checks again to make sure no other thread has created a controller. if (!it.isPlaying) { if (keepPlayingMutex?.isPlaying == true) { // There is a video playing, start this one on mute. - newState.instance.volume = 0f + it.volume = 0f } else { // There is no other video playing. Use the default mute state to // decide if sound is on or not. - newState.instance.volume = if (defaultToStart) 0f else 1f + it.volume = if (defaultToStart) 0f else 1f } } - newState.instance.setMediaItem(mediaItem.value) - newState.instance.prepare() + it.setMediaItem(mediaItem.value) + it.prepare() - controller.value = newState + controller.value = it } } } @@ -538,11 +532,11 @@ fun GetVideoController( GlobalScope.launch(Dispatchers.Main) { if (!keepPlaying.value) { // Stops and releases the media. - (controller.value as? MediaControllerState.Loaded)?.instance?.let { + controller.value?.let { Log.d("PlaybackService", "Releasing Video from Pause $videoUri ") it.stop() it.release() - controller.value = MediaControllerState.NotStarted + controller.value = null } } } @@ -553,7 +547,9 @@ fun GetVideoController( onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) } } - (controller.value as? MediaControllerState.Loaded)?.let { inner(it.instance, keepPlaying) } + controller.value?.let { + inner(it, keepPlaying) + } } // background playing mutex. diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/compose/CachedState.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/compose/CachedState.kt index ea02558ee..0e9add75f 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/compose/CachedState.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/compose/CachedState.kt @@ -35,6 +35,17 @@ fun produceCachedState( } } +@Composable +fun produceCachedState( + cache: CachedState, + key: String, + updateValue: K, +): State { + return produceState(initialValue = cache.cached(updateValue), key1 = key) { + value = cache.update(updateValue) + } +} + interface CachedState { fun cached(k: K): V?