From 7e6b5747a2f4118fa14e33a7485b61f7b42ede5a Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Sat, 25 Jun 2022 16:04:52 +0200 Subject: [PATCH] Drop androidx.media3 --- app/build.gradle.kts | 5 +- app/src/main/AndroidManifest.xml | 20 +- .../it/vfsfitvnm/vimusic/MainActivity.kt | 37 +- .../it/vfsfitvnm/vimusic/MainApplication.kt | 15 - .../vimusic/services/PlayerService.kt | 505 ++++++++---------- .../ui/components/themed/MediaItemMenu.kt | 44 +- .../vimusic/ui/screens/ArtistScreen.kt | 33 +- .../vimusic/ui/screens/HomeScreen.kt | 22 +- .../vimusic/ui/screens/IntentUriScreen.kt | 15 +- .../vimusic/ui/screens/LocalPlaylistScreen.kt | 32 +- .../ui/screens/PlaylistOrAlbumScreen.kt | 52 +- .../vimusic/ui/screens/SearchResultScreen.kt | 29 +- .../screens/settings/OtherSettingsScreen.kt | 19 +- .../screens/settings/PlayerSettingsScreen.kt | 59 +- .../vimusic/ui/views/CurrentPlaylistView.kt | 57 +- .../vimusic/ui/views/PlayerBottomSheet.kt | 28 +- .../vfsfitvnm/vimusic/ui/views/PlayerView.kt | 193 ++++--- .../it/vfsfitvnm/vimusic/utils/Context.kt | 23 + .../vimusic/utils/MediaController.kt | 24 - .../it/vfsfitvnm/vimusic/utils/PlayerState.kt | 5 +- .../it/vfsfitvnm/vimusic/utils/TimerJob.kt | 43 ++ .../vfsfitvnm/vimusic/utils/YoutubePlayer.kt | 23 +- .../it/vfsfitvnm/vimusic/utils/utils.kt | 4 + settings.gradle.kts | 5 +- 24 files changed, 626 insertions(+), 666 deletions(-) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Context.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/MediaController.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/TimerJob.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a156bd2..d958c85 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -86,15 +86,12 @@ dependencies { implementation(libs.accompanist.systemuicontroller) implementation(libs.android.media) - implementation(libs.media3.session) - implementation(libs.media3.exoplayer) + implementation(libs.exoplayer) implementation(libs.room) kapt(libs.room.compiler) implementation(projects.youtubeMusic) - implementation(libs.guava.coroutines) - coreLibraryDesugaring(libs.desugaring) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b008519..a947390 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,6 +31,7 @@ + @@ -63,12 +64,23 @@ + android:exported="false" + android:foregroundServiceType="mediaPlayback"> - - + + + + + + + + + diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt index 483f43e..14fb30d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt @@ -1,8 +1,12 @@ package it.vfsfitvnm.vimusic +import android.content.ComponentName +import android.content.Context import android.content.Intent +import android.content.ServiceConnection import android.net.Uri import android.os.Bundle +import android.os.IBinder import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.ExperimentalAnimationApi @@ -27,10 +31,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.dp +import androidx.media3.common.Player import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.valentinilk.shimmer.LocalShimmerTheme import com.valentinilk.shimmer.defaultShimmerTheme import it.vfsfitvnm.vimusic.enums.ColorPaletteMode +import it.vfsfitvnm.vimusic.services.PlayerService import it.vfsfitvnm.vimusic.ui.components.BottomSheetMenu import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState @@ -42,11 +48,34 @@ import it.vfsfitvnm.vimusic.ui.views.PlayerView import it.vfsfitvnm.vimusic.utils.* -@ExperimentalAnimationApi -@ExperimentalFoundationApi class MainActivity : ComponentActivity() { + private val serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + if (service is PlayerService.Binder) { + this@MainActivity.binder = service + } + } + + override fun onServiceDisconnected(name: ComponentName?) { + binder = null + } + } + + private var binder by mutableStateOf(null) private var uri by mutableStateOf(null, neverEqualPolicy()) + override fun onStart() { + super.onStart() + bindService(intent(), serviceConnection, Context.BIND_AUTO_CREATE) + startService(intent()) + } + + override fun onStop() { + unbindService(serviceConnection) + super.onStop() + } + + @OptIn(ExperimentalFoundationApi::class, ExperimentalAnimationApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -112,7 +141,7 @@ class MainActivity : ComponentActivity() { LocalColorPalette provides colorPalette, LocalShimmerTheme provides shimmerTheme, LocalTypography provides rememberTypography(colorPalette.text), - LocalYoutubePlayer provides rememberYoutubePlayer((application as MainApplication).mediaControllerFuture), + LocalPlayerServiceBinder provides binder, LocalMenuState provides rememberMenuState(), LocalHapticFeedback provides rememberHapticFeedback() ) { @@ -151,3 +180,5 @@ class MainActivity : ComponentActivity() { uri = intent?.data } } + +val LocalPlayerServiceBinder = staticCompositionLocalOf { null } \ No newline at end of file diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainApplication.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainApplication.kt index 93ca9f1..84efd6d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainApplication.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainApplication.kt @@ -1,31 +1,16 @@ package it.vfsfitvnm.vimusic import android.app.Application -import android.content.ComponentName -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.media3.session.MediaController -import androidx.media3.session.SessionToken import coil.ImageLoader import coil.ImageLoaderFactory import coil.disk.DiskCache -import com.google.common.util.concurrent.ListenableFuture -import it.vfsfitvnm.vimusic.services.PlayerService import it.vfsfitvnm.vimusic.utils.preferences -@ExperimentalAnimationApi -@ExperimentalFoundationApi class MainApplication : Application(), ImageLoaderFactory { - lateinit var mediaControllerFuture: ListenableFuture - override fun onCreate() { super.onCreate() - DatabaseInitializer() - - val sessionToken = SessionToken(this, ComponentName(this, PlayerService::class.java)) - mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync() } override fun newImageLoader(): ImageLoader { 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 8a3cce3..5171456 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/services/PlayerService.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/services/PlayerService.kt @@ -2,30 +2,28 @@ package it.vfsfitvnm.vimusic.services import android.app.NotificationChannel import android.app.NotificationManager -import android.app.PendingIntent +import android.app.Service +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable import android.net.Uri import android.os.Build -import android.os.Bundle -import android.os.SystemClock +import android.support.v4.media.MediaMetadataCompat +import android.support.v4.media.session.MediaSessionCompat +import android.support.v4.media.session.PlaybackStateCompat import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.core.app.NotificationCompat -import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.toBitmap import androidx.core.net.toUri -import androidx.core.os.bundleOf +import androidx.media.session.MediaButtonReceiver import androidx.media3.common.* -import androidx.media3.common.util.Util import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.datasource.DataSource import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.ResolvingDataSource +import androidx.media3.datasource.cache.Cache import androidx.media3.datasource.cache.CacheDataSource import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor import androidx.media3.datasource.cache.SimpleCache @@ -34,83 +32,63 @@ 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.* -import androidx.media3.session.MediaNotification.ActionFactory import coil.Coil import coil.request.ImageRequest -import com.google.common.collect.ImmutableList -import com.google.common.util.concurrent.Futures -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.internal import it.vfsfitvnm.vimusic.utils.* import it.vfsfitvnm.youtubemusic.Outcome +import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import kotlinx.coroutines.* +import kotlinx.coroutines.flow.StateFlow import kotlin.math.roundToInt import kotlin.system.exitProcess -val StartRadioCommand = SessionCommand("StartRadioCommand", Bundle.EMPTY) -val StartArtistRadioCommand = SessionCommand("StartArtistRadioCommand", Bundle.EMPTY) -val StopRadioCommand = SessionCommand("StopRadioCommand", Bundle.EMPTY) - -val GetCacheSizeCommand = SessionCommand("GetCacheSizeCommand", Bundle.EMPTY) -val GetSongCacheSizeCommand = SessionCommand("GetSongCacheSizeCommand", Bundle.EMPTY) - -val DeleteSongCacheCommand = SessionCommand("DeleteSongCacheCommand", Bundle.EMPTY) - -val SetSkipSilenceCommand = SessionCommand("SetSkipSilenceCommand", Bundle.EMPTY) - -val GetAudioSessionIdCommand = SessionCommand("GetAudioSessionIdCommand", Bundle.EMPTY) - -val SetSleepTimerCommand = SessionCommand("SetSleepTimerCommand", Bundle.EMPTY) -val GetSleepTimerMillisLeftCommand = SessionCommand("GetSleepTimerMillisLeftCommand", Bundle.EMPTY) -val CancelSleepTimerCommand = SessionCommand("CancelSleepTimerCommand", Bundle.EMPTY) - - -@ExperimentalAnimationApi -@ExperimentalFoundationApi -class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotification.Provider, - PlaybackStatsListener.Callback, Player.Listener { - - companion object { - private const val NotificationId = 1001 - private const val NotificationChannelId = "default_channel_id" - - private const val SleepTimerNotificationId = 1002 - private const val SleepTimerNotificationChannelId = "sleep_timer_channel_id" - } - +class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback { + private lateinit var mediaSession: MediaSessionCompat private lateinit var cache: SimpleCache - private lateinit var player: ExoPlayer - private lateinit var mediaSession: MediaSession + private val stateBuilder = PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_PLAY or + PlaybackStateCompat.ACTION_PAUSE or + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or + PlaybackStateCompat.ACTION_SKIP_TO_NEXT or + PlaybackStateCompat.ACTION_PLAY_PAUSE or + PlaybackStateCompat.ACTION_SEEK_TO + ) + + private val metadataBuilder = MediaMetadataCompat.Builder() private lateinit var notificationManager: NotificationManager + private var timerJob: TimerJob? = null + private var notificationThumbnailSize: Int = 0 private var lastArtworkUri: Uri? = null private var lastBitmap: Bitmap? = null private var radio: YoutubePlayer.Radio? = null - private var sleepTimerJob: Job? = null - private var sleepTimerRealtime: Long? = null - private val coroutineScope = CoroutineScope(Dispatchers.IO) + Job() private val songPendingLoudnessDb = mutableMapOf() + override fun onBind(intent: Intent?) = Binder() + override fun onCreate() { super.onCreate() notificationThumbnailSize = (256 * resources.displayMetrics.density).roundToInt() + lastBitmap = resources.getDrawable(R.drawable.disc_placeholder, null) + ?.toBitmap(notificationThumbnailSize, notificationThumbnailSize) + createNotificationChannel() - setMediaNotificationProvider(this) val cacheEvictor = LeastRecentlyUsedCacheEvictor(preferences.exoPlayerDiskCacheMaxSizeBytes) cache = SimpleCache(cacheDir, cacheEvictor, StandaloneDatabaseProvider(this)) @@ -132,153 +110,39 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific player.repeatMode = preferences.repeatMode player.skipSilenceEnabled = preferences.skipSilence player.playWhenReady = true + player.addListener(this) player.addAnalyticsListener(PlaybackStatsListener(false, this)) - mediaSession = MediaSession.Builder(this, player) - .withSessionActivity() - .setCallback(this) - .build() + mediaSession = MediaSessionCompat(this, "PlayerService") + mediaSession.setCallback(SessionCallback(player)) + mediaSession.setPlaybackState(stateBuilder.build()) + mediaSession.isActive = true + } - player.addListener(this) + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + MediaButtonReceiver.handleIntent(mediaSession, intent) + return START_NOT_STICKY + } + + override fun onTaskRemoved(rootIntent: Intent?) { + if (!player.playWhenReady) { + notificationManager.cancel(NotificationId) + stopSelf() + } + + super.onTaskRemoved(rootIntent) } override fun onDestroy() { + player.removeListener(this) + player.stop() player.release() + mediaSession.isActive = false mediaSession.release() cache.release() super.onDestroy() } - override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession { - return mediaSession - } - - override fun onConnect( - session: MediaSession, - controller: MediaSession.ControllerInfo - ): MediaSession.ConnectionResult { - val sessionCommands = SessionCommands.Builder() - .add(StartRadioCommand) - .add(StartArtistRadioCommand) - .add(StopRadioCommand) - .add(GetCacheSizeCommand) - .add(GetSongCacheSizeCommand) - .add(DeleteSongCacheCommand) - .add(SetSkipSilenceCommand) - .add(GetAudioSessionIdCommand) - .add(SetSleepTimerCommand) - .add(GetSleepTimerMillisLeftCommand) - .add(CancelSleepTimerCommand) - .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 -> player.addMediaItems(it.process().drop(1)) - StartArtistRadioCommand -> player.forcePlayFromBeginning(it.process()) - } - radio = it - } - } - } - StopRadioCommand -> radio = null - GetCacheSizeCommand -> { - return Futures.immediateFuture( - SessionResult( - SessionResult.RESULT_SUCCESS, - bundleOf("cacheSize" to cache.cacheSpace) - ) - ) - } - GetSongCacheSizeCommand -> { - return Futures.immediateFuture( - SessionResult( - SessionResult.RESULT_SUCCESS, - bundleOf("cacheSize" to cache.getCachedBytes( - args.getString("videoId") ?: "", - 0, - C.LENGTH_UNSET.toLong() - )) - ) - ) - } - DeleteSongCacheCommand -> { - args.getString("videoId")?.let { videoId -> - cache.removeResource(videoId) - } - } - SetSkipSilenceCommand -> { - player.skipSilenceEnabled = args.getBoolean("skipSilence") - } - GetAudioSessionIdCommand -> { - return Futures.immediateFuture( - SessionResult( - SessionResult.RESULT_SUCCESS, - bundleOf("audioSessionId" to player.audioSessionId) - ) - ) - } - SetSleepTimerCommand -> { - val delayMillis = args.getLong("delayMillis", 2000) - - sleepTimerJob = coroutineScope.launch { - sleepTimerRealtime = SystemClock.elapsedRealtime() + delayMillis - delay(delayMillis) - - withContext(Dispatchers.Main) { - val notification = NotificationCompat.Builder( - this@PlayerService, - SleepTimerNotificationChannelId - ) - .setContentTitle("Sleep timer ended") - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setAutoCancel(true) - .setOnlyAlertOnce(true) - .setShowWhen(true) - .setSmallIcon(R.drawable.app_icon) - .build() - - notificationManager.notify(SleepTimerNotificationId, notification) - } - - exitProcess(0) - } - } - GetSleepTimerMillisLeftCommand -> { - return Futures.immediateFuture(sleepTimerRealtime?.let { - (SessionResult( - SessionResult.RESULT_SUCCESS, - bundleOf("millisLeft" to it - SystemClock.elapsedRealtime()) - )) - } ?: SessionResult(SessionResult.RESULT_ERROR_INVALID_STATE)) - } - CancelSleepTimerCommand -> { - sleepTimerJob?.cancel() - sleepTimerJob = null - sleepTimerRealtime = null - } - } - - return super.onCustomCommand(session, controller, customCommand, args) - } - override fun onPlaybackStatsReady( eventTime: AnalyticsListener.EventTime, playbackStats: PlaybackStats @@ -308,63 +172,62 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific player.volume = player.currentMediaItem?.mediaId?.let { mediaId -> songPendingLoudnessDb.getOrElse(mediaId) { player.currentMediaItem?.mediaMetadata?.extras?.getFloat("loudnessDb") + }?.takeIf { it > 0 }?.let { loudnessDb -> + (1f - (0.01f + loudnessDb / 14)).coerceIn(0.1f, 1f) } - ?.takeIf { it > 0 } - ?.let { loudnessDb -> - (1f - (0.01f + loudnessDb / 15)).coerceIn(0.1f, 1f) - } } ?: 1f } } - override fun onAddMediaItems( - mediaSession: MediaSession, - controller: MediaSession.ControllerInfo, - mediaItems: List - ): ListenableFuture> { - return Futures.immediateFuture( - mediaItems.map { mediaItem -> - mediaItem.buildUpon() - .setUri(mediaItem.mediaId) - .setCustomCacheKey(mediaItem.mediaId) - .build() - } - ) + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + @Player.DiscontinuityReason reason: Int + ) { + stateBuilder + .setState(PlaybackStateCompat.STATE_NONE, newPosition.positionMs, 1f) + .setBufferedPosition(player.bufferedPosition) + + updateNotification() } - override fun createNotification( - session: MediaSession, - customLayout: ImmutableList, - actionFactory: ActionFactory, - onNotificationChangedCallback: MediaNotification.Provider.Callback - ): MediaNotification { - fun invalidate() { - onNotificationChangedCallback.onNotificationChanged( - createNotification( - session, - customLayout, - actionFactory, - onNotificationChangedCallback - ) + override fun onIsPlayingChanged(isPlaying: Boolean) { + stateBuilder + .setState( + if (isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED, + player.currentPosition, + 1f ) - } + .setBufferedPosition(player.bufferedPosition) + updateNotification() + } + + private fun updateNotification() { + if (player.duration != C.TIME_UNSET) { + metadataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, player.duration) + mediaSession.setMetadata(metadataBuilder.build()) + } + mediaSession.setPlaybackState(stateBuilder.build()) + createNotification() + } + + private fun createNotification() { fun NotificationCompat.Builder.addMediaAction( @DrawableRes resId: Int, - @StringRes stringId: Int, - @Player.Command command: Int + description: String, + @PlaybackStateCompat.MediaKeyAction command: Long ): NotificationCompat.Builder { return addAction( - actionFactory.createMediaAction( - mediaSession, - IconCompat.createWithResource(this@PlayerService, resId), - getString(stringId), - command + NotificationCompat.Action( + resId, + description, + MediaButtonReceiver.buildMediaButtonPendingIntent(this@PlayerService, command) ) ) } - val mediaMetadata = mediaSession.player.mediaMetadata + val mediaMetadata = player.mediaMetadata val builder = NotificationCompat.Builder(applicationContext, NotificationChannelId) .setContentTitle(mediaMetadata.title) @@ -375,72 +238,75 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific .setShowWhen(false) .setSmallIcon(R.drawable.app_icon) .setOngoing(false) - .setContentIntent(mediaSession.sessionActivity) - .setDeleteIntent( - actionFactory.createMediaActionPendingIntent( - mediaSession, - Player.COMMAND_STOP.toLong() - ) - ) + .setContentIntent(activityPendingIntent()) + .setDeleteIntent(broadCastPendingIntent()) + .setChannelId(NotificationChannelId) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setStyle( androidx.media.app.NotificationCompat.MediaStyle() .setShowActionsInCompactView(0, 1, 2) - .setMediaSession(mediaSession.sessionCompatToken as android.support.v4.media.session.MediaSessionCompat.Token) + .setMediaSession(mediaSession.sessionToken) ) .addMediaAction( R.drawable.play_skip_back, - R.string.media3_controls_seek_to_previous_description, - Player.COMMAND_SEEK_TO_PREVIOUS + "Skip back", + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS ).addMediaAction( - if (mediaSession.player.playbackState == Player.STATE_ENDED || !mediaSession.player.playWhenReady) R.drawable.play else R.drawable.pause, - if (mediaSession.player.playbackState == Player.STATE_ENDED || !mediaSession.player.playWhenReady) R.string.media3_controls_play_description else R.string.media3_controls_pause_description, - Player.COMMAND_PLAY_PAUSE + if (player.playbackState == Player.STATE_ENDED || !player.playWhenReady) R.drawable.play else R.drawable.pause, + if (player.playbackState == Player.STATE_ENDED || !player.playWhenReady) "Play" else "Pause", + if (player.playbackState == Player.STATE_ENDED || !player.playWhenReady) PlaybackStateCompat.ACTION_PLAY else PlaybackStateCompat.ACTION_PAUSE ) .addMediaAction( R.drawable.play_skip_forward, - R.string.media3_controls_seek_to_next_description, - Player.COMMAND_SEEK_TO_NEXT + "Skip forward", + PlaybackStateCompat.ACTION_SKIP_TO_NEXT ) if (lastArtworkUri != mediaMetadata.artworkUri) { - coroutineScope.launch(Dispatchers.IO) { - lastBitmap = Coil.imageLoader(applicationContext).execute( - ImageRequest.Builder(applicationContext) - .data(mediaMetadata.artworkUri.thumbnail(notificationThumbnailSize)) - .build() - ).drawable?.let { - lastArtworkUri = mediaMetadata.artworkUri - (it as BitmapDrawable).bitmap - } ?: resources.getDrawable(R.drawable.disc_placeholder, null) - ?.toBitmap(notificationThumbnailSize, notificationThumbnailSize) + lastArtworkUri = mediaMetadata.artworkUri - withContext(Dispatchers.Main) { - invalidate() - } - } + Coil.imageLoader(applicationContext).enqueue( + ImageRequest.Builder(applicationContext) + .data(mediaMetadata.artworkUri.thumbnail(notificationThumbnailSize)) + .listener( + onError = { _, _ -> + lastBitmap = resources.getDrawable(R.drawable.disc_placeholder, null) + ?.toBitmap(notificationThumbnailSize, notificationThumbnailSize) + notificationManager.notify(NotificationId, builder.setLargeIcon(lastBitmap).build()) + }, + onSuccess = { _, result -> + lastBitmap = (result.drawable as BitmapDrawable).bitmap + notificationManager.notify(NotificationId, builder.setLargeIcon(lastBitmap).build()) + } + ) + .build() + ) } - return MediaNotification(NotificationId, builder.build()) - } + val notificationCompat = builder.build() + startForeground(NotificationId, notificationCompat) - override fun handleCustomCommand( - session: MediaSession, - action: String, - extras: Bundle - ): Boolean = false + if (player.playbackState == Player.STATE_ENDED || !player.playWhenReady) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + stopForeground(STOP_FOREGROUND_DETACH) + } else { + stopForeground(false) + } + notificationManager.notify(NotificationId, notificationCompat) + } + } private fun createNotificationChannel() { notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - if (Util.SDK_INT < 26) return + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return with(notificationManager) { if (getNotificationChannel(NotificationChannelId) == null) { createNotificationChannel( NotificationChannel( NotificationChannelId, - getString(R.string.default_notification_channel_name), + "Now playing", NotificationManager.IMPORTANCE_LOW ) ) @@ -504,10 +370,12 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific if (mediaItem?.mediaId == videoId) { Database.internal.queryExecutor.execute { - Database.update(Database.insert(mediaItem).copy( - loudnessDb = loudnessDb, - contentLength = format.contentLength - )) + Database.update( + Database.insert(mediaItem).copy( + loudnessDb = loudnessDb, + contentLength = format.contentLength + ) + ) } } @@ -553,14 +421,99 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific } } - private fun MediaSession.Builder.withSessionActivity(): MediaSession.Builder { - return setSessionActivity( - PendingIntent.getActivity( - this@PlayerService, - 0, - Intent(this@PlayerService, MainActivity::class.java), - if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0 + inner class Binder : android.os.Binder() { + val player: ExoPlayer + get() = this@PlayerService.player + + val cache: Cache + get() = this@PlayerService.cache + + val sleepTimerMillisLeft: StateFlow? + get() = timerJob?.millisLeft + + fun startSleepTimer(delayMillis: Long) { + timerJob?.cancel() + + timerJob = coroutineScope.timer(delayMillis) { + val notification = NotificationCompat + .Builder(this@PlayerService, SleepTimerNotificationChannelId) + .setContentTitle("Sleep timer ended") + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + .setOnlyAlertOnce(true) + .setShowWhen(true) + .setSmallIcon(R.drawable.app_icon) + .build() + + notificationManager.notify(SleepTimerNotificationId, notification) + + exitProcess(0) + } + } + + fun cancelSleepTimer() { + timerJob?.cancel() + timerJob = null + } + + fun startRadio( + endpoint: NavigationEndpoint.Endpoint.Watch?, + ) { + startRadio( + videoId = endpoint?.videoId, + playlistId = endpoint?.playlistId, + playlistSetVideoId = endpoint?.playlistSetVideoId, + parameters = endpoint?.params, + justAdd = false ) - ) + } + + fun startRadio( + videoId: String?, + playlistId: String? = null, + playlistSetVideoId: String? = null, + parameters: String? = null, + justAdd: Boolean = true + ) { + radio = null + YoutubePlayer.Radio( + videoId, playlistId, playlistSetVideoId, parameters + ).let { + coroutineScope.launch(Dispatchers.Main) { + if (justAdd) { + player.addMediaItems(it.process().drop(1)) + } else { + player.forcePlayFromBeginning(it.process()) + } + radio = it + } + } + } + + fun stopRadio() { + radio = null + } + } + + private class SessionCallback(private val player: Player) : MediaSessionCompat.Callback() { + override fun onPlay() = player.play() + override fun onPause() = player.pause() + override fun onSkipToPrevious() = player.seekToPrevious() + override fun onSkipToNext() = player.seekToNext() + override fun onSeekTo(pos: Long) = player.seekTo(pos) + } + + class StopServiceBroadcastReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + context.stopService(context.intent()) + } + } + + companion object { + private const val NotificationId = 1001 + private const val NotificationChannelId = "default_channel_id" + + private const val SleepTimerNotificationId = 1002 + private const val SleepTimerNotificationChannelId = "sleep_timer_channel_id" } } 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 797d8d1..4cc292c 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 @@ -20,14 +20,12 @@ import androidx.media3.common.Player import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.route.empty import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder 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.models.SongWithInfo -import it.vfsfitvnm.vimusic.services.DeleteSongCacheCommand -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 @@ -66,7 +64,7 @@ fun InHistoryMediaItemMenu( // https://issuetracker.google.com/issues/226410236 onDismiss: () -> Unit = LocalMenuState.current.let { it::hide } ) { - val mediaController = LocalYoutubePlayer.current?.mediaController + val binder = LocalPlayerServiceBinder.current val coroutineScope = rememberCoroutineScope() @@ -82,7 +80,7 @@ fun InHistoryMediaItemMenu( }, onConfirm = { onDismiss() - mediaController?.sendCustomCommand(DeleteSongCacheCommand, bundleOf("videoId" to song.song.id)) + binder?.cache?.removeResource(song.song.id) coroutineScope.launch(Dispatchers.IO) { Database.delete(song.song) } @@ -147,32 +145,24 @@ fun NonQueuedMediaItemMenu( onDeleteFromDatabase: (() -> Unit)? = null, onRemoveFromFavorites: (() -> Unit)? = null, ) { - val player = LocalYoutubePlayer.current + val binder = LocalPlayerServiceBinder.current BaseMediaItemMenu( mediaItem = mediaItem, onDismiss = onDismiss, onStartRadio = { - player?.mediaController?.run { - forcePlay(mediaItem) - sendCustomCommand(StartRadioCommand, bundleOf( - "videoId" to mediaItem.mediaId, - "playlistId" to mediaItem.mediaMetadata.extras?.getString("playlistId") - )) - } + binder?.player?.forcePlay(mediaItem) + binder?.startRadio(videoId = mediaItem.mediaId, playlistId = mediaItem.mediaMetadata.extras?.getString("playlistId")) }, onPlaySingle = { - player?.mediaController?.run { - sendCustomCommand(StopRadioCommand, Bundle.EMPTY) - forcePlay(mediaItem) - } + binder?.player?.forcePlay(mediaItem) + }, + onPlayNext = { + binder?.player?.addNext(mediaItem) + }, + onEnqueue = { + binder?.player?.enqueue(mediaItem) }, - onPlayNext = if (player?.playbackState == Player.STATE_READY) ({ - player.mediaController.addNext(mediaItem) - }) else null, - onEnqueue = if (player?.playbackState == Player.STATE_READY) ({ - player.mediaController.enqueue(mediaItem) - }) else null, onRemoveFromPlaylist = onRemoveFromPlaylist, onDeleteFromDatabase = onDeleteFromDatabase, onRemoveFromFavorites = onRemoveFromFavorites, @@ -190,14 +180,14 @@ fun QueuedMediaItemMenu( onDismiss: () -> Unit = LocalMenuState.current.let { it::hide }, onGlobalRouteEmitted: (() -> Unit)? = null ) { - val player = LocalYoutubePlayer.current + val player = LocalPlayerServiceBinder.current?.player BaseMediaItemMenu( mediaItem = mediaItem, onDismiss = onDismiss, - onRemoveFromQueue = if (player?.mediaItemIndex != indexInQueue) ({ - player?.mediaController?.removeMediaItem(indexInQueue) - }) else null, + onRemoveFromQueue = { + player?.removeMediaItem(indexInQueue) + }, onGlobalRouteEmitted = onGlobalRouteEmitted, modifier = modifier ) 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 dfe31d6..d162406 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 @@ -1,6 +1,5 @@ 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 @@ -27,10 +26,9 @@ import coil.compose.AsyncImage import com.valentinilk.shimmer.shimmer import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.SongWithInfo -import it.vfsfitvnm.vimusic.services.StartArtistRadioCommand -import it.vfsfitvnm.vimusic.services.StopRadioCommand import it.vfsfitvnm.vimusic.ui.components.OutcomeItem import it.vfsfitvnm.vimusic.ui.components.TopAppBar import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu @@ -79,7 +77,8 @@ fun ArtistScreen( } host { - val player = LocalYoutubePlayer.current + val binder = LocalPlayerServiceBinder.current + val density = LocalDensity.current val colorPalette = LocalColorPalette.current val typography = LocalTypography.current @@ -160,10 +159,7 @@ fun ArtistScreen( colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable { - player?.mediaController?.sendCustomCommand( - StartArtistRadioCommand, - artist.shuffleEndpoint.asBundle - ) + binder?.startRadio(artist.shuffleEndpoint) } .shadow(elevation = 2.dp, shape = CircleShape) .background( @@ -180,10 +176,7 @@ fun ArtistScreen( colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable { - player?.mediaController?.sendCustomCommand( - StartArtistRadioCommand, - artist.radioEndpoint.asBundle - ) + binder?.startRadio(artist.radioEndpoint) } .shadow(elevation = 2.dp, shape = CircleShape) .background( @@ -224,14 +217,8 @@ fun ArtistScreen( colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable(enabled = songs.isNotEmpty()) { - player?.mediaController?.let { - it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY) - it.forcePlayFromBeginning( - songs - .shuffled() - .map(SongWithInfo::asMediaItem) - ) - } + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning(songs.shuffled().map(SongWithInfo::asMediaItem)) } .padding(horizontal = 8.dp, vertical = 8.dp) .size(20.dp) @@ -248,10 +235,8 @@ fun ArtistScreen( song = song, thumbnailSize = songThumbnailSizePx, onClick = { - player?.mediaController?.let { - it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY) - it.forcePlayAtIndex(songs.map(SongWithInfo::asMediaItem), index) - } + binder?.stopRadio() + binder?.player?.forcePlayAtIndex(songs.map(SongWithInfo::asMediaItem), index) }, menuContent = { InHistoryMediaItemMenu(song = song) 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 6f89308..97fbf98 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,7 +1,6 @@ package it.vfsfitvnm.vimusic.ui.screens import android.net.Uri -import android.os.Bundle import androidx.compose.animation.* import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image @@ -32,15 +31,18 @@ import androidx.compose.ui.zIndex import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.route.fastFade import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.enums.SongCollection 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.TopAppBar -import it.vfsfitvnm.vimusic.ui.components.themed.* +import it.vfsfitvnm.vimusic.ui.components.themed.InFavoritesMediaItemMenu +import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu +import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu +import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalTypography import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem @@ -144,7 +146,7 @@ fun HomeScreen() { } host { - val player = LocalYoutubePlayer.current + val binder = LocalPlayerServiceBinder.current val density = LocalDensity.current val thumbnailSize = remember { @@ -357,10 +359,8 @@ fun HomeScreen() { colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable(enabled = songCollection.isNotEmpty()) { - player?.mediaController?.let { - it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY) - it.forcePlayFromBeginning(songCollection.shuffled().map(SongWithInfo::asMediaItem)) - } + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning(songCollection.shuffled().map(SongWithInfo::asMediaItem)) } .padding(horizontal = 8.dp, vertical = 8.dp) .size(20.dp) @@ -379,10 +379,8 @@ fun HomeScreen() { song = song, thumbnailSize = thumbnailSize, onClick = { - player?.mediaController?.let { - it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY) - it.forcePlayAtIndex(songCollection.map(SongWithInfo::asMediaItem), index) - } + binder?.stopRadio() + binder?.player?.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 bc3847e..44e3080 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,7 +1,6 @@ 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 @@ -18,16 +17,15 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import androidx.media3.common.Player import com.valentinilk.shimmer.ShimmerBounds import com.valentinilk.shimmer.rememberShimmer import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder 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 @@ -70,7 +68,7 @@ fun IntentUriScreen(uri: Uri) { val menuState = LocalMenuState.current val colorPalette = LocalColorPalette.current val density = LocalDensity.current - val player = LocalYoutubePlayer.current + val binder = LocalPlayerServiceBinder.current val coroutineScope = rememberCoroutineScope() val shimmer = rememberShimmer(shimmerBounds = ShimmerBounds.Window) @@ -164,14 +162,13 @@ fun IntentUriScreen(uri: Uri) { MenuEntry( icon = R.drawable.time, text = "Enqueue", - enabled = player?.playbackState == Player.STATE_READY, onClick = { menuState.hide() items.valueOrNull ?.map(YouTube.Item.Song::asMediaItem) ?.let { mediaItems -> - player?.mediaController?.enqueue( + binder?.player?.enqueue( mediaItems ) } @@ -238,10 +235,8 @@ fun IntentUriScreen(uri: Uri) { song = item, thumbnailSizePx = density.run { 54.dp.roundToPx() }, onClick = { - player?.mediaController?.let { - it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY) - it.forcePlayAtIndex(currentItems.value.map(YouTube.Item.Song::asMediaItem), index) - } + binder?.stopRadio() + binder?.player?.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 d725259..37b2608 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,6 +1,5 @@ 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 @@ -22,16 +21,15 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import androidx.media3.common.Player import it.vfsfitvnm.reordering.rememberReorderingState import it.vfsfitvnm.reordering.verticalDragAfterLongPressToReorder import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder 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.* @@ -76,7 +74,7 @@ fun LocalPlaylistScreen( val hapticFeedback = LocalHapticFeedback.current val menuState = LocalMenuState.current - val player = LocalYoutubePlayer.current + val binder = LocalPlayerServiceBinder.current val colorPalette = LocalColorPalette.current val typography = LocalTypography.current @@ -163,14 +161,10 @@ fun LocalPlaylistScreen( MenuEntry( icon = R.drawable.time, text = "Enqueue", - enabled = playlistWithSongs.songs.isNotEmpty() && player?.playbackState == Player.STATE_READY, + enabled = playlistWithSongs.songs.isNotEmpty(), onClick = { menuState.hide() - player?.mediaController?.enqueue( - playlistWithSongs.songs.map( - SongWithInfo::asMediaItem - ) - ) + binder?.player?.enqueue(playlistWithSongs.songs.map(SongWithInfo::asMediaItem)) } ) @@ -234,10 +228,8 @@ fun LocalPlaylistScreen( colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable { - player?.mediaController?.let { - it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY) - it.forcePlayFromBeginning(playlistWithSongs.songs.map(SongWithInfo::asMediaItem).shuffled()) - } + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning(playlistWithSongs.songs.map(SongWithInfo::asMediaItem).shuffled()) } .shadow(elevation = 2.dp, shape = CircleShape) .background( @@ -254,10 +246,8 @@ fun LocalPlaylistScreen( colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable { - player?.mediaController?.let { - it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY) - it.forcePlayFromBeginning(playlistWithSongs.songs.map(SongWithInfo::asMediaItem)) - } + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning(playlistWithSongs.songs.map(SongWithInfo::asMediaItem)) } .shadow(elevation = 2.dp, shape = CircleShape) .background( @@ -280,10 +270,8 @@ fun LocalPlaylistScreen( song = song, thumbnailSize = thumbnailSize, onClick = { - player?.mediaController?.let { - it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY) - it.forcePlayAtIndex(playlistWithSongs.songs.map(SongWithInfo::asMediaItem), index) - } + binder?.stopRadio() + binder?.player?.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 a60fc72..6dc75a9 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,7 +1,6 @@ package it.vfsfitvnm.vimusic.ui.screens import android.content.Intent -import android.os.Bundle import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -25,17 +24,16 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow 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.LocalPlayerServiceBinder 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 @@ -87,7 +85,8 @@ fun PlaylistOrAlbumScreen( host { val context = LocalContext.current val density = LocalDensity.current - val player = LocalYoutubePlayer.current + val binder = LocalPlayerServiceBinder.current + val colorPalette = LocalColorPalette.current val typography = LocalTypography.current val menuState = LocalMenuState.current @@ -142,7 +141,6 @@ fun PlaylistOrAlbumScreen( MenuEntry( icon = R.drawable.time, text = "Enqueue", - enabled = player?.playbackState == Player.STATE_READY, onClick = { menuState.hide() playlistOrAlbum.valueOrNull?.let { album -> @@ -151,7 +149,7 @@ fun PlaylistOrAlbumScreen( song.toMediaItem(browseId, album) } ?.let { mediaItems -> - player?.mediaController?.enqueue( + binder?.player?.enqueue( mediaItems ) } @@ -282,16 +280,14 @@ fun PlaylistOrAlbumScreen( colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable { - player?.mediaController?.let { - it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY) - playlistOrAlbum.items - ?.shuffled() - ?.mapNotNull { song -> - song.toMediaItem(browseId, playlistOrAlbum) - }?.let { mediaItems -> - it.forcePlayFromBeginning(mediaItems) - } - } + binder?.stopRadio() + playlistOrAlbum.items + ?.shuffled() + ?.mapNotNull { song -> + song.toMediaItem(browseId, playlistOrAlbum) + }?.let { mediaItems -> + binder?.player?.forcePlayFromBeginning(mediaItems) + } } .shadow(elevation = 2.dp, shape = CircleShape) .background( @@ -308,13 +304,11 @@ fun PlaylistOrAlbumScreen( colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable { - player?.mediaController?.let { - it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY) - playlistOrAlbum.items?.mapNotNull { song -> - song.toMediaItem(browseId, playlistOrAlbum) - }?.let { mediaItems -> - it.forcePlayFromBeginning(mediaItems) - } + binder?.stopRadio() + playlistOrAlbum.items?.mapNotNull { song -> + song.toMediaItem(browseId, playlistOrAlbum) + }?.let { mediaItems -> + binder?.player?.forcePlayFromBeginning(mediaItems) } } .shadow(elevation = 2.dp, shape = CircleShape) @@ -340,13 +334,11 @@ fun PlaylistOrAlbumScreen( authors = (song.authors ?: playlistOrAlbum.valueOrNull?.authors)?.joinToString("") { it.name }, durationText = song.durationText, onClick = { - player?.mediaController?.let { - it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY) - playlistOrAlbum.valueOrNull?.items?.mapNotNull { song -> - song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!) - }?.let { mediaItems -> - it.forcePlayAtIndex(mediaItems, index) - } + binder?.stopRadio() + playlistOrAlbum.valueOrNull?.items?.mapNotNull { song -> + song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!) + }?.let { mediaItems -> + binder?.player?.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 deda0fa..4ff4b86 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,6 +1,5 @@ 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 @@ -26,16 +25,15 @@ 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 import com.valentinilk.shimmer.rememberShimmer import com.valentinilk.shimmer.shimmer import it.vfsfitvnm.route.RouteHandler +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder 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 @@ -45,7 +43,6 @@ 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 @@ -60,7 +57,7 @@ fun SearchResultScreen( val colorPalette = LocalColorPalette.current val typography = LocalTypography.current val preferences = LocalPreferences.current - val player = LocalYoutubePlayer.current + val binder = LocalPlayerServiceBinder.current val lazyListState = rememberLazyListState() @@ -218,13 +215,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?.let { - it.forcePlay(item.asMediaItem) - it.sendCustomCommand(StartRadioCommand, item.info.endpoint.asBundle) + is YouTube.Item.Song -> { + binder?.player?.forcePlay(item.asMediaItem) + binder?.startRadio(item.info.endpoint) } - is YouTube.Item.Video -> player?.mediaController?.let { - it.forcePlay(item.asMediaItem) - it.sendCustomCommand(StartRadioCommand, item.info.endpoint.asBundle) + is YouTube.Item.Video -> { + binder?.player?.forcePlay(item.asMediaItem) + binder?.startRadio(item.info.endpoint) } } } @@ -573,13 +570,3 @@ fun SmallArtistItem( ) } } - -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/screens/settings/OtherSettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsScreen.kt index 370e253..73cdc63 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsScreen.kt @@ -1,6 +1,5 @@ package it.vfsfitvnm.vimusic.ui.screens.settings -import android.os.Bundle import android.text.format.Formatter import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.* @@ -16,19 +15,17 @@ import androidx.compose.ui.unit.dp import coil.Coil import coil.annotation.ExperimentalCoilApi import it.vfsfitvnm.route.RouteHandler +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.services.GetCacheSizeCommand import it.vfsfitvnm.vimusic.ui.components.SeekBar import it.vfsfitvnm.vimusic.ui.components.TopAppBar import it.vfsfitvnm.vimusic.ui.screens.* import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalTypography import it.vfsfitvnm.vimusic.utils.LocalPreferences -import it.vfsfitvnm.vimusic.utils.LocalYoutubePlayer import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.semiBold import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.guava.await import kotlinx.coroutines.launch @OptIn(ExperimentalCoilApi::class) @@ -58,9 +55,7 @@ fun OtherSettingsScreen() { val colorPalette = LocalColorPalette.current val typography = LocalTypography.current val preferences = LocalPreferences.current - val mediaController = LocalYoutubePlayer.current?.mediaController - - val coilDiskCache = Coil.imageLoader(context).diskCache + val binder = LocalPlayerServiceBinder.current val coroutineScope = rememberCoroutineScope() @@ -97,7 +92,7 @@ fun OtherSettingsScreen() { ) } - coilDiskCache?.let { diskCache -> + Coil.imageLoader(context).diskCache?.let { diskCache -> var diskCacheSize by remember(diskCache) { mutableStateOf(diskCache.size) } @@ -168,9 +163,11 @@ fun OtherSettingsScreen() { ) } - mediaController?.let { mediaController -> - val diskCacheSize by produceState(initialValue = 0L) { - value = mediaController.sendCustomCommand(GetCacheSizeCommand, Bundle.EMPTY).await().extras.getLong("cacheSize") + binder?.cache?.let { cache -> + val diskCacheSize by remember { + derivedStateOf { + cache.cacheSpace + } } var scrubbingDiskCacheMaxSize by remember { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsScreen.kt index 1b0445b..4be5f9e 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsScreen.kt @@ -2,7 +2,6 @@ package it.vfsfitvnm.vimusic.ui.screens.settings import android.content.Intent import android.media.audiofx.AudioEffect -import android.os.Bundle import android.text.format.DateUtils import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -18,12 +17,9 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import androidx.core.os.bundleOf -import androidx.media3.common.C -import androidx.media3.session.SessionResult import it.vfsfitvnm.route.RouteHandler +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.services.* import it.vfsfitvnm.vimusic.ui.components.ChunkyButton import it.vfsfitvnm.vimusic.ui.components.Pager import it.vfsfitvnm.vimusic.ui.components.TopAppBar @@ -32,10 +28,10 @@ import it.vfsfitvnm.vimusic.ui.components.themed.DefaultDialog import it.vfsfitvnm.vimusic.ui.screens.* import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalTypography -import it.vfsfitvnm.vimusic.utils.* -import kotlinx.coroutines.delay -import kotlinx.coroutines.guava.await -import kotlinx.coroutines.isActive +import it.vfsfitvnm.vimusic.utils.LocalPreferences +import it.vfsfitvnm.vimusic.utils.color +import it.vfsfitvnm.vimusic.utils.semiBold +import kotlinx.coroutines.flow.flowOf @ExperimentalAnimationApi @@ -64,39 +60,23 @@ fun PlayerSettingsScreen() { val colorPalette = LocalColorPalette.current val typography = LocalTypography.current val preferences = LocalPreferences.current - val mediaController = LocalYoutubePlayer.current?.mediaController + val binder = LocalPlayerServiceBinder.current val activityResultLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { } - val audioSessionId by produceState(initialValue = C.AUDIO_SESSION_ID_UNSET, mediaController) { + val audioSessionId = remember(binder) { val hasEqualizer = context.packageManager.resolveActivity( Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL), 0 ) != null - if (hasEqualizer) { - value = - mediaController?.sendCustomCommand(GetAudioSessionIdCommand, Bundle.EMPTY) - ?.await()?.extras?.getInt("audioSessionId", C.AUDIO_SESSION_ID_UNSET) - ?: C.AUDIO_SESSION_ID_UNSET - } + if (hasEqualizer) binder?.player?.audioSessionId else null } - var sleepTimerMillisLeft by remember { - mutableStateOf(null) - } - - LaunchedEffect(mediaController) { - while (isActive) { - sleepTimerMillisLeft = - mediaController?.syncCommand(GetSleepTimerMillisLeftCommand) - ?.takeIf { it.resultCode == SessionResult.RESULT_SUCCESS } - ?.extras?.getLong("millisLeft") - delay(1000) - } - } + val sleepTimerMillisLeft by (binder?.sleepTimerMillisLeft + ?: flowOf(null)).collectAsState(initial = null) var isShowingSleepTimerDialog by remember { mutableStateOf(false) @@ -112,8 +92,7 @@ fun PlayerSettingsScreen() { isShowingSleepTimerDialog = false }, onConfirm = { - mediaController?.syncCommand(CancelSleepTimerCommand) - sleepTimerMillisLeft = null + binder?.cancelSleepTimer() } ) } else { @@ -199,14 +178,7 @@ fun PlayerSettingsScreen() { shape = RoundedCornerShape(36.dp), isEnabled = hours > 0 || minutes > 0, onClick = { - mediaController?.syncCommand( - SetSleepTimerCommand, - bundleOf("delayMillis" to (hours * 60 + minutes * 15) * 60 * 1000L) - ) - sleepTimerMillisLeft = - mediaController?.syncCommand(GetSleepTimerMillisLeftCommand)?.extras?.getLong( - "millisLeft" - ) + binder?.startSleepTimer((hours * 60 + minutes * 15) * 60 * 1000L) isShowingSleepTimerDialog = false } ) @@ -253,10 +225,7 @@ fun PlayerSettingsScreen() { text = "Skip silent parts during playback", isChecked = preferences.skipSilence, onCheckedChange = { - mediaController?.sendCustomCommand( - SetSkipSilenceCommand, - bundleOf("skipSilence" to it) - ) + binder?.player?.skipSilenceEnabled = it preferences.skipSilence = it } ) @@ -284,7 +253,7 @@ fun PlayerSettingsScreen() { } ) }, - isEnabled = audioSessionId != C.AUDIO_SESSION_ID_UNSET && audioSessionId != AudioEffect.ERROR_BAD_VALUE + isEnabled = audioSessionId != null ) SettingsEntry( 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 ee8ef48..e8fea14 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 @@ -6,14 +6,15 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.hapticfeedback.HapticFeedbackType @@ -25,33 +26,29 @@ import androidx.compose.ui.unit.dp import androidx.media3.common.Player import com.valentinilk.shimmer.ShimmerBounds import com.valentinilk.shimmer.rememberShimmer -import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.ui.components.BottomSheetState -import it.vfsfitvnm.vimusic.ui.components.Error -import it.vfsfitvnm.vimusic.ui.components.MusicBars -import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu -import it.vfsfitvnm.vimusic.ui.screens.SmallSongItemShimmer -import it.vfsfitvnm.vimusic.ui.styling.LightColorPalette -import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette -import it.vfsfitvnm.vimusic.utils.LocalYoutubePlayer -import it.vfsfitvnm.vimusic.utils.YoutubePlayer import it.vfsfitvnm.reordering.rememberReorderingState import it.vfsfitvnm.reordering.verticalDragAfterLongPressToReorder +import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness -import it.vfsfitvnm.youtubemusic.Outcome -import kotlinx.coroutines.launch +import it.vfsfitvnm.vimusic.ui.components.BottomSheetState +import it.vfsfitvnm.vimusic.ui.components.MusicBars +import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu +import it.vfsfitvnm.vimusic.ui.styling.LightColorPalette +import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette +import it.vfsfitvnm.vimusic.utils.PlayerState @ExperimentalAnimationApi @Composable fun CurrentPlaylistView( + player: Player?, + playerState: PlayerState?, layoutState: BottomSheetState, onGlobalRouteEmitted: () -> Unit, modifier: Modifier = Modifier, ) { val hapticFeedback = LocalHapticFeedback.current val density = LocalDensity.current - val player = LocalYoutubePlayer.current val colorPalette = LocalColorPalette.current val thumbnailSize = remember { @@ -61,30 +58,26 @@ fun CurrentPlaylistView( } val isPaused by derivedStateOf { - player?.playbackState == Player.STATE_ENDED || player?.playWhenReady == false + playerState?.playbackState == Player.STATE_ENDED || playerState?.playWhenReady == false } - val shimmer = rememberShimmer(shimmerBounds = ShimmerBounds.Window) - - val coroutineScope = rememberCoroutineScope() - val lazyListState = - rememberLazyListState(initialFirstVisibleItemIndex = player?.mediaItemIndex ?: 0) + rememberLazyListState(initialFirstVisibleItemIndex = playerState?.mediaItemIndex ?: 0) - val reorderingState = rememberReorderingState(player?.mediaItems ?: emptyList()) + val reorderingState = rememberReorderingState(playerState?.mediaItems ?: emptyList()) LazyColumn( state = lazyListState, modifier = modifier .nestedScroll(remember { - layoutState.nestedScrollConnection(player?.mediaItemIndex == 0) + layoutState.nestedScrollConnection(playerState?.mediaItemIndex == 0) }) ) { itemsIndexed( - items = player?.mediaItems ?: emptyList() + items = playerState?.mediaItems ?: emptyList() ) { index, mediaItem -> val isPlayingThisMediaItem by derivedStateOf { - player?.mediaItemIndex == index + playerState?.mediaItemIndex == index } SongItem( @@ -93,13 +86,13 @@ fun CurrentPlaylistView( onClick = { if (isPlayingThisMediaItem) { if (isPaused) { - player?.mediaController?.play() + player?.play() } else { - player?.mediaController?.pause() + player?.pause() } } else { - player?.mediaController?.playWhenReady = true - player?.mediaController?.seekToDefaultPosition(index) + player?.playWhenReady = true + player?.seekToDefaultPosition(index) } }, menuContent = { @@ -151,7 +144,7 @@ fun CurrentPlaylistView( ) }, onDragEnd = { reachedIndex -> - player?.mediaController?.moveMediaItem(index, reachedIndex) + player?.moveMediaItem(index, reachedIndex) } ) ) @@ -165,7 +158,7 @@ fun CurrentPlaylistView( // SideEffect { // coroutineScope.launch { // YoutubePlayer.Radio.process( -// player.mediaController, +// playerState.mediaController, // force = true // ) // } @@ -194,7 +187,7 @@ fun CurrentPlaylistView( // error = nextContinuation.error, // onRetry = { // coroutineScope.launch { -// YoutubePlayer.Radio.process(player.mediaController, force = true) +// YoutubePlayer.Radio.process(playerState.mediaController, force = true) // } // } // ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt index c3d6030..82ed358 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.media3.common.Player import it.vfsfitvnm.route.Route import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.route.empty @@ -44,13 +45,13 @@ import kotlinx.coroutines.withContext @ExperimentalAnimationApi @Composable fun PlayerBottomSheet( + player: Player?, + playerState: PlayerState?, layoutState: BottomSheetState, song: Song?, onGlobalRouteEmitted: () -> Unit, modifier: Modifier = Modifier, ) { - val player = LocalYoutubePlayer.current ?: return - val colorPalette = LocalColorPalette.current val typography = LocalTypography.current @@ -60,7 +61,7 @@ fun PlayerBottomSheet( var route by rememberRoute() - var nextOutcome by remember(player.mediaItem!!.mediaId) { + var nextOutcome by remember(playerState?.mediaItem?.mediaId) { mutableStateOf>(Outcome.Initial) } @@ -183,13 +184,19 @@ fun PlayerBottomSheet( coroutineScope.launch(Dispatchers.Main) { lyricsOutcome = Outcome.Loading + val mediaItem = player?.currentMediaItem!! + if (nextOutcome.isEvaluable) { nextOutcome = Outcome.Loading + + + val mediaItemIndex = player.currentMediaItemIndex + nextOutcome = withContext(Dispatchers.IO) { YouTube.next( - player.mediaItem!!.mediaId, - player.mediaItem!!.mediaMetadata.extras?.getString("playlistId"), - player.mediaItemIndex + mediaItem.mediaId, + mediaItem.mediaMetadata.extras?.getString("playlistId"), + mediaItemIndex ) } } @@ -200,7 +207,7 @@ fun PlayerBottomSheet( lyrics ?: "" }.map { lyrics -> withContext(Dispatchers.IO) { - (song ?: player.mediaItem?.let(Database::insert))?.let { + (song ?: mediaItem.let(Database::insert)).let { Database.update(it.copy(lyrics = lyrics)) } } @@ -209,7 +216,7 @@ fun PlayerBottomSheet( } }, onSearchOnline = { - player.mediaMetadata.let { + player?.mediaMetadata?.let { context.startActivity(Intent(Intent.ACTION_WEB_SEARCH).apply { putExtra( SearchManager.QUERY, @@ -219,8 +226,9 @@ fun PlayerBottomSheet( } }, onLyricsUpdate = { lyrics -> + val mediaItem = player?.currentMediaItem coroutineScope.launch(Dispatchers.IO) { - (song ?: player.mediaItem?.let(Database::insert))?.let { + (song ?: mediaItem?.let(Database::insert))?.let { Database.update(it.copy(lyrics = lyrics)) } } @@ -230,6 +238,8 @@ fun PlayerBottomSheet( host { CurrentPlaylistView( + player = player, + playerState = playerState, layoutState = layoutState, onGlobalRouteEmitted = onGlobalRouteEmitted, modifier = Modifier diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt index b58f762..0a8fd5f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt @@ -30,15 +30,16 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.core.os.bundleOf import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player +import androidx.media3.datasource.cache.Cache +import androidx.media3.datasource.cache.CacheSpan import coil.compose.AsyncImage import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness -import it.vfsfitvnm.vimusic.services.GetSongCacheSizeCommand import it.vfsfitvnm.vimusic.ui.components.* import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.styling.BlackColorPalette @@ -67,12 +68,15 @@ fun PlayerView( val typography = LocalTypography.current val density = LocalDensity.current val configuration = LocalConfiguration.current - val player = LocalYoutubePlayer.current + val binder = LocalPlayerServiceBinder.current val context = LocalContext.current + val player = binder?.player + val playerState = rememberYoutubePlayer(player) + val coroutineScope = rememberCoroutineScope() - player?.mediaItem ?: return + playerState?.mediaItem ?: return val smallThumbnailSize = remember { density.run { 64.dp.roundToPx() } @@ -108,7 +112,7 @@ fun PlayerView( y = 1.dp.toPx() ), end = Offset( - x = ((size.width - offset) * player.progress) + offset, + x = ((size.width - offset) * playerState.progress) + offset, y = 1.dp.toPx() ), strokeWidth = 2.dp.toPx() @@ -116,7 +120,7 @@ fun PlayerView( } ) { AsyncImage( - model = player.mediaMetadata.artworkUri.thumbnail(smallThumbnailSize), + model = playerState.mediaMetadata.artworkUri.thumbnail(smallThumbnailSize), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier @@ -129,13 +133,13 @@ fun PlayerView( .weight(1f) ) { BasicText( - text = player.mediaMetadata.title?.toString() ?: "", + text = playerState.mediaMetadata.title?.toString() ?: "", style = typography.xs.semiBold, maxLines = 1, overflow = TextOverflow.Ellipsis, ) BasicText( - text = player.mediaMetadata.artist?.toString() ?: "", + text = playerState.mediaMetadata.artist?.toString() ?: "", style = typography.xs, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -143,16 +147,16 @@ fun PlayerView( } when { - player.playbackState == Player.STATE_ENDED || !player.playWhenReady -> Image( + playerState.playbackState == Player.STATE_ENDED || !playerState.playWhenReady -> Image( painter = painterResource(R.drawable.play), contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable { - if (player.playbackState == Player.STATE_IDLE) { - player.mediaController.prepare() + if (playerState.playbackState == Player.STATE_IDLE) { + player?.prepare() } - player.mediaController.play() + player?.play() } .padding(vertical = 8.dp) .padding(horizontal = 16.dp) @@ -164,7 +168,7 @@ fun PlayerView( colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable { - player.mediaController.pause() + player?.pause() } .padding(vertical = 8.dp) .padding(horizontal = 16.dp) @@ -175,10 +179,11 @@ fun PlayerView( } } ) { - val song by remember(player.mediaItem?.mediaId) { - player.mediaItem?.mediaId?.let(Database::songFlow)?.distinctUntilChanged() ?: flowOf( - null - ) + val song by remember(playerState.mediaItem?.mediaId) { + playerState.mediaItem?.mediaId?.let(Database::songFlow)?.distinctUntilChanged() + ?: flowOf( + null + ) }.collectAsState(initial = null, context = Dispatchers.IO) var isShowingStatsForNerds by rememberSaveable { @@ -192,7 +197,7 @@ fun PlayerView( .padding(bottom = 72.dp) .fillMaxSize() ) { - var scrubbingPosition by remember(player.mediaItemIndex) { + var scrubbingPosition by remember(playerState.mediaItemIndex) { mutableStateOf(null) } @@ -211,8 +216,8 @@ fun PlayerView( .clickable { menuState.display { QueuedMediaItemMenu( - mediaItem = player.mediaItem ?: MediaItem.EMPTY, - indexInQueue = player.mediaItemIndex, + mediaItem = playerState.mediaItem ?: MediaItem.EMPTY, + indexInQueue = playerState.mediaItemIndex, onDismiss = menuState::hide, onGlobalRouteEmitted = layoutState.collapse ) @@ -223,9 +228,9 @@ fun PlayerView( ) } - if (player.error == null) { + if (playerState.error == null) { AnimatedContent( - targetState = player.mediaItemIndex, + targetState = playerState.mediaItemIndex, transitionSpec = { val slideDirection = if (targetState > initialState) AnimatedContentScope.SlideDirection.Left else AnimatedContentScope.SlideDirection.Right @@ -240,7 +245,7 @@ fun PlayerView( .align(Alignment.CenterHorizontally) ) { val artworkUri = remember(it) { - player.mediaController.getMediaItemAt(it).mediaMetadata.artworkUri.thumbnail( + player?.getMediaItemAt(it)?.mediaMetadata?.artworkUri.thumbnail( thumbnailSizePx ) } @@ -273,13 +278,37 @@ fun PlayerView( enter = fadeIn(), exit = fadeOut(), ) { - val cachedPercentage = remember(song?.contentLength) { - song?.contentLength?.let { contentLength -> - player.mediaController.syncCommand( - GetSongCacheSizeCommand, - bundleOf("videoId" to song?.id) - ).extras.getLong("cacheSize").toFloat() / contentLength * 100 - }?.roundToInt() ?: 0 + var cachedBytes by remember(song?.id) { + mutableStateOf(binder?.cache?.getCachedBytes(song?.id ?: "", 0, -1) ?: 0L) + } + + DisposableEffect(song?.id) { + val listener = object : Cache.Listener { + override fun onSpanAdded(cache: Cache, span: CacheSpan) { + cachedBytes += span.length + } + + override fun onSpanRemoved(cache: Cache, span: CacheSpan) { + cachedBytes -= span.length + } + + override fun onSpanTouched( + cache: Cache, + oldSpan: CacheSpan, + newSpan: CacheSpan + ) = Unit + } + + song?.id?.let { key -> + binder?.cache?.addListener(key, listener) + } + + + onDispose { + song?.id?.let { key -> + binder?.cache?.removeListener(key, listener) + } + } } Column( @@ -321,7 +350,7 @@ fun PlayerView( Column { BasicText( - text = "${player.volume.times(100).roundToInt()}%", + text = "${playerState.volume.times(100).roundToInt()}%", style = typography.xs.semiBold.color(BlackColorPalette.text) ) BasicText( @@ -332,12 +361,21 @@ fun PlayerView( ) BasicText( text = song?.contentLength?.let { contentLength -> - Formatter.formatShortFileSize(context, contentLength) + Formatter.formatShortFileSize( + context, + contentLength + ) } ?: "Unknown", style = typography.xs.semiBold.color(BlackColorPalette.text) ) BasicText( - text = "$cachedPercentage%", + text = buildString { + append(Formatter.formatShortFileSize(context, cachedBytes)) + + song?.contentLength?.let { contentLenght -> + append(" (${(cachedBytes.toFloat() / contentLenght * 100).roundToInt()}%)") + } + }, style = typography.xs.semiBold.color(BlackColorPalette.text) ) } @@ -354,16 +392,20 @@ fun PlayerView( onClick = { song?.let { song -> coroutineScope.launch(Dispatchers.IO) { - YouTube.player(song.id).map { body -> - Database.update( - song.copy( - loudnessDb = body.playerConfig?.audioConfig?.loudnessDb?.toFloat(), - contentLength = body.streamingData?.adaptiveFormats?.findLast { format -> - format.itag == 251 || format.itag == 140 - }?.let(PlayerResponse.StreamingData.AdaptiveFormat::contentLength) + YouTube + .player(song.id) + .map { body -> + Database.update( + song.copy( + loudnessDb = body.playerConfig?.audioConfig?.loudnessDb?.toFloat(), + contentLength = body.streamingData?.adaptiveFormats + ?.findLast { format -> + format.itag == 251 || format.itag == 140 + } + ?.let(PlayerResponse.StreamingData.AdaptiveFormat::contentLength) + ) ) - ) - } + } } } } @@ -387,18 +429,18 @@ fun PlayerView( .size(thumbnailSizeDp) ) { Error( - error = Outcome.Error.Unhandled(player.error!!), + error = Outcome.Error.Unhandled(playerState.error!!), onRetry = { - player.mediaController.playWhenReady = true - player.mediaController.prepare() - player.error = null + player?.playWhenReady = true + player?.prepare() + playerState.error = null } ) } } BasicText( - text = player.mediaMetadata.title?.toString() ?: "", + text = playerState.mediaMetadata.title?.toString() ?: "", style = typography.l.bold, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -407,7 +449,7 @@ fun PlayerView( ) BasicText( - text = player.mediaMetadata.extras?.getStringArrayList("artistNames") + text = playerState.mediaMetadata.extras?.getStringArrayList("artistNames") ?.joinToString("") ?: "", style = typography.s.semiBold.secondary, maxLines = 1, @@ -417,24 +459,24 @@ fun PlayerView( ) SeekBar( - value = scrubbingPosition ?: player.currentPosition, + value = scrubbingPosition ?: playerState.currentPosition, minimumValue = 0, - maximumValue = player.duration, + maximumValue = playerState.duration, onDragStart = { scrubbingPosition = it }, onDrag = { delta -> - scrubbingPosition = if (player.duration != C.TIME_UNSET) { - scrubbingPosition?.plus(delta)?.coerceIn(0, player.duration) + scrubbingPosition = if (playerState.duration != C.TIME_UNSET) { + scrubbingPosition?.plus(delta)?.coerceIn(0, playerState.duration) } else { null } }, onDragEnd = { - player.mediaController.seekTo( - scrubbingPosition ?: player.mediaController.currentPosition - ) - player.currentPosition = player.mediaController.currentPosition + scrubbingPosition?.let { scrubbingPosition -> + player?.seekTo(scrubbingPosition) + playerState.currentPosition = scrubbingPosition + } scrubbingPosition = null }, color = colorPalette.text, @@ -456,16 +498,16 @@ fun PlayerView( ) { BasicText( text = DateUtils.formatElapsedTime( - (scrubbingPosition ?: player.currentPosition) / 1000 + (scrubbingPosition ?: playerState.currentPosition) / 1000 ), style = typography.xxs.semiBold, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - if (player.duration != C.TIME_UNSET) { + if (playerState.duration != C.TIME_UNSET) { BasicText( - text = DateUtils.formatElapsedTime(player.duration / 1000), + text = DateUtils.formatElapsedTime(playerState.duration / 1000), style = typography.xxs.semiBold, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -488,7 +530,7 @@ fun PlayerView( modifier = Modifier .clickable { coroutineScope.launch(Dispatchers.IO) { - (song ?: player.mediaItem?.let(Database::insert))?.let { + (song ?: playerState.mediaItem?.let(Database::insert))?.let { Database.update(it.toggleLike()) } } @@ -503,24 +545,24 @@ fun PlayerView( colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable { - player.mediaController.seekToPrevious() + player?.seekToPrevious() } .padding(horizontal = 16.dp) .size(32.dp) ) when { - player.playbackState == Player.STATE_ENDED || !player.playWhenReady -> Image( + playerState.playbackState == Player.STATE_ENDED || !playerState.playWhenReady -> Image( painter = painterResource(R.drawable.play_circle), contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable { - if (player.playbackState == Player.STATE_IDLE) { - player.mediaController.prepare() + if (player?.playbackState == Player.STATE_IDLE) { + player.prepare() } - player.mediaController.play() + player?.play() } .size(64.dp) ) @@ -530,7 +572,7 @@ fun PlayerView( colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable { - player.mediaController.pause() + player?.pause() } .size(64.dp) ) @@ -542,7 +584,7 @@ fun PlayerView( colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable { - player.mediaController.seekToNext() + player?.seekToNext() } .padding(horizontal = 16.dp) .size(32.dp) @@ -551,7 +593,7 @@ fun PlayerView( Image( painter = painterResource( - if (player.repeatMode == Player.REPEAT_MODE_ONE) { + if (playerState.repeatMode == Player.REPEAT_MODE_ONE) { R.drawable.repeat_one } else { R.drawable.repeat @@ -559,7 +601,7 @@ fun PlayerView( ), contentDescription = null, colorFilter = ColorFilter.tint( - if (player.repeatMode == Player.REPEAT_MODE_OFF) { + if (playerState.repeatMode == Player.REPEAT_MODE_OFF) { colorPalette.textDisabled } else { colorPalette.text @@ -567,10 +609,13 @@ fun PlayerView( ), modifier = Modifier .clickable { - player.mediaController.repeatMode = - (player.mediaController.repeatMode + 2) % 3 - - preferences.repeatMode = player.mediaController.repeatMode + player?.repeatMode + ?.plus(2) + ?.mod(3) + ?.let { repeatMode -> + player.repeatMode = repeatMode + preferences.repeatMode = repeatMode + } } .padding(horizontal = 16.dp) .size(28.dp) @@ -579,6 +624,8 @@ fun PlayerView( } PlayerBottomSheet( + player = player, + playerState = playerState, layoutState = rememberBottomSheetState(64.dp, layoutState.upperBound - 128.dp), onGlobalRouteEmitted = layoutState.collapse, song = song, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Context.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Context.kt new file mode 100644 index 0000000..a67b148 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Context.kt @@ -0,0 +1,23 @@ +package it.vfsfitvnm.vimusic.utils + +import android.app.Activity +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build + +inline fun Context.intent(): Intent = + Intent(this@Context, T::class.java) + +inline fun Context.broadCastPendingIntent( + requestCode: Int = 0, + flags: Int = if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0, +): PendingIntent = + PendingIntent.getBroadcast(this, requestCode, intent(), flags) + +inline fun Context.activityPendingIntent( + requestCode: Int = 0, + flags: Int = if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0, +): PendingIntent = + PendingIntent.getActivity(this, requestCode, intent(), flags) \ No newline at end of file diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/MediaController.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/MediaController.kt deleted file mode 100644 index a4ae335..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/MediaController.kt +++ /dev/null @@ -1,24 +0,0 @@ -package it.vfsfitvnm.vimusic.utils - -import android.os.Bundle -import androidx.media3.session.MediaController -import androidx.media3.session.SessionCommand -import androidx.media3.session.SessionResult -import com.google.common.util.concurrent.MoreExecutors -import kotlinx.coroutines.guava.await - - -suspend fun MediaController.send(command: SessionCommand, args: Bundle = Bundle.EMPTY): SessionResult { - return sendCustomCommand(command, args).await() -} - -fun MediaController.command(command: SessionCommand, args: Bundle = Bundle.EMPTY, listener: ((SessionResult) -> Unit)? = null) { - val future = sendCustomCommand(command, args) - listener?.let { - future.addListener({ it(future.get()) }, MoreExecutors.directExecutor()) - } -} - -fun MediaController.syncCommand(command: SessionCommand, args: Bundle = Bundle.EMPTY): SessionResult { - return sendCustomCommand(command, args).get() -} \ No newline at end of file diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/PlayerState.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/PlayerState.kt index e657bfa..bf67ef2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/PlayerState.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/PlayerState.kt @@ -6,10 +6,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.media3.common.* -import androidx.media3.session.MediaController import kotlin.math.absoluteValue -open class PlayerState(val mediaController: MediaController) : Player.Listener { +open class PlayerState(val mediaController: Player) : Player.Listener { private val handler = Handler(Looper.getMainLooper()) var currentPosition by mutableStateOf(mediaController.currentPosition) @@ -52,7 +51,6 @@ open class PlayerState(val mediaController: MediaController) : Player.Listener { init { handler.post(object : Runnable { override fun run() { - duration = mediaController.duration currentPosition = mediaController.currentPosition handler.postDelayed(this, 500) } @@ -64,6 +62,7 @@ open class PlayerState(val mediaController: MediaController) : Player.Listener { } override fun onPlaybackStateChanged(playbackState: Int) { + duration = mediaController.duration this.playbackState = playbackState } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/TimerJob.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/TimerJob.kt new file mode 100644 index 0000000..1dd5110 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/TimerJob.kt @@ -0,0 +1,43 @@ +package it.vfsfitvnm.vimusic.utils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + + +interface TimerJob { + val millisLeft: StateFlow + fun cancel() +} + +fun CoroutineScope.timer(delayMillis: Long, onCompletion: () -> Unit): TimerJob { + val millisLeft = MutableStateFlow(delayMillis) + + val job = launch { + while (isActive && millisLeft.value != null) { + delay(1000) + millisLeft.emit(millisLeft.value?.minus(1000)?.takeIf { it > 0 }) + } + } + + val disposableHandle = job.invokeOnCompletion { + if (it == null) { + onCompletion() + } + } + + return object : TimerJob { + override val millisLeft: StateFlow + get() = millisLeft.asStateFlow() + + override fun cancel() { + millisLeft.value = null + disposableHandle.dispose() + job.cancel() + } + } +} 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 8249c13..b97b271 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubePlayer.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubePlayer.kt @@ -2,15 +2,13 @@ package it.vfsfitvnm.vimusic.utils import androidx.compose.runtime.* import androidx.media3.common.MediaItem -import androidx.media3.session.MediaController -import com.google.common.util.concurrent.ListenableFuture +import androidx.media3.common.Player import it.vfsfitvnm.youtubemusic.Outcome import it.vfsfitvnm.youtubemusic.YouTube import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.guava.await import kotlinx.coroutines.withContext -class YoutubePlayer(mediaController: MediaController) : PlayerState(mediaController) { +class YoutubePlayer(mediaController: Player) : PlayerState(mediaController) { data class Radio( private val videoId: String? = null, private val playlistId: String? = null, @@ -45,22 +43,13 @@ class YoutubePlayer(mediaController: MediaController) : PlayerState(mediaControl } } -val LocalYoutubePlayer = compositionLocalOf { null } - @Composable fun rememberYoutubePlayer( - mediaControllerFuture: ListenableFuture + player: Player? ): YoutubePlayer? { - val mediaController by produceState(initialValue = null) { - value = mediaControllerFuture.await() - } - - val playerState = remember(mediaController) { - YoutubePlayer(mediaController ?: return@remember null).also { - // TODO: should we remove the listener later on? - mediaController?.addListener(it) + return remember(player) { + YoutubePlayer(player ?: return@remember null).also { + player.addListener(it) } } - - return playerState } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt index 5fda306..5d5c350 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt @@ -144,6 +144,8 @@ val SongWithInfo.asMediaItem: MediaItem .build() ) .setMediaId(song.id) + .setUri(song.id) + .setCustomCacheKey(song.id) .build() fun YouTube.PlaylistOrAlbum.Item.toMediaItem( @@ -172,6 +174,8 @@ fun YouTube.PlaylistOrAlbum.Item.toMediaItem( .build() ) .setMediaId(info.endpoint?.videoId ?: return null) + .setUri(info.endpoint?.videoId ?: return null) + .setCustomCacheKey(info.endpoint?.videoId ?: return null) .build() } diff --git a/settings.gradle.kts b/settings.gradle.kts index bfa5135..4170c67 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,8 +36,7 @@ dependencyResolutionManagement { alias("room-compiler").to("androidx.room", "room-compiler").versionRef("room") version("media3", "1.0.0-beta01") - alias("media3-session").to("androidx.media3", "media3-session").versionRef("media3") - alias("media3-exoplayer").to("androidx.media3", "media3-exoplayer").versionRef("media3") + alias("exoplayer").to("androidx.media3", "media3-exoplayer").versionRef("media3") version("ktor", "2.0.2") alias("ktor-client-core").to("io.ktor", "ktor-client-core").versionRef("ktor") @@ -49,8 +48,6 @@ dependencyResolutionManagement { alias("brotli").to("org.brotli", "dec").version("0.1.2") - alias("guava-coroutines").to("org.jetbrains.kotlinx", "kotlinx-coroutines-guava").version("1.6.2") - alias("desugaring").to("com.android.tools", "desugar_jdk_libs").version("1.1.5") }