Tweak player code
This commit is contained in:
parent
7869f1a388
commit
6ebb5dfc65
8 changed files with 186 additions and 200 deletions
|
@ -81,7 +81,6 @@ import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey
|
|||
import it.vfsfitvnm.vimusic.utils.forcePlay
|
||||
import it.vfsfitvnm.vimusic.utils.getEnum
|
||||
import it.vfsfitvnm.vimusic.utils.intent
|
||||
import it.vfsfitvnm.vimusic.utils.listener
|
||||
import it.vfsfitvnm.vimusic.utils.preferences
|
||||
import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey
|
||||
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) {
|
||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED && mediaItem != null) {
|
||||
if (mediaItem.mediaMetadata.extras?.getBoolean("isFromPersistentQueue") != true) {
|
||||
|
@ -376,7 +375,11 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
player.addListener(listener)
|
||||
|
||||
onDispose { player.removeListener(listener) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,10 +45,10 @@ import it.vfsfitvnm.vimusic.ui.components.SeekBar
|
|||
import it.vfsfitvnm.vimusic.ui.components.themed.IconButton
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
import it.vfsfitvnm.vimusic.ui.styling.favoritesIcon
|
||||
import it.vfsfitvnm.vimusic.utils.DisposableListener
|
||||
import it.vfsfitvnm.vimusic.utils.bold
|
||||
import it.vfsfitvnm.vimusic.utils.forceSeekToNext
|
||||
import it.vfsfitvnm.vimusic.utils.forceSeekToPrevious
|
||||
import it.vfsfitvnm.vimusic.utils.rememberRepeatMode
|
||||
import it.vfsfitvnm.vimusic.utils.secondary
|
||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -70,7 +70,17 @@ fun Controls(
|
|||
val binder = LocalPlayerServiceBinder.current
|
||||
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) {
|
||||
mutableStateOf<Long?>(null)
|
||||
|
|
|
@ -29,6 +29,8 @@ import androidx.compose.foundation.text.BasicText
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
|
@ -44,6 +46,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import coil.compose.AsyncImage
|
||||
import it.vfsfitvnm.innertube.models.NavigationEndpoint
|
||||
import it.vfsfitvnm.route.OnGlobalRoute
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||
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.collapsedPlayerProgressBar
|
||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||
import it.vfsfitvnm.vimusic.utils.DisposableListener
|
||||
import it.vfsfitvnm.vimusic.utils.forceSeekToNext
|
||||
import it.vfsfitvnm.vimusic.utils.isLandscape
|
||||
import it.vfsfitvnm.vimusic.utils.rememberMediaItem
|
||||
import it.vfsfitvnm.vimusic.utils.rememberPositionAndDuration
|
||||
import it.vfsfitvnm.vimusic.utils.rememberShouldBePlaying
|
||||
import it.vfsfitvnm.vimusic.utils.positionAndDurationState
|
||||
import it.vfsfitvnm.vimusic.utils.seamlessPlay
|
||||
import it.vfsfitvnm.vimusic.utils.secondary
|
||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||
import it.vfsfitvnm.vimusic.utils.shouldBePlaying
|
||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||
import it.vfsfitvnm.innertube.models.NavigationEndpoint
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
|
@ -84,12 +86,33 @@ fun Player(
|
|||
|
||||
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 shouldBePlaying by rememberShouldBePlaying(binder.player)
|
||||
val positionAndDuration by rememberPositionAndDuration(binder.player)
|
||||
val positionAndDuration by binder.player.positionAndDurationState()
|
||||
|
||||
val windowInsets = WindowInsets.systemBars
|
||||
|
||||
|
|
|
@ -31,6 +31,9 @@ import androidx.compose.foundation.text.BasicText
|
|||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
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.Modifier
|
||||
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.res.painterResource
|
||||
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 it.vfsfitvnm.reordering.ReorderingLazyColumn
|
||||
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.onOverlay
|
||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||
import it.vfsfitvnm.vimusic.utils.DisposableListener
|
||||
import it.vfsfitvnm.vimusic.utils.medium
|
||||
import it.vfsfitvnm.vimusic.utils.rememberMediaItemIndex
|
||||
import it.vfsfitvnm.vimusic.utils.rememberShouldBePlaying
|
||||
import it.vfsfitvnm.vimusic.utils.rememberWindows
|
||||
import it.vfsfitvnm.vimusic.utils.shouldBePlaying
|
||||
import it.vfsfitvnm.vimusic.utils.shuffleQueue
|
||||
import it.vfsfitvnm.vimusic.utils.smoothScrollToTop
|
||||
import it.vfsfitvnm.vimusic.utils.windows
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
|
@ -112,19 +118,52 @@ fun Queue(
|
|||
|
||||
binder?.player ?: return@BottomSheet
|
||||
|
||||
val player = binder.player
|
||||
|
||||
val menuState = LocalMenuState.current
|
||||
|
||||
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||
val thumbnailSizePx = thumbnailSizeDp.px
|
||||
|
||||
val mediaItemIndex by rememberMediaItemIndex(binder.player)
|
||||
val windows by rememberWindows(binder.player)
|
||||
val shouldBePlaying by rememberShouldBePlaying(binder.player)
|
||||
var mediaItemIndex by remember {
|
||||
mutableStateOf(if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex)
|
||||
}
|
||||
|
||||
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(
|
||||
lazyListState = rememberLazyListState(initialFirstVisibleItemIndex = mediaItemIndex),
|
||||
key = windows,
|
||||
onDragEnd = binder.player::moveMediaItem,
|
||||
onDragEnd = player::moveMediaItem,
|
||||
extraItemCount = 0
|
||||
)
|
||||
|
||||
|
@ -219,13 +258,13 @@ fun Queue(
|
|||
onClick = {
|
||||
if (isPlayingThisMediaItem) {
|
||||
if (shouldBePlaying) {
|
||||
binder.player.pause()
|
||||
player.pause()
|
||||
} else {
|
||||
binder.player.play()
|
||||
player.play()
|
||||
}
|
||||
} else {
|
||||
binder.player.playWhenReady = true
|
||||
binder.player.seekToDefaultPosition(window.firstPeriodIndex)
|
||||
player.playWhenReady = true
|
||||
player.seekToDefaultPosition(window.firstPeriodIndex)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -266,7 +305,7 @@ fun Queue(
|
|||
reorderingState.coroutineScope.launch {
|
||||
reorderingState.lazyListState.smoothScrollToTop()
|
||||
}.invokeOnCompletion {
|
||||
binder.player.shuffleQueue()
|
||||
player.shuffleQueue()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -26,8 +26,12 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.datasource.cache.Cache
|
||||
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.LocalPlayerServiceBinder
|
||||
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.onOverlay
|
||||
import it.vfsfitvnm.vimusic.ui.styling.overlay
|
||||
import it.vfsfitvnm.vimusic.utils.DisposableListener
|
||||
import it.vfsfitvnm.vimusic.utils.color
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
@ -70,7 +71,17 @@ fun StatsForNerds(
|
|||
Database.format(mediaId).distinctUntilChanged()
|
||||
}.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) {
|
||||
val listener = object : Cache.Listener {
|
||||
|
@ -193,7 +204,8 @@ fun StatsForNerds(
|
|||
onClick = {
|
||||
query {
|
||||
runBlocking(Dispatchers.IO) {
|
||||
Innertube.player(PlayerBody(videoId = mediaId))
|
||||
Innertube
|
||||
.player(PlayerBody(videoId = mediaId))
|
||||
?.map { response ->
|
||||
response.streamingData?.adaptiveFormats
|
||||
?.findLast { format ->
|
||||
|
@ -205,7 +217,9 @@ fun StatsForNerds(
|
|||
itag = format.itag,
|
||||
mimeType = format.mimeType,
|
||||
bitrate = format.bitrate,
|
||||
loudnessDb = response.playerConfig?.audioConfig?.loudnessDb?.toFloat()?.plus(7),
|
||||
loudnessDb = response.playerConfig?.audioConfig?.loudnessDb
|
||||
?.toFloat()
|
||||
?.plus(7),
|
||||
contentLength = format.contentLength,
|
||||
lastModified = format.lastModified
|
||||
)
|
||||
|
|
|
@ -17,12 +17,18 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
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 it.vfsfitvnm.vimusic.Database
|
||||
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.LocalAppearance
|
||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||
import it.vfsfitvnm.vimusic.utils.rememberError
|
||||
import it.vfsfitvnm.vimusic.utils.rememberMediaItem
|
||||
import it.vfsfitvnm.vimusic.utils.rememberMediaItemIndex
|
||||
import it.vfsfitvnm.vimusic.utils.currentWindow
|
||||
import it.vfsfitvnm.vimusic.utils.DisposableListener
|
||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||
import java.net.UnknownHostException
|
||||
import java.nio.channels.UnresolvedAddressException
|
||||
|
@ -57,17 +62,38 @@ fun Thumbnail(
|
|||
it to (it - 64.dp).px
|
||||
}
|
||||
|
||||
val mediaItemIndex by rememberMediaItemIndex(player)
|
||||
val mediaItem by rememberMediaItem(player)
|
||||
var nullableWindow by remember {
|
||||
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(
|
||||
targetState = mediaItemIndex to mediaItem,
|
||||
targetState = window,
|
||||
transitionSpec = {
|
||||
val duration = 500
|
||||
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(
|
||||
targetContentEnter = slideIntoContainer(
|
||||
|
@ -92,9 +118,7 @@ fun Thumbnail(
|
|||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) { (_, currentMediaItem) ->
|
||||
val currentMediaItem = currentMediaItem ?: return@AnimatedContent
|
||||
|
||||
) {currentWindow ->
|
||||
Box(
|
||||
modifier = modifier
|
||||
.aspectRatio(1f)
|
||||
|
@ -102,7 +126,7 @@ fun Thumbnail(
|
|||
.size(thumbnailSizeDp)
|
||||
) {
|
||||
AsyncImage(
|
||||
model = currentMediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx),
|
||||
model = currentWindow.mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
|
@ -116,23 +140,23 @@ fun Thumbnail(
|
|||
)
|
||||
|
||||
Lyrics(
|
||||
mediaId = currentMediaItem.mediaId,
|
||||
mediaId = currentWindow.mediaItem.mediaId,
|
||||
isDisplayed = isShowingLyrics && error == null,
|
||||
onDismiss = { onShowLyrics(false) },
|
||||
onLyricsUpdate = { areSynchronized, mediaId, lyrics ->
|
||||
query {
|
||||
if (areSynchronized) {
|
||||
if (Database.updateSynchronizedLyrics(mediaId, lyrics) == 0) {
|
||||
if (mediaId == currentMediaItem.mediaId) {
|
||||
Database.insert(currentMediaItem) { song ->
|
||||
if (mediaId == currentWindow.mediaItem.mediaId) {
|
||||
Database.insert(currentWindow.mediaItem) { song ->
|
||||
song.copy(synchronizedLyrics = lyrics)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (Database.updateLyrics(mediaId, lyrics) == 0) {
|
||||
if (mediaId == currentMediaItem.mediaId) {
|
||||
Database.insert(currentMediaItem) { song ->
|
||||
if (mediaId == currentWindow.mediaItem.mediaId) {
|
||||
Database.insert(currentWindow.mediaItem) { song ->
|
||||
song.copy(lyrics = lyrics)
|
||||
}
|
||||
}
|
||||
|
@ -141,12 +165,12 @@ fun Thumbnail(
|
|||
}
|
||||
},
|
||||
size = thumbnailSizeDp,
|
||||
mediaMetadataProvider = currentMediaItem::mediaMetadata,
|
||||
mediaMetadataProvider = currentWindow.mediaItem::mediaMetadata,
|
||||
durationProvider = player::getDuration,
|
||||
)
|
||||
|
||||
StatsForNerds(
|
||||
mediaId = currentMediaItem.mediaId,
|
||||
mediaId = currentWindow.mediaItem.mediaId,
|
||||
isDisplayed = isShowingStatsForNerds && error == null,
|
||||
onDismiss = { onShowStatsForNerds(false) }
|
||||
)
|
||||
|
|
|
@ -5,6 +5,9 @@ import androidx.media3.common.MediaItem
|
|||
import androidx.media3.common.Player
|
||||
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>
|
||||
get() = List(windowCount) {
|
||||
getWindow(it, Timeline.Window()).mediaItem
|
||||
|
|
|
@ -2,127 +2,33 @@ package it.vfsfitvnm.vimusic.utils
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.DisposableEffectResult
|
||||
import androidx.compose.runtime.DisposableEffectScope
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.neverEqualPolicy
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Timeline
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
context(DisposableEffectScope)
|
||||
fun Player.listener(listener: Player.Listener): DisposableEffectResult {
|
||||
addListener(listener)
|
||||
return onDispose {
|
||||
removeListener(listener)
|
||||
@Composable
|
||||
inline fun Player.DisposableListener(crossinline listenerProvider: () -> Player.Listener) {
|
||||
DisposableEffect(this) {
|
||||
val listener = listenerProvider()
|
||||
addListener(listener)
|
||||
onDispose { removeListener(listener) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberMediaItemIndex(player: Player): State<Int> {
|
||||
val mediaItemIndexState = remember(player) {
|
||||
mutableStateOf(if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex)
|
||||
fun Player.positionAndDurationState(): State<Pair<Long, Long>> {
|
||||
val state = remember {
|
||||
mutableStateOf(currentPosition to duration)
|
||||
}
|
||||
|
||||
DisposableEffect(player) {
|
||||
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) {
|
||||
LaunchedEffect(this) {
|
||||
var isSeeking = false
|
||||
|
||||
val listener = object : Player.Listener {
|
||||
|
@ -133,7 +39,7 @@ fun rememberPositionAndDuration(player: Player): State<Pair<Long, Long>> {
|
|||
}
|
||||
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
value = player.currentPosition to value.second
|
||||
state.value = currentPosition to state.value.second
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity(
|
||||
|
@ -143,65 +49,29 @@ fun rememberPositionAndDuration(player: Player): State<Pair<Long, Long>> {
|
|||
) {
|
||||
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
||||
isSeeking = true
|
||||
value = player.currentPosition to player.duration
|
||||
state.value = currentPosition to duration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
player.addListener(listener)
|
||||
addListener(listener)
|
||||
|
||||
val pollJob = launch {
|
||||
while (isActive) {
|
||||
delay(500)
|
||||
if (!isSeeking) {
|
||||
value = player.currentPosition to player.duration
|
||||
state.value = currentPosition to duration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
awaitDispose {
|
||||
try {
|
||||
suspendCancellableCoroutine<Nothing> { }
|
||||
} finally {
|
||||
pollJob.cancel()
|
||||
player.removeListener(listener)
|
||||
removeListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue