Tweak player code

This commit is contained in:
vfsfitvnm 2022-10-16 20:42:16 +02:00
parent 7869f1a388
commit 6ebb5dfc65
8 changed files with 186 additions and 200 deletions

View file

@ -81,7 +81,6 @@ import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey
import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.getEnum import it.vfsfitvnm.vimusic.utils.getEnum
import it.vfsfitvnm.vimusic.utils.intent import it.vfsfitvnm.vimusic.utils.intent
import it.vfsfitvnm.vimusic.utils.listener
import it.vfsfitvnm.vimusic.utils.preferences import it.vfsfitvnm.vimusic.utils.preferences
import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey
import it.vfsfitvnm.innertube.Innertube import it.vfsfitvnm.innertube.Innertube
@ -366,7 +365,7 @@ class MainActivity : ComponentActivity() {
} }
} }
player.listener(object : Player.Listener { val listener = object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED && mediaItem != null) { if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED && mediaItem != null) {
if (mediaItem.mediaMetadata.extras?.getBoolean("isFromPersistentQueue") != true) { if (mediaItem.mediaMetadata.extras?.getBoolean("isFromPersistentQueue") != true) {
@ -376,7 +375,11 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
}) }
player.addListener(listener)
onDispose { player.removeListener(listener) }
} }
} }
} }

View file

