diff --git a/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/PlayerListener.android.kt b/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/PlayerListener.android.kt index b9b027e6..9a3b0820 100644 --- a/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/PlayerListener.android.kt +++ b/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/PlayerListener.android.kt @@ -21,6 +21,7 @@ actual abstract class PlayerListener { actual open fun onSongAdded(index: Int, song: Song) {} actual open fun onSongRemoved(index: Int, song: Song) {} actual open fun onSongMoved(from: Int, to: Int) {} + actual open fun onRadioCancelRequested() {} actual open fun onEvents() {} diff --git a/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/ForegroundPlayerService.kt b/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/ForegroundPlayerService.kt index d88e2561..32be95b8 100644 --- a/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/ForegroundPlayerService.kt +++ b/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/ForegroundPlayerService.kt @@ -31,12 +31,13 @@ import com.toasterofbread.spmp.platform.PlayerServiceCommand import com.toasterofbread.spmp.platform.playerservice.* import com.toasterofbread.spmp.platform.visualiser.MusicVisualiser import com.toasterofbread.spmp.shared.R +import com.toasterofbread.spmp.service.playercontroller.RadioHandler import kotlinx.coroutines.* import spms.socketapi.shared.SpMsPlayerRepeatMode import spms.socketapi.shared.SpMsPlayerState @androidx.annotation.OptIn(UnstableApi::class) -open class ForegroundPlayerService: MediaSessionService(), PlayerService { +open class ForegroundPlayerService(private val play_when_ready: Boolean): MediaSessionService(), PlayerService { override val load_state: PlayerServiceLoadState = PlayerServiceLoadState(false) override val connection_error: Throwable? = null override val context: AppContext get() = _context @@ -83,6 +84,8 @@ open class ForegroundPlayerService: MediaSessionService(), PlayerService { listener.removeFromPlayer(player) } + protected open fun onRadioCancelled() {} + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) return START_NOT_STICKY @@ -110,7 +113,7 @@ open class ForegroundPlayerService: MediaSessionService(), PlayerService { _context = AppContext(this, coroutine_scope) _context.getPrefs().addListener(prefs_listener) - initialiseSessionAndPlayer() + initialiseSessionAndPlayer(play_when_ready) _service_player = object : PlayerServicePlayer(this) { override fun onUndoStateChanged() { @@ -118,6 +121,14 @@ open class ForegroundPlayerService: MediaSessionService(), PlayerService { listener.onUndoStateChanged() } } + + override val radio: RadioHandler = + object : RadioHandler(this, context) { + override fun onRadioCancelled() { + super.onRadioCancelled() + this@ForegroundPlayerService.onRadioCancelled() + } + } } val audio_manager = getSystemService(AUDIO_SERVICE) as AudioManager? diff --git a/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformExternalPlayerService.android.kt b/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformExternalPlayerService.android.kt index 9a526030..c5a708cd 100644 --- a/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformExternalPlayerService.android.kt +++ b/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformExternalPlayerService.android.kt @@ -8,83 +8,162 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import spms.socketapi.shared.SpMsPlayerRepeatMode import spms.socketapi.shared.SpMsPlayerState +import androidx.media3.common.Player +import androidx.media3.common.MediaItem as ExoMediaItem -actual class PlatformExternalPlayerService: ForegroundPlayerService(), PlayerService { - private val service: ExternalPlayerService = ExternalPlayerService() +actual class PlatformExternalPlayerService: ForegroundPlayerService(play_when_ready = true), PlayerService { + // private var cancelling_radio: Boolean = false - private val service_listener = object : PlayerListener() { - override fun onSongAdded(index: Int, song: Song) = this@PlatformExternalPlayerService.onSongAdded(index, song) - override fun onPlayingChanged(is_playing: Boolean) = this@PlatformExternalPlayerService.onPlayingChanged(is_playing) - override fun onSeeked(position_ms: Long) = this@PlatformExternalPlayerService.onSeeked(position_ms) - override fun onSongMoved(from: Int, to: Int) = this@PlatformExternalPlayerService.onSongMoved(from, to) - override fun onSongRemoved(index: Int, song: Song) = this@PlatformExternalPlayerService.onSongRemoved(index) - override fun onSongTransition(song: Song?, manual: Boolean) = this@PlatformExternalPlayerService.onSongTransition(song, manual) + override fun onRadioCancelled() { + super.onRadioCancelled() + server.onRadioCancelled() } + private val server: ExternalPlayerService = + object : ExternalPlayerService(plays_audio = true) { + override fun createServicePlayer(): PlayerServicePlayer = this@PlatformExternalPlayerService.service_player + } + + private val server_listener: PlayerListener = + object : PlayerListener() { + override fun onSongAdded(index: Int, song: Song) = this@PlatformExternalPlayerService.onSongAdded(index, song) + override fun onPlayingChanged(is_playing: Boolean) = this@PlatformExternalPlayerService.onPlayingChanged(is_playing) + override fun onSeeked(position_ms: Long) = this@PlatformExternalPlayerService.onSeeked(position_ms) + override fun onSongMoved(from: Int, to: Int) = this@PlatformExternalPlayerService.onSongMoved(from, to) + override fun onSongRemoved(index: Int, song: Song) = this@PlatformExternalPlayerService.onSongRemoved(index) + override fun onSongTransition(song: Song?, manual: Boolean) = this@PlatformExternalPlayerService.onSongTransition(current_song_index) + // override fun onRadioCancelRequested() { + // cancelling_radio = true + // this@PlatformExternalPlayerService.service_player.radio_instance.cancelRadio() + // cancelling_radio = false + // } + } + + private val player_listener: Player.Listener = + object : Player.Listener { + private var last_seek_position: Long? = null + + override fun onMediaItemTransition(mediaItem: ExoMediaItem?, reason: Int) { + if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK && player.currentMediaItemIndex != current_song_index) { + server.seekToSong(player.currentMediaItemIndex) + } + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + if (isPlaying == target_playing || player.playbackState != Player.STATE_READY) { + return + } + + if (isPlaying) { + server.play() + } + else { + server.pause() + } + } + + override fun onPositionDiscontinuity(oldPosition: Player.PositionInfo, newPosition: Player.PositionInfo, reason: Int) { + if (newPosition.positionMs == target_seek) { + return + } + + if (reason == Player.DISCONTINUITY_REASON_SEEK && newPosition.positionMs != last_seek_position) { + last_seek_position = newPosition.positionMs + pause() + server.seekTo(newPosition.positionMs) + + if (player.playbackState == Player.STATE_READY) { + server.notifyReadyToPlay() + } + } + } + + override fun onPlaybackStateChanged(playbackState: Int) { + if (playbackState == Player.STATE_READY) { + server.notifyReadyToPlay() + } + } + } + + private var target_playing: Boolean = false + private var target_seek: Long? = null + override fun onCreate() { super.onCreate() - service._context = context - service.addListener(service_listener) + server._context = context + server.addListener(server_listener) + + player.addListener(player_listener) - service.onCreate() + server.onCreate() } override fun onDestroy() { super.onDestroy() - service.onDestroy() + server.onDestroy() } private fun onSongAdded(index: Int, song: Song) { coroutine_scope.launch(Dispatchers.Main) { super.addSong(song, index) }} private fun onPlayingChanged(is_playing: Boolean) { coroutine_scope.launch(Dispatchers.Main) { + target_playing = is_playing if (is_playing) super.play() else super.pause() }} private fun onSeeked(position_ms: Long) { coroutine_scope.launch(Dispatchers.Main) { + target_seek = position_ms super.seekTo(position_ms) }} private fun onSongMoved(from: Int, to: Int) { coroutine_scope.launch(Dispatchers.Main) { super.moveSong(from, to) }} - private fun onSongRemoved(index: Int) { coroutine_scope.launch(Dispatchers.Main) { + private fun onSongRemoved(index: Int) { coroutine_scope.launch(Dispatchers.Main) { super.removeSong(index) }} - private fun onSongTransition(song: Song?, manual: Boolean) { coroutine_scope.launch(Dispatchers.Main) { - super.seekToSong(current_song_index) + private fun onSongTransition(index: Int) { coroutine_scope.launch(Dispatchers.Main) { + if (index < 0 || index == player.currentMediaItemIndex) { + return@launch + } + try { + super.seekToSong(index) + } + catch (e: Throwable) { + throw RuntimeException("seekToSong($index) failed", e) + } }} - override val load_state: PlayerServiceLoadState get() = service.load_state - override val state: SpMsPlayerState get() = service.state - override val is_playing: Boolean get() = service.is_playing - override val song_count: Int get() = service.song_count - override val current_song_index: Int get() = service.current_song_index - override val current_position_ms: Long get() = service.current_position_ms - override val duration_ms: Long get() = service.duration_ms - override val has_focus: Boolean get() = service.has_focus - override val radio_instance: RadioInstance get() = service.radio_instance + override val load_state: PlayerServiceLoadState get() = server.load_state + override val state: SpMsPlayerState get() = server.state + override val is_playing: Boolean get() = server.is_playing + override val song_count: Int get() = server.song_count + override val current_song_index: Int get() = server.current_song_index + override val current_position_ms: Long get() = server.current_position_ms + override val duration_ms: Long get() = server.duration_ms + override val has_focus: Boolean get() = server.has_focus + override val radio_instance: RadioInstance get() = server.radio_instance override var repeat_mode: SpMsPlayerRepeatMode - get() = service.repeat_mode - set(value) { service.repeat_mode = value } + get() = server.repeat_mode + set(value) { server.repeat_mode = value } override var volume: Float - get() = service.volume - set(value) { service.volume = value } - - override fun play() = service.play() - override fun pause() = service.pause() - override fun playPause() = service.playPause() - override fun seekTo(position_ms: Long) = service.seekTo(position_ms) - override fun seekToSong(index: Int) = service.seekToSong(index) - override fun seekToNext() = service.seekToNext() - override fun seekToPrevious() = service.seekToPrevious() - override fun getSong(): Song? = service.getSong() - override fun getSong(index: Int): Song? = service.getSong(index) - override fun addSong(song: Song, index: Int) = service.addSong(song, index) - override fun moveSong(from: Int, to: Int) = service.moveSong(from, to) - override fun removeSong(index: Int) = service.removeSong(index) - override fun addListener(listener: PlayerListener) = service.addListener(listener) - override fun removeListener(listener: PlayerListener) = service.removeListener(listener) + get() = server.volume + set(value) { server.volume = value } + + override fun play() = server.play() + override fun pause() = server.pause() + override fun playPause() = server.playPause() + override fun seekTo(position_ms: Long) = server.seekTo(position_ms) + override fun seekToSong(index: Int) = server.seekToSong(index) + override fun seekToNext() = server.seekToNext() + override fun seekToPrevious() = server.seekToPrevious() + override fun getSong(): Song? = server.getSong() + override fun getSong(index: Int): Song? = server.getSong(index) + override fun addSong(song: Song, index: Int) = server.addSong(song, index) + override fun moveSong(from: Int, to: Int) = server.moveSong(from, to) + override fun removeSong(index: Int) = server.removeSong(index) + override fun addListener(listener: PlayerListener) = server.addListener(listener) + override fun removeListener(listener: PlayerListener) = server.removeListener(listener) actual companion object: InternalPlayerServiceCompanion(PlatformExternalPlayerService::class), PlayerServiceCompanion { override fun isServiceRunning(context: AppContext): Boolean = true diff --git a/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformInternalPlayerService.android.kt b/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformInternalPlayerService.android.kt index f3452b6e..df29daee 100644 --- a/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformInternalPlayerService.android.kt +++ b/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformInternalPlayerService.android.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.graphics.Color import com.toasterofbread.spmp.platform.AppContext import ProgramArguments -actual class PlatformInternalPlayerService: ForegroundPlayerService(), PlayerService { +actual class PlatformInternalPlayerService: ForegroundPlayerService(play_when_ready = true), PlayerService { actual companion object: InternalPlayerServiceCompanion(PlatformInternalPlayerService::class), PlayerServiceCompanion { actual fun isAvailable(context: AppContext, launch_arguments: ProgramArguments): Boolean = true } diff --git a/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/initialiseSessionAndPlayer.kt b/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/initialiseSessionAndPlayer.kt index f8e210b1..8158c6a5 100644 --- a/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/initialiseSessionAndPlayer.kt +++ b/shared/src/androidMain/kotlin/com/toasterofbread/spmp/platform/playerservice/initialiseSessionAndPlayer.kt @@ -38,7 +38,7 @@ import dev.toastbits.ytmkt.formats.VideoFormatsEndpoint import kotlinx.coroutines.runBlocking import java.util.concurrent.Executors -internal fun ForegroundPlayerService.initialiseSessionAndPlayer() { +internal fun ForegroundPlayerService.initialiseSessionAndPlayer(play_when_ready: Boolean) { audio_sink = DefaultAudioSink.Builder(context.ctx) .setAudioProcessorChain( DefaultAudioProcessorChain( @@ -108,7 +108,7 @@ internal fun ForegroundPlayerService.initialiseSessionAndPlayer() { val player_listener: InternalPlayerServicePlayerListener = InternalPlayerServicePlayerListener(this) player.addListener(player_listener) - player.playWhenReady = true + player.playWhenReady = play_when_ready player.prepare() val controller_future: ListenableFuture = diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/lyrics/SongLyrics.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/lyrics/SongLyrics.kt index 43b130c3..8f7603bb 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/lyrics/SongLyrics.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/lyrics/SongLyrics.kt @@ -73,18 +73,18 @@ data class SongLyrics( get() = start!! .. end!! } - init { - lazyAssert { - synchronized(lines) { - for (line in lines) { - for (term in line) { - if (sync_type != SyncType.NONE && (term.start == null || term.end == null)) { - return@lazyAssert false - } - } - } - } - return@lazyAssert true - } - } + // init { + // lazyAssert { + // synchronized(lines) { + // for (line in lines) { + // for (term in line) { + // if (sync_type != SyncType.NONE && (term.start == null || term.end == null)) { + // return@lazyAssert false + // } + // } + // } + // } + // return@lazyAssert true + // } + // } } diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/radio/RadioInstance.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/radio/RadioInstance.kt index bfd6885e..17192250 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/radio/RadioInstance.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/radio/RadioInstance.kt @@ -54,7 +54,7 @@ abstract class RadioInstance(val context: AppContext) { state = state.copy(current_filter_index = filter_index) } - fun cancelRadio() { + open fun cancelRadio() { cancelCurrentJob() state = RadioState() } @@ -191,5 +191,8 @@ abstract class RadioInstance(val context: AppContext) { return filtered } + + override fun toString(): String = + "RadioInstance(state=$state, is_loading=$is_loading, load_error=$load_error)" } diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/radio/RadioState.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/radio/RadioState.kt index a4b4d986..06c5ac3a 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/radio/RadioState.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/radio/RadioState.kt @@ -29,7 +29,7 @@ data class RadioState( val current_filter_index: Int? = null ) { fun isContinuationAvailable(): Boolean = - continuation != null || !initial_songs_loaded + continuation != null || (item_uid != null && !initial_songs_loaded) internal suspend fun loadContinuation(context: AppContext): Result = runCatching { if (item_uid == null) { diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/PlayerListener.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/PlayerListener.kt index 24a6478c..53f89059 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/PlayerListener.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/PlayerListener.kt @@ -17,6 +17,7 @@ expect abstract class PlayerListener() { open fun onSongAdded(index: Int, song: Song) open fun onSongRemoved(index: Int, song: Song) open fun onSongMoved(from: Int, to: Int) + open fun onRadioCancelRequested() open fun onEvents() } diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/ExternalPlayerService.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/ExternalPlayerService.kt index 71e83517..fa7d3909 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/ExternalPlayerService.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/ExternalPlayerService.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.graphics.Color import com.toasterofbread.spmp.model.mediaitem.song.Song import com.toasterofbread.spmp.model.radio.RadioInstance import com.toasterofbread.spmp.platform.AppContext +import com.toasterofbread.spmp.service.playercontroller.RadioHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.cancel @@ -17,7 +18,7 @@ import dev.toastbits.composekit.platform.PlatformPreferencesListener import dev.toastbits.composekit.platform.PlatformPreferences import io.ktor.client.request.get -open class ExternalPlayerService: SpMsPlayerService(), PlayerService { +open class ExternalPlayerService(plays_audio: Boolean, private val create_player: Boolean = true): SpMsPlayerService(plays_audio = plays_audio), PlayerService { override val load_state: PlayerServiceLoadState get() = socket_load_state override val connection_error: Throwable? get() = socket_connection_error private val coroutine_scope: CoroutineScope = CoroutineScope(Job()) @@ -29,6 +30,43 @@ open class ExternalPlayerService: SpMsPlayerService(), PlayerService { _context = context } + internal fun notifyReadyToPlay() { + val song: Song = getSong() ?: return + sendRequest("readyToPlay", JsonPrimitive(current_song_index), JsonPrimitive(song.id), JsonPrimitive(duration_ms)) + } + + private var cancelling_radio: Boolean = false + + override fun onRadioCancelRequested() { + cancelling_radio = true + radio_instance.cancelRadio() + cancelling_radio = false + } + + internal fun onRadioCancelled() { + if (cancelling_radio) { + return + } + sendRequest("cancelRadio") + } + + protected open fun createServicePlayer(): PlayerServicePlayer = + object : PlayerServicePlayer(this) { + override fun onUndoStateChanged() { + for (listener in listeners) { + listener.onUndoStateChanged() + } + } + + override val radio: RadioHandler = + object : RadioHandler(this, context) { + override fun onRadioCancelled() { + super.onRadioCancelled() + this@ExternalPlayerService.onRadioCancelled() + } + } + } + private lateinit var _service_player: PlayerServicePlayer override val service_player: PlayerServicePlayer get() = _service_player @@ -149,13 +187,7 @@ open class ExternalPlayerService: SpMsPlayerService(), PlayerService { override fun onCreate() { super.onCreate() - _service_player = object : PlayerServicePlayer(this) { - override fun onUndoStateChanged() { - for (listener in listeners) { - listener.onUndoStateChanged() - } - } - } + _service_player = createServicePlayer() coroutine_scope.launch { val prefs_listener: PlatformPreferencesListener = diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlayerServicePlayer.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlayerServicePlayer.kt index 55df641f..b9b1c133 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlayerServicePlayer.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlayerServicePlayer.kt @@ -52,11 +52,11 @@ private const val UPDATE_INTERVAL: Long = 30000 // ms private const val SONG_MARK_WATCHED_POSITION = 1000 // ms @Suppress("LeakingThis") -abstract class PlayerServicePlayer(private val service: PlayerService) { +abstract class PlayerServicePlayer(internal val service: PlayerService) { private val context: AppContext get() = service.context private val coroutine_scope: CoroutineScope = CoroutineScope(Dispatchers.Main) - internal val radio: RadioHandler = RadioHandler(this, context) + internal open val radio: RadioHandler = RadioHandler(this, context) private val persistent_queue: PersistentQueueHandler = PersistentQueueHandler(this, context) private val discord_status: DiscordStatusHandler = DiscordStatusHandler(this, context) private val undo_handler: UndoHandler = UndoHandler(this, service) @@ -79,7 +79,7 @@ abstract class PlayerServicePlayer(private val service: PlayerService) { abstract fun onUndoStateChanged() - private val prefs_listener = + private val prefs_listener = PlatformPreferencesListener { _, key -> when (key) { context.settings.discord_auth.DISCORD_ACCOUNT_TOKEN.key -> { diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/SpMsPlayerService.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/SpMsPlayerService.kt index 605e4a90..d17668aa 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/SpMsPlayerService.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/SpMsPlayerService.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import dev.toastbits.composekit.platform.PlatformPreferences import dev.toastbits.composekit.platform.PlatformPreferencesListener +import dev.toastbits.composekit.platform.Platform import dev.toastbits.composekit.utils.common.launchSingle import com.toasterofbread.spmp.model.mediaitem.song.Song import com.toasterofbread.spmp.model.settings.unpackSetData @@ -33,9 +34,9 @@ import java.net.InetAddress import spms.socketapi.shared.* private const val POLL_STATE_INTERVAL: Long = 100 -private const val POLL_TIMEOUT_MS: Long = 10000 +private const val POLL_TIMEOUT_MS: Long = 3000 -abstract class SpMsPlayerService: PlatformServiceImpl(), ClientServerPlayerService { +abstract class SpMsPlayerService(val plays_audio: Boolean): PlatformServiceImpl(), ClientServerPlayerService { override var connected_server: ClientServerPlayerService.ServerInfo? by mutableStateOf(null) private val clients_result_channel: Channel = Channel() @@ -45,13 +46,14 @@ abstract class SpMsPlayerService: PlatformServiceImpl(), ClientServerPlayerServi var socket_connection_error: Throwable? by mutableStateOf(null) private set + internal abstract fun onRadioCancelRequested() + private fun getServerPort(): Int = context.settings.platform.SERVER_PORT.get() private fun getServerIp(): String = context.settings.platform.SERVER_IP_ADDRESS.get() private fun getClientName(): String { - val host: String = InetAddress.getLocalHost().hostName - val os: String = System.getProperty("os.name") - + val os: String = Platform.getOSName() + var host: String = Platform.getHostName() return getString("app_name") + " [$os, $host]" } @@ -182,7 +184,7 @@ abstract class SpMsPlayerService: PlatformServiceImpl(), ClientServerPlayerServi val handshake: SpMsClientHandshake = SpMsClientHandshake( name = getClientName(), - type = SpMsClientType.SPMP_STANDALONE, + type = if (plays_audio) SpMsClientType.SPMP_PLAYER else SpMsClientType.SPMP_STANDALONE, machine_id = getSpMsMachineId(context), language = context.getUiLanguage() ) diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/applyPlayerEvents.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/applyPlayerEvents.kt index 5208dfc8..7a41027e 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/applyPlayerEvents.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/applyPlayerEvents.kt @@ -39,7 +39,12 @@ private fun SpMsPlayerService.applyEvent(event: SpMsPlayerEvent) { when (event.type) { SpMsPlayerEvent.Type.ITEM_TRANSITION -> { - _current_song_index = event.properties["index"]!!.int + val target_index: Int = event.properties["index"]!!.int + if (target_index == _current_song_index) { + return + } + + _current_song_index = target_index _duration_ms = -1 updateCurrentSongPosition(0) @@ -49,6 +54,83 @@ private fun SpMsPlayerService.applyEvent(event: SpMsPlayerEvent) { it.onEvents() } } + SpMsPlayerEvent.Type.SEEKED -> { + val position_ms: Long = event.properties["position_ms"]!!.long + updateCurrentSongPosition(position_ms) + listeners.forEach { + it.onSeeked(position_ms) + it.onEvents() + } + } + SpMsPlayerEvent.Type.ITEM_ADDED -> { + val song: SongData = SongData(event.properties["item_id"]!!.content) + val index: Int = event.properties["index"]!!.int + + if (index <= _current_song_index) { + _current_song_index++ + } + + playlist.add(minOf(playlist.size, index), song) + listeners.forEach { + it.onSongAdded(index, song) + it.onEvents() + } + service_player.session_started = true + } + SpMsPlayerEvent.Type.ITEM_REMOVED -> { + val index: Int = event.properties["index"]!!.int + if (index !in playlist.indices) { + return + } + + val song: Song = playlist.removeAt(index) + val transitioning: Boolean = index == _current_song_index + + if (index <= _current_song_index) { + _current_song_index-- + } + + listeners.forEach { + it.onSongRemoved(index, song) + if (transitioning) { + it.onSongTransition(playlist.getOrNull(_current_song_index), false) + } + it.onEvents() + } + } + SpMsPlayerEvent.Type.ITEM_MOVED -> { + val to: Int = event.properties["to"]!!.int + val from: Int = event.properties["from"]!!.int + + val song: Song = playlist.removeAt(from) + playlist.add(to, song) + + if (from == _current_song_index) { + _current_song_index = to + } + + listeners.forEach { + it.onSongMoved(from, to) + it.onEvents() + } + } + SpMsPlayerEvent.Type.CLEARED -> { + for (i in playlist.indices.reversed()) { + val song: Song = playlist.removeAt(i) + listeners.forEach { + it.onSongRemoved(i, song) + } + } + listeners.forEach { + it.onEvents() + } + } + SpMsPlayerEvent.Type.CANCEL_RADIO -> { + onRadioCancelRequested() + // listeners.forEach { + // it.onRadioCancelRequested() + // } + } SpMsPlayerEvent.Type.PROPERTY_CHANGED -> { val key: String = event.properties["key"]!!.content val value: JsonPrimitive = event.properties["value"]!! @@ -101,67 +183,6 @@ private fun SpMsPlayerService.applyEvent(event: SpMsPlayerEvent) { else -> throw NotImplementedError(key) } } - SpMsPlayerEvent.Type.SEEKED -> { - val position_ms: Long = event.properties["position_ms"]!!.long - updateCurrentSongPosition(position_ms) - listeners.forEach { - it.onSeeked(position_ms) - it.onEvents() - } - } - SpMsPlayerEvent.Type.ITEM_ADDED -> { - val song: SongData = SongData(event.properties["item_id"]!!.content) - val index: Int = event.properties["index"]!!.int - playlist.add(minOf(playlist.size, index), song) - listeners.forEach { - it.onSongAdded(index, song) - it.onEvents() - } - service_player.session_started = true - } - SpMsPlayerEvent.Type.ITEM_REMOVED -> { - val index: Int = event.properties["index"]!!.int - if (index in playlist.indices) { - val song: Song = playlist.removeAt(index) - listeners.forEach { - it.onSongRemoved(index, song) - it.onEvents() - } - } - } - SpMsPlayerEvent.Type.ITEM_MOVED -> { - val to: Int = event.properties["to"]!!.int - val from: Int = event.properties["from"]!!.int - - val song: Song = playlist.removeAt(from) - playlist.add(to, song) - - if (from == _current_song_index) { - _current_song_index = to - } - - listeners.forEach { - it.onSongMoved(from, to) - - if (from == _current_song_index) { - it.onSongTransition(song, true) - } - - it.onEvents() - } - } - SpMsPlayerEvent.Type.CLEARED -> { - for (i in playlist.indices.reversed()) { - val song: Song = playlist.removeAt(i) - listeners.forEach { - it.onSongRemoved(i, song) - } - } - listeners.forEach { - it.onEvents() - } - } - SpMsPlayerEvent.Type.READY_TO_PLAY -> {} } } diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/service/playercontroller/RadioHandler.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/service/playercontroller/RadioHandler.kt index 92b95166..adfb0098 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/service/playercontroller/RadioHandler.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/service/playercontroller/RadioHandler.kt @@ -12,12 +12,20 @@ import com.toasterofbread.spmp.platform.playerservice.PlayerService // Radio continuation will be added if the amount of remaining songs (including current) falls below this private const val RADIO_MIN_LENGTH: Int = 10 -class RadioHandler(val player: PlayerServicePlayer, val context: AppContext) { - val instance: RadioInstance = object : RadioInstance(context) { - override suspend fun onLoadCompleted(result: RadioInstance.LoadResult, is_continuation: Boolean) { - onRadioLoadCompleted(result, is_continuation) +open class RadioHandler(val player: PlayerServicePlayer, val context: AppContext) { + val instance: RadioInstance = + object : RadioInstance(context) { + override suspend fun onLoadCompleted(result: RadioInstance.LoadResult, is_continuation: Boolean) { + onRadioLoadCompleted(result, is_continuation) + } + + override fun cancelRadio() { + super.cancelRadio() + onRadioCancelled() + } } - } + + open fun onRadioCancelled() {} fun setUndoableRadioState( new_radio_state: RadioState, diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/QueueItems.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/QueueItems.kt index b7b94214..159a07b7 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/QueueItems.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/QueueItems.kt @@ -29,7 +29,7 @@ fun LazyListScope.QueueItems( ) { val items: List = song_items.toList() items(items.size, { items[it].key }) { index -> - val item: QueueTabItem = song_items[index] + val item: QueueTabItem = song_items.getOrNull(index) ?: return@items ReorderableItem(queue_list_state, item.key, item_modifier) { is_dragging -> LaunchedEffect(is_dragging) { if (is_dragging) { diff --git a/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/PlayerListener.desktop.kt b/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/PlayerListener.desktop.kt index ec5beb52..00161616 100644 --- a/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/PlayerListener.desktop.kt +++ b/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/PlayerListener.desktop.kt @@ -16,5 +16,6 @@ actual abstract class PlayerListener actual constructor() { actual open fun onSongAdded(index: Int, song: Song) {} actual open fun onSongRemoved(index: Int, song: Song) {} actual open fun onSongMoved(from: Int, to: Int) {} + actual open fun onRadioCancelRequested() {} actual open fun onEvents() {} } diff --git a/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/playerservice/DesktopExternalPlayerService.kt b/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/playerservice/DesktopExternalPlayerService.kt new file mode 100644 index 00000000..8936400f --- /dev/null +++ b/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/playerservice/DesktopExternalPlayerService.kt @@ -0,0 +1,21 @@ +package com.toasterofbread.spmp.platform.playerservice + +abstract class DesktopExternalPlayerService: ExternalPlayerService(plays_audio = false) { + // private var cancelling_radio = true + // private val server_listener: PlayerListener = + // object : PlayerListener() { + // override fun onRadioCancelRequested() { + // cancelling_radio = true + // radio_instance.cancelRadio() + // cancelling_radio = false + // } + // } + + // override fun onRadioCancelled() { + // if (cancelling_radio) { + // return + // } + // super.onRadioCancelled() + // server.sendRadioCancellationEvent() + // } +} diff --git a/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformExternalPlayerService.desktop.kt b/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformExternalPlayerService.desktop.kt index ba66b1e3..7d3d4d5f 100644 --- a/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformExternalPlayerService.desktop.kt +++ b/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformExternalPlayerService.desktop.kt @@ -2,7 +2,7 @@ package com.toasterofbread.spmp.platform.playerservice import com.toasterofbread.spmp.platform.AppContext -actual class PlatformExternalPlayerService: ExternalPlayerService(), PlayerService { +actual class PlatformExternalPlayerService: DesktopExternalPlayerService(), PlayerService { actual companion object: PlayerServiceCompanion { override fun isServiceRunning(context: AppContext): Boolean = true diff --git a/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformInternalPlayerService.desktop.kt b/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformInternalPlayerService.desktop.kt index 9649a4d2..95db3813 100644 --- a/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformInternalPlayerService.desktop.kt +++ b/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlatformInternalPlayerService.desktop.kt @@ -23,7 +23,7 @@ import ProgramArguments private class PlayerServiceBinder(val service: PlatformInternalPlayerService): PlatformBinder() // actual class PlatformInternalPlayerService: SpMsPlayerService(), PlayerService { -actual class PlatformInternalPlayerService: ExternalPlayerService() { +actual class PlatformInternalPlayerService: DesktopExternalPlayerService() { // actual override val load_state: PlayerServiceLoadState get() = socket_load_state // actual override val connection_error: Throwable? get() = socket_connection_error // actual override val context: AppContext get() = super.context diff --git a/spmp-server b/spmp-server index a4bcc534..a9f7ddc5 160000 --- a/spmp-server +++ b/spmp-server @@ -1 +1 @@ -Subproject commit a4bcc534f32c8a22d4acd504b5742aa2d8cb2151 +Subproject commit a9f7ddc5e1343eff41a50e5e019212e6e7151c9e