From 00f4da20b6cb0b274fa67d4200d881ad93a03805 Mon Sep 17 00:00:00 2001 From: cottongin Date: Wed, 18 Mar 2026 06:10:17 -0400 Subject: [PATCH] feat: rewrite RadioPlaybackService as MediaLibraryService with browse tree Made-with: Cursor --- .../radio247/service/NotificationHelper.kt | 65 ---- .../radio247/service/RadioPlaybackService.kt | 283 ++++++++++++++---- .../radio247/service/RadioPlayerAdapter.kt | 149 ++++----- 3 files changed, 286 insertions(+), 211 deletions(-) delete mode 100644 app/src/main/java/xyz/cottongin/radio247/service/NotificationHelper.kt diff --git a/app/src/main/java/xyz/cottongin/radio247/service/NotificationHelper.kt b/app/src/main/java/xyz/cottongin/radio247/service/NotificationHelper.kt deleted file mode 100644 index dfa230e..0000000 --- a/app/src/main/java/xyz/cottongin/radio247/service/NotificationHelper.kt +++ /dev/null @@ -1,65 +0,0 @@ -package xyz.cottongin.radio247.service - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Context -import androidx.core.app.NotificationCompat -import android.support.v4.media.session.MediaSessionCompat -import xyz.cottongin.radio247.R -import xyz.cottongin.radio247.audio.IcyMetadata -import xyz.cottongin.radio247.audio.MetadataFormatter -import xyz.cottongin.radio247.data.model.Station - -class NotificationHelper(private val context: Context) { - companion object { - const val CHANNEL_ID = "radio_playback" - const val NOTIFICATION_ID = 1 - } - - fun createChannel() { - val channel = NotificationChannel( - CHANNEL_ID, - "Radio Playback", - NotificationManager.IMPORTANCE_LOW - ).apply { - description = "24/7 Radio playback" - } - context.getSystemService(NotificationManager::class.java) - .createNotificationChannel(channel) - } - - fun buildNotification( - station: Station, - metadata: IcyMetadata?, - isReconnecting: Boolean, - mediaSession: MediaSessionCompat, - stopPendingIntent: PendingIntent - ): Notification { - return NotificationCompat.Builder(context, CHANNEL_ID) - .setContentTitle(station.name) - .setContentText( - when { - isReconnecting -> "Reconnecting..." - metadata != null -> MetadataFormatter.formatTrackInfo(metadata, fallback = "Playing") - else -> "Playing" - } - ) - .setSmallIcon(R.drawable.ic_notification) - .setOngoing(true) - .setStyle( - androidx.media.app.NotificationCompat.MediaStyle() - .setMediaSession(mediaSession.sessionToken) - .setShowActionsInCompactView(0) - ) - .addAction( - NotificationCompat.Action.Builder( - android.R.drawable.ic_media_pause, - "Stop", - stopPendingIntent - ).build() - ) - .build() - } -} diff --git a/app/src/main/java/xyz/cottongin/radio247/service/RadioPlaybackService.kt b/app/src/main/java/xyz/cottongin/radio247/service/RadioPlaybackService.kt index 27c98fc..35dbac5 100644 --- a/app/src/main/java/xyz/cottongin/radio247/service/RadioPlaybackService.kt +++ b/app/src/main/java/xyz/cottongin/radio247/service/RadioPlaybackService.kt @@ -1,18 +1,27 @@ package xyz.cottongin.radio247.service import android.app.PendingIntent -import android.app.Service import android.content.Context import android.content.Intent import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest -import android.os.IBinder +import android.os.Bundle import android.os.PowerManager import android.util.Log -import androidx.lifecycle.LifecycleService -import android.support.v4.media.session.MediaSessionCompat +import com.google.common.util.concurrent.SettableFuture +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.session.CommandButton +import androidx.media3.session.LibraryResult +import androidx.media3.session.MediaLibraryService +import androidx.media3.session.MediaSession +import androidx.media3.session.SessionCommand +import androidx.media3.session.SessionResult +import com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture import xyz.cottongin.radio247.RadioApplication import xyz.cottongin.radio247.audio.AudioEngine import xyz.cottongin.radio247.audio.AudioEngineEvent @@ -25,6 +34,7 @@ import xyz.cottongin.radio247.data.db.StationDao import xyz.cottongin.radio247.data.model.ConnectionSpan import xyz.cottongin.radio247.data.model.ListeningSession import xyz.cottongin.radio247.data.model.MetadataSnapshot +import xyz.cottongin.radio247.data.model.Playlist import xyz.cottongin.radio247.data.model.Station import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -42,7 +52,7 @@ import kotlinx.coroutines.Job class ConnectionFailedException(cause: Throwable) : Exception("Connection failed", cause) -class RadioPlaybackService : LifecycleService() { +class RadioPlaybackService : MediaLibraryService() { companion object { private const val TAG = "RadioPlayback" const val ACTION_PLAY = "xyz.cottongin.radio247.PLAY" @@ -64,11 +74,12 @@ class RadioPlaybackService : LifecycleService() { private val connectionSpanDao: ConnectionSpanDao get() = database.connectionSpanDao() private val metadataSnapshotDao: MetadataSnapshotDao get() = database.metadataSnapshotDao() - private val notificationHelper = NotificationHelper(this) private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private val reconnectionMutex = Mutex() - private var mediaSession: MediaSessionCompat? = null + private var mediaSession: MediaLibraryService.MediaLibrarySession? = null + private var playerAdapter: RadioPlayerAdapter? = null + private val seekToLiveCommand = SessionCommand("SEEK_TO_LIVE", Bundle.EMPTY) private var engine: AudioEngine? = null private var wakeLock: PowerManager.WakeLock? = null private var wifiLock: android.net.wifi.WifiManager.WifiLock? = null @@ -101,12 +112,16 @@ class RadioPlaybackService : LifecycleService() { controller.updateState(newState) } + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibraryService.MediaLibrarySession? { + return mediaSession + } + override fun onCreate() { super.onCreate() - notificationHelper.createChannel() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) when (intent?.action) { ACTION_PLAY -> { val stationId = intent.getLongExtra(EXTRA_STATION_ID, -1L) @@ -150,8 +165,6 @@ class RadioPlaybackService : LifecycleService() { } } - override fun onBind(intent: Intent): IBinder? = null - override fun onDestroy() { cleanupResources() serviceScope.cancel() @@ -164,6 +177,8 @@ class RadioPlaybackService : LifecycleService() { releaseLocks() mediaSession?.release() mediaSession = null + playerAdapter?.clearState() + playerAdapter = null unregisterNetworkCallback() controller.updateLatency(0) } @@ -181,7 +196,8 @@ class RadioPlaybackService : LifecycleService() { acquireLocks() ensureMediaSession() - startForegroundWithPlaceholder(station) + playerAdapter?.updateStation(station) + playerAdapter?.updatePlaybackState(PlaybackState.Connecting(station)) try { val urls = app.streamResolver.resolveUrls(station) @@ -200,15 +216,15 @@ class RadioPlaybackService : LifecycleService() { val currentState = controller.state.value when { currentState is PlaybackState.Paused -> { - updateNotification(station, currentMetadata, isReconnecting = false, isPaused = true) + playerAdapter?.updatePlaybackState(PlaybackState.Paused(station = station, metadata = currentMetadata, sessionStartedAt = sessionStartedAt)) } else -> { val isActiveJob = playJob == coroutineContext[Job] if (isActiveJob) { transition(PlaybackState.Idle) + playerAdapter?.updatePlaybackState(PlaybackState.Idle) endListeningSession() cleanupResources() - stopForeground(Service.STOP_FOREGROUND_REMOVE) stopSelf() } } @@ -256,20 +272,199 @@ class RadioPlaybackService : LifecycleService() { } private fun ensureMediaSession() { - if (mediaSession == null) { - mediaSession = MediaSessionCompat(this, "Radio247").apply { - setFlags( - MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or - MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS + if (mediaSession != null) return + + val adapter = RadioPlayerAdapter( + onPlay = { + val state = controller.state.value + val station = when (state) { + is PlaybackState.Playing -> state.station + is PlaybackState.Paused -> state.station + is PlaybackState.Connecting -> state.station + is PlaybackState.Reconnecting -> state.station + is PlaybackState.Idle -> null + } + station?.let { controller.play(it) } + }, + onStop = { controller.stop() } + ) + playerAdapter = adapter + + val sessionActivityIntent = packageManager + .getLaunchIntentForPackage(packageName) + ?.let { intent -> + PendingIntent.getActivity( + this, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) - setCallback(object : MediaSessionCompat.Callback() { - override fun onStop() { - this@RadioPlaybackService.controller.stop() - } - }) - isActive = true } + + val seekToLiveButton = CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD) + .setDisplayName("Live") + .setSessionCommand(seekToLiveCommand) + .build() + + mediaSession = MediaLibrarySession.Builder(this, adapter, LibrarySessionCallback()) + .also { builder -> + sessionActivityIntent?.let { builder.setSessionActivity(it) } + } + .setCustomLayout(listOf(seekToLiveButton)) + .build() + } + + private inner class LibrarySessionCallback : MediaLibraryService.MediaLibrarySession.Callback { + + override fun onGetLibraryRoot( + session: MediaLibraryService.MediaLibrarySession, + browser: MediaSession.ControllerInfo, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture> { + val root = MediaItem.Builder() + .setMediaId("root") + .setMediaMetadata( + MediaMetadata.Builder() + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED) + .build() + ) + .build() + return Futures.immediateFuture(LibraryResult.ofItem(root, params)) } + + override fun onGetChildren( + session: MediaLibraryService.MediaLibrarySession, + browser: MediaSession.ControllerInfo, + parentId: String, + page: Int, + pageSize: Int, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture>> { + val future = SettableFuture.create>>() + serviceScope.launch { + try { + val items = when { + parentId == "root" -> buildRootChildren() + parentId == "unsorted" -> buildUnsortedStations() + parentId.startsWith("playlist:") -> { + val playlistId = parentId.removePrefix("playlist:").toLong() + buildPlaylistStations(playlistId) + } + else -> emptyList() + } + future.set(LibraryResult.ofItemList(ImmutableList.copyOf(items), params)) + } catch (_: Exception) { + future.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_UNKNOWN)) + } + } + return future + } + + override fun onCustomCommand( + session: MediaSession, + controller: MediaSession.ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture { + if (customCommand.customAction == "SEEK_TO_LIVE") { + handleSeekLive() + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + return super.onCustomCommand(session, controller, customCommand, args) + } + + override fun onConnect( + session: MediaSession, + controller: MediaSession.ControllerInfo + ): MediaSession.ConnectionResult { + val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS + .buildUpon() + .add(seekToLiveCommand) + .build() + return MediaSession.ConnectionResult.AcceptedResultBuilder(session) + .setAvailableSessionCommands(sessionCommands) + .build() + } + + override fun onAddMediaItems( + mediaSession: MediaSession, + controller: MediaSession.ControllerInfo, + mediaItems: MutableList + ): ListenableFuture> { + val item = mediaItems.firstOrNull() + ?: return Futures.immediateFuture(emptyList()) + val mediaId = item.mediaId + if (mediaId.startsWith("station:")) { + val stationId = mediaId.removePrefix("station:").toLongOrNull() + if (stationId != null) { + serviceScope.launch { + val station = stationDao.getStationById(stationId) + station?.let { this@RadioPlaybackService.controller.play(it) } + } + } + } + return Futures.immediateFuture(mediaItems) + } + } + + private suspend fun buildRootChildren(): List { + val items = mutableListOf() + val playlists: List = database.playlistDao().getAllPlaylists().first() + for (playlist in playlists) { + items.add( + MediaItem.Builder() + .setMediaId("playlist:${playlist.id}") + .setMediaMetadata( + MediaMetadata.Builder() + .setTitle(playlist.name) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS) + .build() + ) + .build() + ) + } + val unsorted = stationDao.getUnsortedStations().first() + if (unsorted.isNotEmpty()) { + items.add( + MediaItem.Builder() + .setMediaId("unsorted") + .setMediaMetadata( + MediaMetadata.Builder() + .setTitle("Unsorted") + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS) + .build() + ) + .build() + ) + } + return items + } + + private suspend fun buildPlaylistStations(playlistId: Long): List { + return stationDao.getStationsByPlaylist(playlistId).first().map { it.toMediaItem() } + } + + private suspend fun buildUnsortedStations(): List { + return stationDao.getUnsortedStations().first().map { it.toMediaItem() } + } + + private fun Station.toMediaItem(): MediaItem { + return MediaItem.Builder() + .setMediaId("station:${id}") + .setMediaMetadata( + MediaMetadata.Builder() + .setTitle(name) + .setArtworkUri(defaultArtworkUrl?.let { android.net.Uri.parse(it) }) + .setIsPlayable(true) + .setIsBrowsable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC) + .build() + ) + .build() } private suspend fun startEngine(station: Station, urls: List) { @@ -284,7 +479,7 @@ class RadioPlaybackService : LifecycleService() { sessionStartedAt = sessionStartedAt ) ) - updateNotification(station, null, isReconnecting = false) + playerAdapter?.updatePlaybackState(PlaybackState.Connecting(station = station)) val bufferMs = app.preferences.bufferMs.first() engine = AudioEngine(url, bufferMs) @@ -328,7 +523,7 @@ class RadioPlaybackService : LifecycleService() { connectionStartedAt = connectionStartedAt ) ) - updateNotification(station, null, false) + playerAdapter?.updatePlaybackState(PlaybackState.Playing(station = station)) controller.updateLatency(engine!!.estimatedLatencyMs) } is AudioEngineEvent.MetadataChanged -> { @@ -337,7 +532,16 @@ class RadioPlaybackService : LifecycleService() { if (playingState is PlaybackState.Playing) { transition(playingState.copy(metadata = event.metadata)) } - updateNotification(station, event.metadata, false) + serviceScope.launch { + val artUrl = app.albumArtResolver.resolve( + artist = event.metadata.artist, + title = event.metadata.title, + icyStreamUrl = event.metadata.streamUrl, + stationArtworkUrl = station.defaultArtworkUrl + ) + val artUri = artUrl?.let { android.net.Uri.parse(it) } + playerAdapter?.updateMetadata(station, event.metadata, artUri) + } persistMetadataSnapshot(station.id, event.metadata) } is AudioEngineEvent.StreamInfoReceived -> { @@ -395,7 +599,7 @@ class RadioPlaybackService : LifecycleService() { attempt = attempt ) ) - updateNotification(station, currentMetadata, isReconnecting = true) + playerAdapter?.updatePlaybackState(PlaybackState.Reconnecting(station = station, metadata = currentMetadata, sessionStartedAt = sessionStartedAt, attempt = attempt)) val delayMs = minOf(1000L * (1 shl (attempt - 1)), 30_000L) val chunk = 500L var remaining = delayMs @@ -439,29 +643,6 @@ class RadioPlaybackService : LifecycleService() { } } - private fun startForegroundWithPlaceholder(station: Station) { - updateNotification(station, null, isReconnecting = false) - } - - private fun updateNotification(station: Station, metadata: IcyMetadata?, isReconnecting: Boolean, isPaused: Boolean = false) { - val session = mediaSession ?: return - val stopIntent = Intent(this, RadioPlaybackService::class.java).apply { - action = ACTION_STOP - } - val stopPendingIntent = PendingIntent.getService( - this, 0, stopIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - val notification = notificationHelper.buildNotification( - station = station, - metadata = metadata, - isReconnecting = isReconnecting, - mediaSession = session, - stopPendingIntent = stopPendingIntent - ) - startForeground(NotificationHelper.NOTIFICATION_ID, notification) - } - private suspend fun persistMetadataSnapshot(stationId: Long, metadata: IcyMetadata) { val now = System.currentTimeMillis() withContext(Dispatchers.IO) { diff --git a/app/src/main/java/xyz/cottongin/radio247/service/RadioPlayerAdapter.kt b/app/src/main/java/xyz/cottongin/radio247/service/RadioPlayerAdapter.kt index ebe6cd0..1ab81d9 100644 --- a/app/src/main/java/xyz/cottongin/radio247/service/RadioPlayerAdapter.kt +++ b/app/src/main/java/xyz/cottongin/radio247/service/RadioPlayerAdapter.kt @@ -1,100 +1,74 @@ package xyz.cottongin.radio247.service import android.os.Looper -import androidx.media3.common.BasePlayer -import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata -import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player -import androidx.media3.common.Timeline +import androidx.media3.common.SimpleBasePlayer +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture import xyz.cottongin.radio247.audio.IcyMetadata import xyz.cottongin.radio247.data.model.Station class RadioPlayerAdapter( private val onPlay: () -> Unit, private val onStop: () -> Unit, -) : BasePlayer() { +) : SimpleBasePlayer(Looper.getMainLooper()) { - private val listeners = mutableListOf() + @Volatile private var _playbackState: Int = Player.STATE_IDLE + + @Volatile private var _playWhenReady: Boolean = false + + @Volatile private var _mediaMetadata: MediaMetadata = MediaMetadata.EMPTY + + @Volatile private var _currentMediaItem: MediaItem? = null - override fun getApplicationLooper(): Looper = Looper.getMainLooper() + private val commands = Player.Commands.Builder() + .add(Player.COMMAND_PLAY_PAUSE) + .add(Player.COMMAND_STOP) + .add(Player.COMMAND_GET_CURRENT_MEDIA_ITEM) + .add(Player.COMMAND_GET_MEDIA_ITEMS_METADATA) + .build() - override fun addListener(listener: Player.Listener) { - listeners.add(listener) - } + override fun getState(): SimpleBasePlayer.State { + val mediaItem = _currentMediaItem + val playlist = if (mediaItem != null) { + listOf(getPlaceholderMediaItemData(mediaItem)) + } else { + emptyList() + } - override fun removeListener(listener: Player.Listener) { - listeners.remove(listener) - } - - override fun getAvailableCommands(): Player.Commands = - Player.Commands.Builder() - .add(Player.COMMAND_PLAY_PAUSE) - .add(Player.COMMAND_STOP) - .add(Player.COMMAND_GET_CURRENT_MEDIA_ITEM) - .add(Player.COMMAND_GET_MEDIA_ITEMS_METADATA) + return SimpleBasePlayer.State.Builder() + .setAvailableCommands(commands) + .setPlaybackState(_playbackState) + .setPlayWhenReady(_playWhenReady, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) + .setCurrentMediaItemIndex(if (mediaItem != null) 0 else 0) + .setContentPositionMs(0L) + .setIsLoading(false) + .setPlaylist(playlist) .build() - - override fun getPlaybackState(): Int = _playbackState - override fun getPlayWhenReady(): Boolean = _playWhenReady - override fun isPlaying(): Boolean = _playbackState == Player.STATE_READY && _playWhenReady - override fun getMediaMetadata(): MediaMetadata = _mediaMetadata - override fun getCurrentMediaItem(): MediaItem? = _currentMediaItem - - override fun play() { - _playWhenReady = true - onPlay() } - override fun pause() { + override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> { + _playWhenReady = playWhenReady + if (playWhenReady) { + onPlay() + } else { + onStop() + } + return Futures.immediateVoidFuture() + } + + override fun handleStop(): ListenableFuture<*> { _playWhenReady = false onStop() + return Futures.immediateVoidFuture() } - override fun stop() { - _playWhenReady = false - onStop() - } - - override fun prepare() {} - - override fun setPlayWhenReady(playWhenReady: Boolean) { - if (playWhenReady) play() else pause() - } - - override fun release() { - listeners.clear() - } - - override fun getPlaybackParameters(): PlaybackParameters = PlaybackParameters.DEFAULT - override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {} - - override fun getRepeatMode(): Int = Player.REPEAT_MODE_OFF - override fun setRepeatMode(repeatMode: Int) {} - - override fun getShuffleModeEnabled(): Boolean = false - override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {} - - override fun seekToDefaultPosition() {} - override fun seekToDefaultPosition(mediaItemIndex: Int) {} - override fun seekTo(positionMs: Long) {} - override fun seekTo(mediaItemIndex: Int, positionMs: Long) {} - - override fun getCurrentTimeline(): Timeline = Timeline.EMPTY - override fun getCurrentMediaItemIndex(): Int = 0 - override fun getContentPosition(): Long = 0 - override fun getContentDuration(): Long = C.TIME_UNSET - override fun getCurrentPosition(): Long = 0 - override fun getDuration(): Long = C.TIME_UNSET - override fun getBufferedPosition(): Long = 0 - override fun getTotalBufferedDuration(): Long = 0 - override fun isCurrentMediaItemLive(): Boolean = true - fun updatePlaybackState(state: PlaybackState) { val (newPlayerState, newPlayWhenReady) = when (state) { is PlaybackState.Idle -> Player.STATE_IDLE to false @@ -103,26 +77,9 @@ class RadioPlayerAdapter( is PlaybackState.Paused -> Player.STATE_READY to false is PlaybackState.Reconnecting -> Player.STATE_BUFFERING to true } - - val stateChanged = newPlayerState != _playbackState - val playWhenReadyChanged = newPlayWhenReady != _playWhenReady _playbackState = newPlayerState _playWhenReady = newPlayWhenReady - - if (stateChanged || playWhenReadyChanged) { - listeners.forEach { listener -> - if (playWhenReadyChanged) { - listener.onPlayWhenReadyChanged( - newPlayWhenReady, - Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST - ) - } - if (stateChanged) { - listener.onPlaybackStateChanged(newPlayerState) - } - listener.onIsPlayingChanged(isPlaying) - } - } + invalidateState() } fun updateStation(station: Station) { @@ -135,12 +92,7 @@ class RadioPlayerAdapter( .build() ) .build() - listeners.forEach { - it.onMediaItemTransition( - _currentMediaItem, - Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED - ) - } + invalidateState() } fun updateMetadata( @@ -155,6 +107,13 @@ class RadioPlayerAdapter( .setArtworkUri(artworkUri) .setIsPlayable(true) .build() - listeners.forEach { it.onMediaMetadataChanged(_mediaMetadata) } + invalidateState() + } + + fun clearState() { + _currentMediaItem = null + _mediaMetadata = MediaMetadata.EMPTY + _playbackState = Player.STATE_IDLE + _playWhenReady = false } }