@ -45,10 +45,10 @@ import it.vfsfitvnm.vimusic.ui.components.SeekBar
import it.vfsfitvnm.vimusic.ui.components.themed.IconButton import it.vfsfitvnm.vimusic.ui.components.themed.IconButton
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.favoritesIcon import it.vfsfitvnm.vimusic.ui.styling.favoritesIcon
import it.vfsfitvnm.vimusic.utils.DisposableListener
import it.vfsfitvnm.vimusic.utils.bold import it.vfsfitvnm.vimusic.utils.bold
import it.vfsfitvnm.vimusic.utils.forceSeekToNext import it.vfsfitvnm.vimusic.utils.forceSeekToNext
import it.vfsfitvnm.vimusic.utils.forceSeekToPrevious import it.vfsfitvnm.vimusic.utils.forceSeekToPrevious
import it.vfsfitvnm.vimusic.utils.rememberRepeatMode
import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.semiBold
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -70,7 +70,17 @@ fun Controls(
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
binder?.player ?: return binder?.player ?: return
val repeatMode by rememberRepeatMode(binder.player) var repeatMode by remember {
mutableStateOf(binder.player.repeatMode)
}
binder.player.DisposableListener {
object : Player.Listener {
override fun onRepeatModeChanged(newRepeatMode: Int) {
repeatMode = newRepeatMode
}
}
}
var scrubbingPosition by remember(mediaId) { var scrubbingPosition by remember(mediaId) {
mutableStateOf<Long?>(null) mutableStateOf<Long?>(null)

View file

@ -29,6 +29,8 @@ import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.neverEqualPolicy
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -44,6 +46,7 @@ import androidx.compose.ui.unit.dp
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import coil.compose.AsyncImage import coil.compose.AsyncImage
import it.vfsfitvnm.innertube.models.NavigationEndpoint
import it.vfsfitvnm.route.OnGlobalRoute import it.vfsfitvnm.route.OnGlobalRoute
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
@ -58,16 +61,15 @@ import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.collapsedPlayerProgressBar import it.vfsfitvnm.vimusic.ui.styling.collapsedPlayerProgressBar
import it.vfsfitvnm.vimusic.ui.styling.px import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.utils.DisposableListener
import it.vfsfitvnm.vimusic.utils.forceSeekToNext import it.vfsfitvnm.vimusic.utils.forceSeekToNext
import it.vfsfitvnm.vimusic.utils.isLandscape import it.vfsfitvnm.vimusic.utils.isLandscape
import it.vfsfitvnm.vimusic.utils.rememberMediaItem import it.vfsfitvnm.vimusic.utils.positionAndDurationState
import it.vfsfitvnm.vimusic.utils.rememberPositionAndDuration
import it.vfsfitvnm.vimusic.utils.rememberShouldBePlaying
import it.vfsfitvnm.vimusic.utils.seamlessPlay import it.vfsfitvnm.vimusic.utils.seamlessPlay
import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.shouldBePlaying
import it.vfsfitvnm.vimusic.utils.thumbnail import it.vfsfitvnm.vimusic.utils.thumbnail
import it.vfsfitvnm.innertube.models.NavigationEndpoint
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ExperimentalFoundationApi @ExperimentalFoundationApi
@ -84,12 +86,33 @@ fun Player(
binder?.player ?: return binder?.player ?: return
val nullableMediaItem by rememberMediaItem(binder.player) var nullableMediaItem by remember {
mutableStateOf(binder.player.currentMediaItem, neverEqualPolicy())
}
var shouldBePlaying by remember {
mutableStateOf(binder.player.shouldBePlaying)
}
binder.player.DisposableListener {
object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
nullableMediaItem = mediaItem
}
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
shouldBePlaying = binder.player.shouldBePlaying
}
override fun onPlaybackStateChanged(playbackState: Int) {
shouldBePlaying = binder.player.shouldBePlaying
}
}
}
val mediaItem = nullableMediaItem ?: return val mediaItem = nullableMediaItem ?: return
val shouldBePlaying by rememberShouldBePlaying(binder.player) val positionAndDuration by binder.player.positionAndDurationState()
val positionAndDuration by rememberPositionAndDuration(binder.player)
val windowInsets = WindowInsets.systemBars val windowInsets = WindowInsets.systemBars

View file

@ -31,6 +31,9 @@ import androidx.compose.foundation.text.BasicText
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
@ -40,6 +43,9 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.Timeline
import com.valentinilk.shimmer.shimmer import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.reordering.ReorderingLazyColumn import it.vfsfitvnm.reordering.ReorderingLazyColumn
import it.vfsfitvnm.reordering.animateItemPlacement import it.vfsfitvnm.reordering.animateItemPlacement
@ -61,12 +67,12 @@ import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.onOverlay import it.vfsfitvnm.vimusic.ui.styling.onOverlay
import it.vfsfitvnm.vimusic.ui.styling.px import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.utils.DisposableListener
import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.rememberMediaItemIndex import it.vfsfitvnm.vimusic.utils.shouldBePlaying
import it.vfsfitvnm.vimusic.utils.rememberShouldBePlaying
import it.vfsfitvnm.vimusic.utils.rememberWindows
import it.vfsfitvnm.vimusic.utils.shuffleQueue import it.vfsfitvnm.vimusic.utils.shuffleQueue
import it.vfsfitvnm.vimusic.utils.smoothScrollToTop import it.vfsfitvnm.vimusic.utils.smoothScrollToTop
import it.vfsfitvnm.vimusic.utils.windows
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ExperimentalFoundationApi @ExperimentalFoundationApi
@ -112,19 +118,52 @@ fun Queue(
binder?.player ?: return@BottomSheet binder?.player ?: return@BottomSheet
val player = binder.player
val menuState = LocalMenuState.current val menuState = LocalMenuState.current
val thumbnailSizeDp = Dimensions.thumbnails.song val thumbnailSizeDp = Dimensions.thumbnails.song
val thumbnailSizePx = thumbnailSizeDp.px val thumbnailSizePx = thumbnailSizeDp.px
val mediaItemIndex by rememberMediaItemIndex(binder.player) var mediaItemIndex by remember {
val windows by rememberWindows(binder.player) mutableStateOf(if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex)
val shouldBePlaying by rememberShouldBePlaying(binder.player) }
var windows by remember {
mutableStateOf(player.currentTimeline.windows)
}
var shouldBePlaying by remember {
mutableStateOf(binder.player.shouldBePlaying)
}
player.DisposableListener {
object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
mediaItemIndex =
if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex
}
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
windows = timeline.windows
mediaItemIndex =
if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex
}
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
shouldBePlaying = binder.player.shouldBePlaying
}
override fun onPlaybackStateChanged(playbackState: Int) {
shouldBePlaying = binder.player.shouldBePlaying
}
}
}
val reorderingState = rememberReorderingState( val reorderingState = rememberReorderingState(
lazyListState = rememberLazyListState(initialFirstVisibleItemIndex = mediaItemIndex), lazyListState = rememberLazyListState(initialFirstVisibleItemIndex = mediaItemIndex),
key = windows, key = windows,
onDragEnd = binder.player::moveMediaItem, onDragEnd = player::moveMediaItem,
extraItemCount = 0 extraItemCount = 0
) )
@ -219,13 +258,13 @@ fun Queue(
onClick = { onClick = {
if (isPlayingThisMediaItem) { if (isPlayingThisMediaItem) {
if (shouldBePlaying) { if (shouldBePlaying) {
binder.player.pause() player.pause()
} else { } else {
binder.player.play() player.play()
} }
} else { } else {
binder.player.playWhenReady = true player.playWhenReady = true
binder.player.seekToDefaultPosition(window.firstPeriodIndex) player.seekToDefaultPosition(window.firstPeriodIndex)
} }
} }
) )
@ -266,7 +305,7 @@ fun Queue(
reorderingState.coroutineScope.launch { reorderingState.coroutineScope.launch {
reorderingState.lazyListState.smoothScrollToTop() reorderingState.lazyListState.smoothScrollToTop()
}.invokeOnCompletion { }.invokeOnCompletion {
binder.player.shuffleQueue() player.shuffleQueue()
} }
} }
) )

