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.PlaybackStats
import androidx.media3.exoplayer.analytics.PlaybackStatsListener import androidx.media3.exoplayer.analytics.PlaybackStatsListener
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.session.MediaController import androidx.media3.session.*
import androidx.media3.session.MediaNotification
import androidx.media3.session.MediaNotification.ActionFactory import androidx.media3.session.MediaNotification.ActionFactory
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import com.google.common.util.concurrent.ListenableFuture
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.MainActivity import it.vfsfitvnm.vimusic.MainActivity
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.utils.RingBuffer import it.vfsfitvnm.vimusic.utils.RingBuffer
import it.vfsfitvnm.vimusic.utils.YoutubePlayer import it.vfsfitvnm.vimusic.utils.YoutubePlayer
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
import it.vfsfitvnm.vimusic.utils.insert import it.vfsfitvnm.vimusic.utils.insert
import it.vfsfitvnm.youtubemusic.Outcome import it.vfsfitvnm.youtubemusic.Outcome
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
val StartRadioCommand = SessionCommand("StartRadioCommand", Bundle.EMPTY)
val StartArtistRadioCommand = SessionCommand("StartArtistRadioCommand", Bundle.EMPTY)
val StopRadioCommand = SessionCommand("StopRadioCommand", Bundle.EMPTY)
@ExperimentalAnimationApi @ExperimentalAnimationApi
@ExperimentalFoundationApi @ExperimentalFoundationApi
class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller, class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
MediaNotification.Provider, MediaNotification.Provider,
PlaybackStatsListener.Callback, Player.Listener,YoutubePlayer.Radio.Listener { MediaSession.SessionCallback,
PlaybackStatsListener.Callback, Player.Listener {
companion object { companion object {
private const val NotificationId = 1001 private const val NotificationId = 1001
@@ -74,6 +78,8 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
private var lastArtworkUri: Uri? = null private var lastArtworkUri: Uri? = null
private var lastBitmap: Bitmap? = null private var lastBitmap: Bitmap? = null
private var radio: YoutubePlayer.Radio? = null
private val coroutineScope = CoroutineScope(Dispatchers.IO) + Job() private val coroutineScope = CoroutineScope(Dispatchers.IO) + Job()
override fun onCreate() { override fun onCreate() {
@@ -101,11 +107,11 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
mediaSession = MediaSession.Builder(this, player) mediaSession = MediaSession.Builder(this, player)
.withSessionActivity() .withSessionActivity()
.setSessionCallback(this)
.setMediaItemFiller(this) .setMediaItemFiller(this)
.build() .build()
player.addListener(this) player.addListener(this)
YoutubePlayer.Radio.listener = this
} }
override fun onDestroy() { override fun onDestroy() {
@@ -119,6 +125,49 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
return mediaSession 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( override fun onPlaybackStatsReady(
eventTime: AnalyticsListener.EventTime, eventTime: AnalyticsListener.EventTime,
playbackStats: PlaybackStats 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) { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
if (YoutubePlayer.Radio.isActive) { radio?.let { radio ->
coroutineScope.launch { if (mediaSession.player.mediaItemCount - mediaSession.player.currentMediaItemIndex <= 3) {
YoutubePlayer.Radio.process(mediaSession.player) coroutineScope.launch(Dispatchers.Main) {
mediaSession.player.addMediaItems(radio.process())
}
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
package it.vfsfitvnm.vimusic.ui.screens package it.vfsfitvnm.vimusic.ui.screens
import android.os.Bundle
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background 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.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.valentinilk.shimmer.Shimmer import com.valentinilk.shimmer.Shimmer
import com.valentinilk.shimmer.ShimmerBounds import com.valentinilk.shimmer.ShimmerBounds
@@ -33,6 +35,7 @@ import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.services.StartRadioCommand
import it.vfsfitvnm.vimusic.ui.components.* import it.vfsfitvnm.vimusic.ui.components.*
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder 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.vimusic.utils.*
import it.vfsfitvnm.youtubemusic.Outcome import it.vfsfitvnm.youtubemusic.Outcome
import it.vfsfitvnm.youtubemusic.YouTube import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -214,17 +218,13 @@ fun SearchResultScreen(
is YouTube.Item.Album -> playlistOrAlbumRoute(item.info.endpoint!!.browseId) is YouTube.Item.Album -> playlistOrAlbumRoute(item.info.endpoint!!.browseId)
is YouTube.Item.Artist -> artistRoute(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.Playlist -> playlistOrAlbumRoute(item.info.endpoint!!.browseId)
is YouTube.Item.Song -> { is YouTube.Item.Song -> player?.mediaController?.let {
player?.mediaController?.forcePlay(item.asMediaItem) it.forcePlay(item.asMediaItem)
item.info.endpoint?.let { it.sendCustomCommand(StartRadioCommand, item.info.endpoint.asBundle)
YoutubePlayer.Radio.setup(it, false)
}
} }
is YouTube.Item.Video -> { is YouTube.Item.Video -> player?.mediaController?.let {
player?.mediaController?.forcePlay(item.asMediaItem) it.forcePlay(item.asMediaItem)
item.info.endpoint?.let { it.sendCustomCommand(StartRadioCommand, item.info.endpoint.asBundle)
YoutubePlayer.Radio.setup(it, false)
}
} }
} }
} }
@@ -572,4 +572,14 @@ fun SmallArtistItem(
.weight(1f) .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) { // if (YoutubePlayer.Radio.isActive && player != null) {
when (val nextContinuation = YoutubePlayer.Radio.nextContinuation) { // when (val nextContinuation = YoutubePlayer.Radio.nextContinuation) {
is Outcome.Loading, is Outcome.Success<*> -> { // is Outcome.Loading, is Outcome.Success<*> -> {
if (nextContinuation is Outcome.Success<*>) { // if (nextContinuation is Outcome.Success<*>) {
item { // item {
SideEffect { // SideEffect {
coroutineScope.launch { // coroutineScope.launch {
YoutubePlayer.Radio.process( // YoutubePlayer.Radio.process(
player.mediaController, // player.mediaController,
force = true // force = true
) // )
} // }
} // }
} // }
} // }
//
items(count = 3, key = { it }) { index -> // items(count = 3, key = { it }) { index ->
SmallSongItemShimmer( // SmallSongItemShimmer(
shimmer = shimmer, // shimmer = shimmer,
thumbnailSizeDp = 54.dp, // thumbnailSizeDp = 54.dp,
modifier = Modifier // modifier = Modifier
.alpha(1f - index * 0.125f) // .alpha(1f - index * 0.125f)
.fillMaxWidth() // .fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 16.dp) // .padding(vertical = 4.dp, horizontal = 16.dp)
) // )
} // }
} // }
is Outcome.Error -> item { // is Outcome.Error -> item {
Error( // Error(
error = nextContinuation // error = nextContinuation
) // )
} // }
is Outcome.Recovered<*> -> item { // is Outcome.Recovered<*> -> item {
Error( // Error(
error = nextContinuation.error, // error = nextContinuation.error,
onRetry = { // onRetry = {
coroutineScope.launch { // coroutineScope.launch {
YoutubePlayer.Radio.process(player.mediaController, force = true) // YoutubePlayer.Radio.process(player.mediaController, force = true)
} // }
} // }
) // )
} // }
else -> {} // else -> {}
} // }
} // }
} }
} }

View File

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