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