Rework YouTube Radio

This commit is contained in:
vfsfitvnm 2022-06-11 23:01:06 +02:00
parent f39276875d
commit a0e42473e6
10 changed files with 222 additions and 219 deletions

View file

@ -32,29 +32,33 @@ import androidx.media3.exoplayer.analytics.AnalyticsListener
import androidx.media3.exoplayer.analytics.PlaybackStats
import androidx.media3.exoplayer.analytics.PlaybackStatsListener
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.session.MediaController
import androidx.media3.session.MediaNotification
import androidx.media3.session.*
import androidx.media3.session.MediaNotification.ActionFactory
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import coil.ImageLoader
import coil.request.ImageRequest
import com.google.common.util.concurrent.ListenableFuture
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.MainActivity
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.utils.RingBuffer
import it.vfsfitvnm.vimusic.utils.YoutubePlayer
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
import it.vfsfitvnm.vimusic.utils.insert
import it.vfsfitvnm.youtubemusic.Outcome
import kotlinx.coroutines.*
import kotlin.math.roundToInt
val StartRadioCommand = SessionCommand("StartRadioCommand", Bundle.EMPTY)
val StartArtistRadioCommand = SessionCommand("StartArtistRadioCommand", Bundle.EMPTY)
val StopRadioCommand = SessionCommand("StopRadioCommand", Bundle.EMPTY)
@ExperimentalAnimationApi
@ExperimentalFoundationApi
class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
MediaNotification.Provider,
PlaybackStatsListener.Callback, Player.Listener,YoutubePlayer.Radio.Listener {
MediaSession.SessionCallback,
PlaybackStatsListener.Callback, Player.Listener {
companion object {
private const val NotificationId = 1001
@ -74,6 +78,8 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
private var lastArtworkUri: Uri? = null
private var lastBitmap: Bitmap? = null
private var radio: YoutubePlayer.Radio? = null
private val coroutineScope = CoroutineScope(Dispatchers.IO) + Job()
override fun onCreate() {
@ -101,11 +107,11 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
mediaSession = MediaSession.Builder(this, player)
.withSessionActivity()
.setSessionCallback(this)
.setMediaItemFiller(this)
.build()
player.addListener(this)
YoutubePlayer.Radio.listener = this
}
override fun onDestroy() {
@ -119,6 +125,49 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
return mediaSession
}
override fun onConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo
): MediaSession.ConnectionResult {
val sessionCommands = SessionCommands.Builder()
.add(StartRadioCommand)
.add(StartArtistRadioCommand)
.add(StopRadioCommand)
.build()
val playerCommands = Player.Commands.Builder().addAllCommands().build()
return MediaSession.ConnectionResult.accept(sessionCommands,playerCommands)
}
override fun onCustomCommand(
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
when (customCommand) {
StartRadioCommand, StartArtistRadioCommand -> {
radio = null
YoutubePlayer.Radio(
videoId = args.getString("videoId"),
playlistId = args.getString("playlistId"),
playlistSetVideoId = args.getString("playlistSetVideoId"),
parameters = args.getString("params"),
).let {
coroutineScope.launch(Dispatchers.Main) {
when (customCommand) {
StartRadioCommand -> mediaSession.player.addMediaItems(it.process().drop(1))
StartArtistRadioCommand -> mediaSession.player.forcePlayFromBeginning(it.process())
}
radio = it
}
}
}
StopRadioCommand -> radio = null
}
return super.onCustomCommand(session, controller, customCommand, args)
}
override fun onPlaybackStatsReady(
eventTime: AnalyticsListener.EventTime,
playbackStats: PlaybackStats
@ -132,18 +181,12 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
}
}
override fun process(play: Boolean) {
if (YoutubePlayer.Radio.isActive) {
coroutineScope.launch {
YoutubePlayer.Radio.process(mediaSession.player, play = play)
}
}
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
if (YoutubePlayer.Radio.isActive) {
coroutineScope.launch {
YoutubePlayer.Radio.process(mediaSession.player)
radio?.let { radio ->
if (mediaSession.player.mediaItemCount - mediaSession.player.currentMediaItemIndex <= 3) {
coroutineScope.launch(Dispatchers.Main) {
mediaSession.player.addMediaItems(radio.process())
}
}
}
}

View file

@ -1,5 +1,6 @@
package it.vfsfitvnm.vimusic.ui.components.themed
import android.os.Bundle
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.with
@ -13,6 +14,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.core.os.bundleOf
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import it.vfsfitvnm.route.RouteHandler
@ -23,6 +25,8 @@ import it.vfsfitvnm.vimusic.internal
import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.SongInPlaylist
import it.vfsfitvnm.vimusic.models.SongWithInfo
import it.vfsfitvnm.vimusic.services.StartRadioCommand
import it.vfsfitvnm.vimusic.services.StopRadioCommand
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.screens.rememberArtistRoute
import it.vfsfitvnm.vimusic.ui.screens.rememberCreatePlaylistRoute
@ -145,13 +149,19 @@ fun NonQueuedMediaItemMenu(
mediaItem = mediaItem,
onDismiss = onDismiss,
onStartRadio = {
val playlistId = mediaItem.mediaMetadata.extras?.getString("playlistId")
YoutubePlayer.Radio.setup(playlistId = playlistId)
player?.mediaController?.forcePlay(mediaItem)
player?.mediaController?.run {
forcePlay(mediaItem)
sendCustomCommand(StartRadioCommand, bundleOf(
"videoId" to mediaItem.mediaId,
"playlistId" to mediaItem.mediaMetadata.extras?.getString("playlistId")
))
}
},
onPlaySingle = {
YoutubePlayer.Radio.reset()
player?.mediaController?.forcePlay(mediaItem)
player?.mediaController?.run {
sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
forcePlay(mediaItem)
}
},
onPlayNext = if (player?.playbackState == Player.STATE_READY) ({
player.mediaController.addNext(mediaItem)

View file

@ -21,6 +21,7 @@ import coil.compose.AsyncImage
import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.services.StartArtistRadioCommand
import it.vfsfitvnm.vimusic.ui.components.ExpandableText
import it.vfsfitvnm.vimusic.ui.components.Message
import it.vfsfitvnm.vimusic.ui.components.OutcomeItem
@ -69,6 +70,7 @@ fun ArtistScreen(
}
host {
val player = LocalYoutubePlayer.current
val density = LocalDensity.current
val colorPalette = LocalColorPalette.current
val typography = LocalTypography.current
@ -137,8 +139,7 @@ fun ArtistScreen(
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
YoutubePlayer.Radio.reset()
artist.shuffleEndpoint?.let(YoutubePlayer.Radio::setup)
player?.mediaController?.sendCustomCommand(StartArtistRadioCommand, artist.shuffleEndpoint.asBundle)
}
.shadow(elevation = 2.dp, shape = CircleShape)
.background(color = colorPalette.elevatedBackground, shape = CircleShape)
@ -152,8 +153,7 @@ fun ArtistScreen(
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
YoutubePlayer.Radio.reset()
artist.radioEndpoint?.let(YoutubePlayer.Radio::setup)
player?.mediaController?.sendCustomCommand(StartArtistRadioCommand, artist.radioEndpoint.asBundle)
}
.shadow(elevation = 2.dp, shape = CircleShape)
.background(color = colorPalette.elevatedBackground, shape = CircleShape)

View file

@ -1,6 +1,7 @@
package it.vfsfitvnm.vimusic.ui.screens
import android.net.Uri
import android.os.Bundle
import androidx.compose.animation.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -36,6 +37,7 @@ import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.SearchQuery
import it.vfsfitvnm.vimusic.models.SongWithInfo
import it.vfsfitvnm.vimusic.services.StopRadioCommand
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.themed.*
@ -331,11 +333,10 @@ fun HomeScreen(
enabled = songCollection.isNotEmpty(),
onClick = {
menuState.hide()
YoutubePlayer.Radio.reset()
player?.mediaController?.forcePlayFromBeginning(
songCollection
.map(SongWithInfo::asMediaItem)
)
player?.mediaController?.let {
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
it.forcePlayFromBeginning(songCollection.map(SongWithInfo::asMediaItem))
}
}
)
@ -345,12 +346,10 @@ fun HomeScreen(
enabled = songCollection.isNotEmpty(),
onClick = {
menuState.hide()
YoutubePlayer.Radio.reset()
player?.mediaController?.forcePlayFromBeginning(
songCollection
.shuffled()
.map(SongWithInfo::asMediaItem)
)
player?.mediaController?.let {
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
it.forcePlayFromBeginning(songCollection.shuffled().map(SongWithInfo::asMediaItem))
}
}
)
@ -385,11 +384,10 @@ fun HomeScreen(
song = song,
thumbnailSize = thumbnailSize,
onClick = {
YoutubePlayer.Radio.reset()
player?.mediaController?.forcePlayAtIndex(
songCollection.map(SongWithInfo::asMediaItem),
index
)
player?.mediaController?.let {
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
it.forcePlayAtIndex(songCollection.map(SongWithInfo::asMediaItem), index)
}
},
menuContent = {
when (preferences.homePageSongCollection) {

View file

@ -1,6 +1,7 @@
package it.vfsfitvnm.vimusic.ui.screens
import android.net.Uri
import android.os.Bundle
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -26,6 +27,7 @@ import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.internal
import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.SongInPlaylist
import it.vfsfitvnm.vimusic.services.StopRadioCommand
import it.vfsfitvnm.vimusic.ui.components.Error
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.Message
@ -238,13 +240,10 @@ fun IntentUriScreen(uri: Uri) {
song = item,
thumbnailSizePx = density.run { 54.dp.roundToPx() },
onClick = {
YoutubePlayer.Radio.reset()
player?.mediaController?.forcePlayAtIndex(
currentItems.value.map(
YouTube.Item.Song::asMediaItem
), index
)
player?.mediaController?.let {
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
it.forcePlayAtIndex(currentItems.value.map(YouTube.Item.Song::asMediaItem), index)
}
}
)
}

View file

@ -1,5 +1,6 @@
package it.vfsfitvnm.vimusic.ui.screens
import android.os.Bundle
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -30,6 +31,7 @@ import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.PlaylistWithSongs
import it.vfsfitvnm.vimusic.models.SongInPlaylist
import it.vfsfitvnm.vimusic.models.SongWithInfo
import it.vfsfitvnm.vimusic.services.StopRadioCommand
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.themed.*
@ -232,12 +234,10 @@ fun LocalPlaylistScreen(
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
YoutubePlayer.Radio.reset()
player?.mediaController?.forcePlayFromBeginning(
playlistWithSongs.songs
.map(SongWithInfo::asMediaItem)
.shuffled()
)
player?.mediaController?.let {
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
it.forcePlayFromBeginning(playlistWithSongs.songs.map(SongWithInfo::asMediaItem).shuffled())
}
}
.shadow(elevation = 2.dp, shape = CircleShape)
.background(
@ -254,12 +254,10 @@ fun LocalPlaylistScreen(
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
YoutubePlayer.Radio.reset()
player?.mediaController?.forcePlayFromBeginning(
playlistWithSongs.songs.map(
SongWithInfo::asMediaItem
)
)
player?.mediaController?.let {
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
it.forcePlayFromBeginning(playlistWithSongs.songs.map(SongWithInfo::asMediaItem))
}
}
.shadow(elevation = 2.dp, shape = CircleShape)
.background(
@ -282,12 +280,10 @@ fun LocalPlaylistScreen(
song = song,
thumbnailSize = thumbnailSize,
onClick = {
YoutubePlayer.Radio.reset()
player?.mediaController?.forcePlayAtIndex(
playlistWithSongs.songs.map(
SongWithInfo::asMediaItem
), index
)
player?.mediaController?.let {
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
it.forcePlayAtIndex(playlistWithSongs.songs.map(SongWithInfo::asMediaItem), index)
}
},
menuContent = {
InPlaylistMediaItemMenu(

View file

@ -1,6 +1,7 @@
package it.vfsfitvnm.vimusic.ui.screens
import android.content.Intent
import android.os.Bundle
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@ -22,21 +23,22 @@ import androidx.compose.ui.unit.dp
import androidx.media3.common.Player
import coil.compose.AsyncImage
import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.internal
import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.SongInPlaylist
import it.vfsfitvnm.vimusic.services.StopRadioCommand
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.OutcomeItem
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.themed.*
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.*
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.ui.components.themed.*
import it.vfsfitvnm.youtubemusic.Outcome
import it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.Dispatchers
@ -276,14 +278,16 @@ fun PlaylistOrAlbumScreen(
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
YoutubePlayer.Radio.reset()
playlistOrAlbum.items
?.shuffled()
?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum)
}?.let { mediaItems ->
player?.mediaController?.forcePlayFromBeginning(mediaItems)
}
player?.mediaController?.let {
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
playlistOrAlbum.items
?.shuffled()
?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum)
}?.let { mediaItems ->
it.forcePlayFromBeginning(mediaItems)
}
}
}
.shadow(elevation = 2.dp, shape = CircleShape)
.background(
@ -300,12 +304,13 @@ fun PlaylistOrAlbumScreen(
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
YoutubePlayer.Radio.reset()
playlistOrAlbum.items?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum)
}?.let { mediaItems ->
player?.mediaController?.forcePlayFromBeginning(mediaItems)
player?.mediaController?.let {
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
playlistOrAlbum.items?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum)
}?.let { mediaItems ->
it.forcePlayFromBeginning(mediaItems)
}
}
}
.shadow(elevation = 2.dp, shape = CircleShape)
@ -326,12 +331,13 @@ fun PlaylistOrAlbumScreen(
authors = (song.authors ?: playlistOrAlbum.authors)?.joinToString("") { it.name },
durationText = song.durationText,
onClick = {
YoutubePlayer.Radio.reset()
playlistOrAlbum.items?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum)
}?.let { mediaItems ->
player?.mediaController?.forcePlayAtIndex(mediaItems, index)
player?.mediaController?.let {
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
playlistOrAlbum.items?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum)
}?.let { mediaItems ->
it.forcePlayAtIndex(mediaItems, index)
}
}
},
startContent = {

View file

@ -1,5 +1,6 @@
package it.vfsfitvnm.vimusic.ui.screens
import android.os.Bundle
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -25,6 +26,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import coil.compose.AsyncImage
import com.valentinilk.shimmer.Shimmer
import com.valentinilk.shimmer.ShimmerBounds
@ -33,6 +35,7 @@ import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.services.StartRadioCommand
import it.vfsfitvnm.vimusic.ui.components.*
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
@ -42,6 +45,7 @@ import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.*
import it.vfsfitvnm.youtubemusic.Outcome
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -214,17 +218,13 @@ fun SearchResultScreen(
is YouTube.Item.Album -> playlistOrAlbumRoute(item.info.endpoint!!.browseId)
is YouTube.Item.Artist -> artistRoute(item.info.endpoint!!.browseId)
is YouTube.Item.Playlist -> playlistOrAlbumRoute(item.info.endpoint!!.browseId)
is YouTube.Item.Song -> {
player?.mediaController?.forcePlay(item.asMediaItem)
item.info.endpoint?.let {
YoutubePlayer.Radio.setup(it, false)
}
is YouTube.Item.Song -> player?.mediaController?.let {
it.forcePlay(item.asMediaItem)
it.sendCustomCommand(StartRadioCommand, item.info.endpoint.asBundle)
}
is YouTube.Item.Video -> {
player?.mediaController?.forcePlay(item.asMediaItem)
item.info.endpoint?.let {
YoutubePlayer.Radio.setup(it, false)
}
is YouTube.Item.Video -> player?.mediaController?.let {
it.forcePlay(item.asMediaItem)
it.sendCustomCommand(StartRadioCommand, item.info.endpoint.asBundle)
}
}
}
@ -572,4 +572,14 @@ fun SmallArtistItem(
.weight(1f)
)
}
}
}
val NavigationEndpoint.Endpoint.Watch?.asBundle: Bundle
get() = this?.let {
bundleOf(
"videoId" to videoId,
"playlistId" to playlistId,
"playlistSetVideoId" to playlistSetVideoId,
"params" to params,
)
} ?: Bundle.EMPTY

View file

@ -157,50 +157,50 @@ fun CurrentPlaylistView(
)
}
if (YoutubePlayer.Radio.isActive && player != null) {
when (val nextContinuation = YoutubePlayer.Radio.nextContinuation) {
is Outcome.Loading, is Outcome.Success<*> -> {
if (nextContinuation is Outcome.Success<*>) {
item {
SideEffect {
coroutineScope.launch {
YoutubePlayer.Radio.process(
player.mediaController,
force = true
)
}
}
}
}
items(count = 3, key = { it }) { index ->
SmallSongItemShimmer(
shimmer = shimmer,
thumbnailSizeDp = 54.dp,
modifier = Modifier
.alpha(1f - index * 0.125f)
.fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 16.dp)
)
}
}
is Outcome.Error -> item {
Error(
error = nextContinuation
)
}
is Outcome.Recovered<*> -> item {
Error(
error = nextContinuation.error,
onRetry = {
coroutineScope.launch {
YoutubePlayer.Radio.process(player.mediaController, force = true)
}
}
)
}
else -> {}
}
}
// if (YoutubePlayer.Radio.isActive && player != null) {
// when (val nextContinuation = YoutubePlayer.Radio.nextContinuation) {
// is Outcome.Loading, is Outcome.Success<*> -> {
// if (nextContinuation is Outcome.Success<*>) {
// item {
// SideEffect {
// coroutineScope.launch {
// YoutubePlayer.Radio.process(
// player.mediaController,
// force = true
// )
// }
// }
// }
// }
//
// items(count = 3, key = { it }) { index ->
// SmallSongItemShimmer(
// shimmer = shimmer,
// thumbnailSizeDp = 54.dp,
// modifier = Modifier
// .alpha(1f - index * 0.125f)
// .fillMaxWidth()
// .padding(vertical = 4.dp, horizontal = 16.dp)
// )
// }
// }
// is Outcome.Error -> item {
// Error(
// error = nextContinuation
// )
// }
// is Outcome.Recovered<*> -> item {
// Error(
// error = nextContinuation.error,
// onRetry = {
// coroutineScope.launch {
// YoutubePlayer.Radio.process(player.mediaController, force = true)
// }
// }
// )
// }
// else -> {}
// }
// }
}
}

View file

@ -1,106 +1,47 @@
package it.vfsfitvnm.vimusic.utils
import androidx.compose.runtime.*
import androidx.media3.common.Player
import androidx.media3.common.MediaItem
import androidx.media3.session.MediaController
import com.google.common.util.concurrent.ListenableFuture
import it.vfsfitvnm.youtubemusic.Outcome
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.withContext
class YoutubePlayer(mediaController: MediaController) : PlayerState(mediaController) {
object Radio {
var isActive by mutableStateOf(false)
var listener: Listener? = null
private var videoId: String? = null
private var playlistId: String? = null
private var playlistSetVideoId: String? = null
private var parameters: String? = null
data class Radio(
private val videoId: String? = null,
private val playlistId: String? = null,
private val playlistSetVideoId: String? = null,
private val parameters: String? = null
) {
var nextContinuation by mutableStateOf<Outcome<String?>>(Outcome.Initial)
fun setup(videoId: String? = null, playlistId: String? = null, playlistSetVideoId: String? = null, parameters: String? = null) {
this.videoId = videoId
this.playlistId = playlistId
this.playlistSetVideoId = playlistSetVideoId
this.parameters = parameters
isActive = true
nextContinuation = Outcome.Initial
}
fun setup(watchEndpoint: NavigationEndpoint.Endpoint.Watch?, play: Boolean = true) {
setup(
videoId = watchEndpoint?.videoId,
playlistId = watchEndpoint?.playlistId,
parameters = watchEndpoint?.params,
playlistSetVideoId = watchEndpoint?.playlistSetVideoId
)
listener?.process(play)
}
suspend fun process(player: Player, force: Boolean = false, play: Boolean = false) {
if (!isActive) return
if (!force && !play) {
val isFirstSong = withContext(Dispatchers.Main) {
player.mediaItemCount == 0 || (player.currentMediaItemIndex == 0 && player.mediaItemCount == 1)
}
val isNearEndSong = withContext(Dispatchers.Main) {
player.mediaItemCount - player.currentMediaItemIndex <= 3
}
if (!isFirstSong && !isNearEndSong) {
return
}
}
suspend fun process(): List<MediaItem> {
println("process: ${nextContinuation.valueOrNull}")
val token = nextContinuation.valueOrNull
nextContinuation = Outcome.Loading
var mediaItems: List<MediaItem>? = null
nextContinuation = withContext(Dispatchers.IO) {
YouTube.next(
videoId = videoId ?: withContext(Dispatchers.Main) {
player.lastMediaItem?.mediaId ?: error("This should not happen")
},
videoId = videoId ?: error("This should not happen"),
playlistId = playlistId,
params = parameters,
playlistSetVideoId = playlistSetVideoId,
continuation = token
)
}.map { nextResult ->
nextResult.items?.map(it.vfsfitvnm.youtubemusic.YouTube.Item.Song::asMediaItem)?.let { mediaItems ->
withContext(Dispatchers.Main) {
if (play) {
player.forcePlayFromBeginning(mediaItems)
} else {
player.addMediaItems(mediaItems.drop(if (token == null) 1 else 0))
}
}
}
mediaItems = nextResult.items?.map(YouTube.Item.Song::asMediaItem)
nextResult.continuation?.takeUnless { token == nextResult.continuation }
}.recoverWith(token)
}
fun reset() {
videoId = null
playlistId = null
playlistSetVideoId = null
parameters = null
isActive = false
nextContinuation = Outcome.Initial
}
interface Listener {
fun process(play: Boolean)
return mediaItems ?: emptyList()
}
}
}