diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/services/PlayerService.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/services/PlayerService.kt index 05313de..2b3c3b0 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/services/PlayerService.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/services/PlayerService.kt @@ -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 { + 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()) + } } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt index e4261ab..bf15cd7 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt @@ -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) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt index 13a30e3..e59083d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt @@ -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) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt index b03c7fc..861dd55 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt @@ -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) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt index f4b867f..de3ea1e 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt @@ -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) + } } ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/LocalPlaylistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/LocalPlaylistScreen.kt index 37d02c9..d725259 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/LocalPlaylistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/LocalPlaylistScreen.kt @@ -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( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistOrAlbumScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistOrAlbumScreen.kt index 1e35709..8df4a65 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistOrAlbumScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistOrAlbumScreen.kt @@ -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 = { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchResultScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchResultScreen.kt index 11bda51..223408b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchResultScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchResultScreen.kt @@ -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) ) } -} \ No newline at end of file +} + +val NavigationEndpoint.Endpoint.Watch?.asBundle: Bundle + get() = this?.let { + bundleOf( + "videoId" to videoId, + "playlistId" to playlistId, + "playlistSetVideoId" to playlistSetVideoId, + "params" to params, + ) + } ?: Bundle.EMPTY \ No newline at end of file diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/CurrentPlaylistView.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/CurrentPlaylistView.kt index 6e5305a..ee8ef48 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/CurrentPlaylistView.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/CurrentPlaylistView.kt @@ -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 -> {} +// } +// } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubePlayer.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubePlayer.kt index fae5a31..84eb92b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubePlayer.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubePlayer.kt @@ -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.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 { + println("process: ${nextContinuation.valueOrNull}") val token = nextContinuation.valueOrNull nextContinuation = Outcome.Loading + var mediaItems: List? = 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() } } }