feat: rewrite RadioPlaybackService as MediaLibraryService with browse tree

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-18 06:10:17 -04:00
parent 04225de888
commit 00f4da20b6
3 changed files with 286 additions and 211 deletions

View File

@@ -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<Player.Listener>()
@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
}
}