View file

@ -26,8 +26,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.media3.common.Player
import androidx.media3.datasource.cache.Cache import androidx.media3.datasource.cache.Cache
import androidx.media3.datasource.cache.CacheSpan import androidx.media3.datasource.cache.CacheSpan
import it.vfsfitvnm.innertube.Innertube
import it.vfsfitvnm.innertube.models.bodies.PlayerBody
import it.vfsfitvnm.innertube.requests.player
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.models.Format import it.vfsfitvnm.vimusic.models.Format
@ -35,12 +39,9 @@ import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.onOverlay import it.vfsfitvnm.vimusic.ui.styling.onOverlay
import it.vfsfitvnm.vimusic.ui.styling.overlay import it.vfsfitvnm.vimusic.ui.styling.overlay
import it.vfsfitvnm.vimusic.utils.DisposableListener
import it.vfsfitvnm.vimusic.utils.color import it.vfsfitvnm.vimusic.utils.color
import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.rememberVolume
import it.vfsfitvnm.innertube.Innertube
import it.vfsfitvnm.innertube.models.bodies.PlayerBody
import it.vfsfitvnm.innertube.requests.player
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@ -70,7 +71,17 @@ fun StatsForNerds(
Database.format(mediaId).distinctUntilChanged() Database.format(mediaId).distinctUntilChanged()
}.collectAsState(initial = null, context = Dispatchers.IO) }.collectAsState(initial = null, context = Dispatchers.IO)
val volume by rememberVolume(binder.player) var volume by remember {
mutableStateOf(binder.player.volume)
}
binder.player.DisposableListener {
object : Player.Listener {
override fun onVolumeChanged(newVolume: Float) {
volume = newVolume
}
}
}
DisposableEffect(mediaId) { DisposableEffect(mediaId) {
val listener = object : Cache.Listener { val listener = object : Cache.Listener {
@ -193,7 +204,8 @@ fun StatsForNerds(
onClick = { onClick = {
query { query {
runBlocking(Dispatchers.IO) { runBlocking(Dispatchers.IO) {
Innertube.player(PlayerBody(videoId = mediaId)) Innertube
.player(PlayerBody(videoId = mediaId))
?.map { response -> ?.map { response ->
response.streamingData?.adaptiveFormats response.streamingData?.adaptiveFormats
?.findLast { format -> ?.findLast { format ->
@ -205,7 +217,9 @@ fun StatsForNerds(
itag = format.itag, itag = format.itag,
mimeType = format.mimeType, mimeType = format.mimeType,
bitrate = format.bitrate, bitrate = format.bitrate,
loudnessDb = response.playerConfig?.audioConfig?.loudnessDb?.toFloat()?.plus(7), loudnessDb = response.playerConfig?.audioConfig?.loudnessDb
?.toFloat()
?.plus(7),
contentLength = format.contentLength, contentLength = format.contentLength,
lastModified = format.lastModified lastModified = format.lastModified
) )

View file

@ -17,12 +17,18 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import coil.compose.AsyncImage import coil.compose.AsyncImage
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
@ -34,9 +40,8 @@ import it.vfsfitvnm.vimusic.service.VideoIdMismatchException
import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.px import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.utils.rememberError import it.vfsfitvnm.vimusic.utils.currentWindow
import it.vfsfitvnm.vimusic.utils.rememberMediaItem import it.vfsfitvnm.vimusic.utils.DisposableListener
import it.vfsfitvnm.vimusic.utils.rememberMediaItemIndex
import it.vfsfitvnm.vimusic.utils.thumbnail import it.vfsfitvnm.vimusic.utils.thumbnail
import java.net.UnknownHostException import java.net.UnknownHostException
import java.nio.channels.UnresolvedAddressException import java.nio.channels.UnresolvedAddressException
@ -57,17 +62,38 @@ fun Thumbnail(
it to (it - 64.dp).px it to (it - 64.dp).px
} }
val mediaItemIndex by rememberMediaItemIndex(player) var nullableWindow by remember {
val mediaItem by rememberMediaItem(player) mutableStateOf(player.currentWindow)
}
val error by rememberError(player) var error by remember {
mutableStateOf<PlaybackException?>(player.playerError)
}
player.DisposableListener {
object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
nullableWindow = player.currentWindow
}
override fun onPlaybackStateChanged(playbackState: Int) {
error = player.playerError
}
override fun onPlayerError(playbackException: PlaybackException) {
error = playbackException
}
}
}
val window = nullableWindow ?: return
AnimatedContent( AnimatedContent(
targetState = mediaItemIndex to mediaItem, targetState = window,
transitionSpec = { transitionSpec = {
val duration = 500 val duration = 500
val slideDirection = val slideDirection =
if (targetState.first > initialState.first) AnimatedContentScope.SlideDirection.Left else AnimatedContentScope.SlideDirection.Right if (targetState.firstPeriodIndex > initialState.firstPeriodIndex) AnimatedContentScope.SlideDirection.Left else AnimatedContentScope.SlideDirection.Right
ContentTransform( ContentTransform(
targetContentEnter = slideIntoContainer( targetContentEnter = slideIntoContainer(
@ -92,9 +118,7 @@ fun Thumbnail(
) )
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { (_, currentMediaItem) -> ) {currentWindow ->
val currentMediaItem = currentMediaItem ?: return@AnimatedContent
Box( Box(
modifier = modifier modifier = modifier
.aspectRatio(1f) .aspectRatio(1f)
@ -102,7 +126,7 @@ fun Thumbnail(
.size(thumbnailSizeDp) .size(thumbnailSizeDp)
) { ) {
AsyncImage( AsyncImage(
model = currentMediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx), model = currentWindow.mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx),
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = Modifier modifier = Modifier
@ -116,23 +140,23 @@ fun Thumbnail(
) )
Lyrics( Lyrics(
mediaId = currentMediaItem.mediaId, mediaId = currentWindow.mediaItem.mediaId,
isDisplayed = isShowingLyrics && error == null, isDisplayed = isShowingLyrics && error == null,
onDismiss = { onShowLyrics(false) }, onDismiss = { onShowLyrics(false) },
onLyricsUpdate = { areSynchronized, mediaId, lyrics -> onLyricsUpdate = { areSynchronized, mediaId, lyrics ->
query { query {
if (areSynchronized) { if (areSynchronized) {
if (Database.updateSynchronizedLyrics(mediaId, lyrics) == 0) { if (Database.updateSynchronizedLyrics(mediaId, lyrics) == 0) {
if (mediaId == currentMediaItem.mediaId) { if (mediaId == currentWindow.mediaItem.mediaId) {
Database.insert(currentMediaItem) { song -> Database.insert(currentWindow.mediaItem) { song ->
song.copy(synchronizedLyrics = lyrics) song.copy(synchronizedLyrics = lyrics)
} }
} }
} }
} else { } else {
if (Database.updateLyrics(mediaId, lyrics) == 0) { if (Database.updateLyrics(mediaId, lyrics) == 0) {
if (mediaId == currentMediaItem.mediaId) { if (mediaId == currentWindow.mediaItem.mediaId) {
Database.insert(currentMediaItem) { song -> Database.insert(currentWindow.mediaItem) { song ->
song.copy(lyrics = lyrics) song.copy(lyrics = lyrics)
} }
} }
@ -141,12 +165,12 @@ fun Thumbnail(
} }
}, },
size = thumbnailSizeDp, size = thumbnailSizeDp,
mediaMetadataProvider = currentMediaItem::mediaMetadata, mediaMetadataProvider = currentWindow.mediaItem::mediaMetadata,
durationProvider = player::getDuration, durationProvider = player::getDuration,
) )
StatsForNerds( StatsForNerds(
mediaId = currentMediaItem.mediaId, mediaId = currentWindow.mediaItem.mediaId,
isDisplayed = isShowingStatsForNerds && error == null, isDisplayed = isShowingStatsForNerds && error == null,
onDismiss = { onShowStatsForNerds(false) } onDismiss = { onShowStatsForNerds(false) }
) )

View file

@ -5,6 +5,9 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Timeline import androidx.media3.common.Timeline
val Player.currentWindow: Timeline.Window?
get() = if (mediaItemCount == 0) null else currentTimeline.getWindow(currentMediaItemIndex, Timeline.Window())
val Timeline.mediaItems: List<MediaItem> val Timeline.mediaItems: List<MediaItem>
get() = List(windowCount) { get() = List(windowCount) {
getWindow(it, Timeline.Window()).mediaItem getWindow(it, Timeline.Window()).mediaItem

View file

@ -2,127 +2,33 @@ package it.vfsfitvnm.vimusic.utils
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.DisposableEffectResult import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.DisposableEffectScope
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.neverEqualPolicy
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Timeline
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
context(DisposableEffectScope) @Composable
fun Player.listener(listener: Player.Listener): DisposableEffectResult { inline fun Player.DisposableListener(crossinline listenerProvider: () -> Player.Listener) {
addListener(listener) DisposableEffect(this) {
return onDispose { val listener = listenerProvider()
removeListener(listener) addListener(listener)
onDispose { removeListener(listener) }
} }
} }
@Composable @Composable
fun rememberMediaItemIndex(player: Player): State<Int> { fun Player.positionAndDurationState(): State<Pair<Long, Long>> {
val mediaItemIndexState = remember(player) { val state = remember {
mutableStateOf(if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex) mutableStateOf(currentPosition to duration)
} }
DisposableEffect(player) { LaunchedEffect(this) {
player.listener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
mediaItemIndexState.value =
if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex
}
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
mediaItemIndexState.value =
if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex
}
})
}
return mediaItemIndexState
}
@Composable
fun rememberMediaItem(player: Player): State<MediaItem?> {
val state = remember(player) {
mutableStateOf(player.currentMediaItem, neverEqualPolicy())
}
DisposableEffect(player) {
player.listener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
state.value = mediaItem
}
})
}
return state
}
@Composable
fun rememberWindows(player: Player): State<List<Timeline.Window>> {
val windowsState = remember(player) {
mutableStateOf(player.currentTimeline.windows)
}
DisposableEffect(player) {
player.listener(object : Player.Listener {
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
windowsState.value = timeline.windows
}
})
}
return windowsState
}
@Composable
fun rememberShouldBePlaying(player: Player): State<Boolean> {
val state = remember(player) {
mutableStateOf(!(player.playbackState == Player.STATE_ENDED || !player.playWhenReady))
}
DisposableEffect(player) {
player.listener(object : Player.Listener {
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
state.value = !(player.playbackState == Player.STATE_ENDED || !playWhenReady)
}
override fun onPlaybackStateChanged(playbackState: Int) {
state.value = !(playbackState == Player.STATE_ENDED || !player.playWhenReady)
}
})
}
return state
}
@Composable
fun rememberRepeatMode(player: Player): State<Int> {
val state = remember(player) {
mutableStateOf(player.repeatMode)
}
DisposableEffect(player) {
player.listener(object : Player.Listener {
override fun onRepeatModeChanged(repeatMode: Int) {
state.value = repeatMode
}
})
}
return state
}
@Composable
fun rememberPositionAndDuration(player: Player): State<Pair<Long, Long>> {
val state = produceState(initialValue = player.currentPosition to player.duration) {
var isSeeking = false var isSeeking = false
val listener = object : Player.Listener { val listener = object : Player.Listener {
@ -133,7 +39,7 @@ fun rememberPositionAndDuration(player: Player): State<Pair<Long, Long>> {
} }
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
value = player.currentPosition to value.second state.value = currentPosition to state.value.second
} }
override fun onPositionDiscontinuity( override fun onPositionDiscontinuity(
@ -143,65 +49,29 @@ fun rememberPositionAndDuration(player: Player): State<Pair<Long, Long>> {
) { ) {
if (reason == Player.DISCONTINUITY_REASON_SEEK) { if (reason == Player.DISCONTINUITY_REASON_SEEK) {
isSeeking = true isSeeking = true
value = player.currentPosition to player.duration state.value = currentPosition to duration
} }
} }
} }
player.addListener(listener) addListener(listener)
val pollJob = launch { val pollJob = launch {
while (isActive) { while (isActive) {
delay(500) delay(500)
if (!isSeeking) { if (!isSeeking) {
value = player.currentPosition to player.duration state.value = currentPosition to duration
} }
} }
} }
awaitDispose { try {
suspendCancellableCoroutine<Nothing> { }
} finally {
pollJob.cancel() pollJob.cancel()
player.removeListener(listener) removeListener(listener)
} }
} }
return state return state
} }
@Composable
fun rememberVolume(player: Player): State<Float> {
val volumeState = remember(player) {
mutableStateOf(player.volume)
}
DisposableEffect(player) {
player.listener(object : Player.Listener {
override fun onVolumeChanged(volume: Float) {
volumeState.value = volume
}
})
}
return volumeState
}
@Composable
fun rememberError(player: Player): State<PlaybackException?> {
val errorState = remember(player) {
mutableStateOf(player.playerError)
}
DisposableEffect(player) {
player.listener(object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
errorState.value = player.playerError
}
override fun onPlayerError(playbackException: PlaybackException) {
errorState.value = playbackException
}
})
}
return errorState
}