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.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) }
}
}
}

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.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)

View file

@ -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

View file

@ -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()
}
}
)

View file

@ -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
)

View file

@ -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) }
)

View file

@ -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

View file

@ -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
}