feat: rewrite RadioPlaybackService as MediaLibraryService with browse tree
Made-with: Cursor
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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<LibraryResult<MediaItem>> {
|
||||
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<LibraryResult<ImmutableList<MediaItem>>> {
|
||||
val future = SettableFuture.create<LibraryResult<ImmutableList<MediaItem>>>()
|
||||
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<SessionResult> {
|
||||
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<MediaItem>
|
||||
): ListenableFuture<List<MediaItem>> {
|
||||
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<MediaItem> {
|
||||
val items = mutableListOf<MediaItem>()
|
||||
val playlists: List<Playlist> = 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<MediaItem> {
|
||||
return stationDao.getStationsByPlaylist(playlistId).first().map { it.toMediaItem() }
|
||||
}
|
||||
|
||||
private suspend fun buildUnsortedStations(): List<MediaItem> {
|
||||
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<String>) {
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user