Makes a cache for Media Items

This commit is contained in:
Vitor Pamplona 2024-03-26 11:56:10 -04:00
parent b45f9bd460
commit cd32c4db72
2 changed files with 85 additions and 78 deletions

View File

@ -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<MediaItemData, MediaItem>(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<MediaItem>) -> Unit,
) {
val mediaItem =
produceState<MediaItem?>(
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<MediaControllerState>(
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<Boolean>(
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.

View File

@ -35,6 +35,17 @@ fun <K, V> produceCachedState(
}
}
@Composable
fun <K, V> produceCachedState(
cache: CachedState<K, V>,
key: String,
updateValue: K,
): State<V?> {
return produceState(initialValue = cache.cached(updateValue), key1 = key) {
value = cache.update(updateValue)
}
}
interface CachedState<K, V> {
fun cached(k: K): V?