From 5d7e2dbba280e7ef004d1576aed6204fbd6f4c6d Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 9 Jun 2024 14:30:29 +0200 Subject: [PATCH] Send QueueEntry to playback backend --- .../core/src/main/kotlin/PlaybackManager.kt | 5 +- playback/core/src/main/kotlin/PlayerState.kt | 7 --- .../src/main/kotlin/backend/PlayerBackend.kt | 6 +- .../kotlin/mediastream/MediaStreamState.kt | 58 +++++++------------ .../src/main/kotlin/ExoPlayerBackend.kt | 10 +++- .../kotlin/playsession/PlaySessionService.kt | 3 +- 6 files changed, 37 insertions(+), 52 deletions(-) diff --git a/playback/core/src/main/kotlin/PlaybackManager.kt b/playback/core/src/main/kotlin/PlaybackManager.kt index af538eb779..bb1f67ac22 100644 --- a/playback/core/src/main/kotlin/PlaybackManager.kt +++ b/playback/core/src/main/kotlin/PlaybackManager.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.cancel import org.jellyfin.playback.core.backend.BackendService import org.jellyfin.playback.core.backend.PlayerBackend import org.jellyfin.playback.core.mediastream.MediaStreamResolver +import org.jellyfin.playback.core.mediastream.MediaStreamState import org.jellyfin.playback.core.plugin.PlayerService import timber.log.Timber import kotlin.reflect.KClass @@ -26,12 +27,14 @@ class PlaybackManager internal constructor( val state: PlayerState = MutablePlayerState( options = options, scope = CoroutineScope(Job(job)), - mediaStreamResolvers = mediaStreamResolvers, backendService = backendService, ) init { services.forEach { it.initialize(this, state, Job(job)) } + + // FIXME: This should be more integrated in the future + MediaStreamState(state, CoroutineScope(job), mediaStreamResolvers, backendService) } fun addService(service: PlayerService) { diff --git a/playback/core/src/main/kotlin/PlayerState.kt b/playback/core/src/main/kotlin/PlayerState.kt index 4fa79cba0a..fd4c04bb07 100644 --- a/playback/core/src/main/kotlin/PlayerState.kt +++ b/playback/core/src/main/kotlin/PlayerState.kt @@ -6,9 +6,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import org.jellyfin.playback.core.backend.BackendService import org.jellyfin.playback.core.backend.PlayerBackendEventListener -import org.jellyfin.playback.core.mediastream.DefaultMediaStreamState -import org.jellyfin.playback.core.mediastream.MediaStreamResolver -import org.jellyfin.playback.core.mediastream.MediaStreamState import org.jellyfin.playback.core.mediastream.PlayableMediaStream import org.jellyfin.playback.core.model.PlayState import org.jellyfin.playback.core.model.PlaybackOrder @@ -23,7 +20,6 @@ import kotlin.time.Duration interface PlayerState { val queue: PlayerQueueState - val streams: MediaStreamState val volume: PlayerVolumeState val playState: StateFlow val speed: StateFlow @@ -65,11 +61,9 @@ interface PlayerState { class MutablePlayerState( private val options: PlaybackManagerOptions, scope: CoroutineScope, - mediaStreamResolvers: Collection, private val backendService: BackendService, ) : PlayerState { override val queue: PlayerQueueState - override val streams: MediaStreamState override val volume: PlayerVolumeState private val _playState = MutableStateFlow(PlayState.STOPPED) @@ -104,7 +98,6 @@ class MutablePlayerState( }) queue = DefaultPlayerQueueState(this, scope, backendService) - streams = DefaultMediaStreamState(this, scope, mediaStreamResolvers, backendService) volume = options.playerVolumeState } diff --git a/playback/core/src/main/kotlin/backend/PlayerBackend.kt b/playback/core/src/main/kotlin/backend/PlayerBackend.kt index f72f404552..bc19119f5c 100644 --- a/playback/core/src/main/kotlin/backend/PlayerBackend.kt +++ b/playback/core/src/main/kotlin/backend/PlayerBackend.kt @@ -1,8 +1,8 @@ package org.jellyfin.playback.core.backend import org.jellyfin.playback.core.mediastream.MediaStream -import org.jellyfin.playback.core.mediastream.PlayableMediaStream import org.jellyfin.playback.core.model.PositionInfo +import org.jellyfin.playback.core.queue.QueueEntry import org.jellyfin.playback.core.support.PlaySupportReport import org.jellyfin.playback.core.ui.PlayerSubtitleView import org.jellyfin.playback.core.ui.PlayerSurfaceView @@ -27,8 +27,8 @@ interface PlayerBackend { // Mutation - fun prepareStream(stream: PlayableMediaStream) - fun playStream(stream: PlayableMediaStream) + fun prepareItem(item: QueueEntry) + fun playItem(item: QueueEntry) fun play() fun pause() diff --git a/playback/core/src/main/kotlin/mediastream/MediaStreamState.kt b/playback/core/src/main/kotlin/mediastream/MediaStreamState.kt index e9b547f776..f82173f5e6 100644 --- a/playback/core/src/main/kotlin/mediastream/MediaStreamState.kt +++ b/playback/core/src/main/kotlin/mediastream/MediaStreamState.kt @@ -2,9 +2,6 @@ package org.jellyfin.playback.core.mediastream import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.plus @@ -14,23 +11,12 @@ import org.jellyfin.playback.core.backend.PlayerBackend import org.jellyfin.playback.core.queue.QueueEntry import timber.log.Timber -interface MediaStreamState { - val current: StateFlow - val next: StateFlow -} - -class DefaultMediaStreamState( +internal class MediaStreamState( state: PlayerState, coroutineScope: CoroutineScope, private val mediaStreamResolvers: Collection, private val backendService: BackendService, -) : MediaStreamState { - private val _current = MutableStateFlow(null) - override val current: StateFlow get() = _current.asStateFlow() - - private val _next = MutableStateFlow(null) - override val next: StateFlow get() = _next.asStateFlow() - +) { init { state.queue.entry.onEach { entry -> Timber.d("Queue entry changed to $entry") @@ -39,9 +25,11 @@ class DefaultMediaStreamState( if (entry == null) { backend.setCurrent(null) } else { - val stream = entry.getOrComputeMediaStream(backend) + val hasMediaStream = entry.ensureMediaStream(backend) - if (stream == null) { + if (hasMediaStream) { + backend.setCurrent(entry) + } else { Timber.e("Unable to resolve stream for entry $entry") // TODO: Somehow notify the user that we skipped an unplayable entry @@ -50,8 +38,6 @@ class DefaultMediaStreamState( } else { backend.setCurrent(null) } - } else { - backend.setCurrent(stream) } } }.launchIn(coroutineScope + Dispatchers.Main) @@ -59,26 +45,24 @@ class DefaultMediaStreamState( // TODO Register some kind of event when $current item is at -30 seconds to setNext() } - private suspend fun QueueEntry.getOrComputeMediaStream( + private suspend fun QueueEntry.ensureMediaStream( backend: PlayerBackend, - ): PlayableMediaStream? = mediaStream ?: mediaStreamResolvers.firstNotNullOfOrNull { resolver -> - runCatching { - resolver.getStream(this, backend::supportsStream) - }.onFailure { - Timber.e(it, "Media stream resolver failed for $this") - }.getOrNull() - }.also { mediaStream = it } + ): Boolean { + mediaStream = mediaStream ?: mediaStreamResolvers.firstNotNullOfOrNull { resolver -> + runCatching { + resolver.getStream(this, backend::supportsStream) + }.onFailure { + Timber.e(it, "Media stream resolver failed for $this") + }.getOrNull() + } - private fun PlayerBackend.setCurrent(stream: PlayableMediaStream?) { - Timber.d("Current stream changed to $stream") - _current.value = stream - - if (stream == null) stop() - else playStream(stream) + return mediaStream != null } - private fun PlayerBackend.setNext(stream: PlayableMediaStream) { - _current.value = stream - prepareStream(stream) + private fun PlayerBackend.setCurrent(item: QueueEntry?) { + Timber.d("Current item changed to $item") + + if (item == null) stop() + else playItem(item) } } diff --git a/playback/exoplayer/src/main/kotlin/ExoPlayerBackend.kt b/playback/exoplayer/src/main/kotlin/ExoPlayerBackend.kt index ea7f25b388..015288458e 100644 --- a/playback/exoplayer/src/main/kotlin/ExoPlayerBackend.kt +++ b/playback/exoplayer/src/main/kotlin/ExoPlayerBackend.kt @@ -23,8 +23,10 @@ import androidx.media3.ui.SubtitleView import org.jellyfin.playback.core.backend.BasePlayerBackend import org.jellyfin.playback.core.mediastream.MediaStream import org.jellyfin.playback.core.mediastream.PlayableMediaStream +import org.jellyfin.playback.core.mediastream.mediaStream import org.jellyfin.playback.core.model.PlayState import org.jellyfin.playback.core.model.PositionInfo +import org.jellyfin.playback.core.queue.QueueEntry import org.jellyfin.playback.core.support.PlaySupportReport import org.jellyfin.playback.core.ui.PlayerSubtitleView import org.jellyfin.playback.core.ui.PlayerSurfaceView @@ -127,7 +129,8 @@ class ExoPlayerBackend( } } - override fun prepareStream(stream: PlayableMediaStream) { + override fun prepareItem(item: QueueEntry) { + val stream = requireNotNull(item.mediaStream) val mediaItem = MediaItem.Builder().apply { setTag(stream) setMediaId(stream.hashCode().toString()) @@ -142,7 +145,8 @@ class ExoPlayerBackend( exoPlayer.prepare() } - override fun playStream(stream: PlayableMediaStream) { + override fun playItem(item: QueueEntry) { + val stream = requireNotNull(item.mediaStream) if (currentStream == stream) return currentStream = stream @@ -152,7 +156,7 @@ class ExoPlayerBackend( streamIsPrepared = streamIsPrepared || exoPlayer.getMediaItemAt(index).mediaId == stream.hashCode().toString() } - if (!streamIsPrepared) prepareStream(stream) + if (!streamIsPrepared) prepareItem(item) exoPlayer.seekToNextMediaItem() exoPlayer.play() diff --git a/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt b/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt index 5136b0f359..5085d18c7d 100644 --- a/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt +++ b/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.playback.core.mediastream.MediaConversionMethod import org.jellyfin.playback.core.mediastream.PlayableMediaStream +import org.jellyfin.playback.core.mediastream.mediaStream import org.jellyfin.playback.core.model.PlayState import org.jellyfin.playback.core.model.RepeatMode import org.jellyfin.playback.core.plugin.PlayerService @@ -29,7 +30,7 @@ class PlaySessionService( private var reportedStream: PlayableMediaStream? = null override suspend fun onInitialize() { - state.streams.current.onEach { stream -> onMediaStreamChange(stream) }.launchIn(coroutineScope) + state.queue.entry.onEach { item -> onMediaStreamChange(item?.mediaStream) }.launchIn(coroutineScope) state.playState.onEach { playState -> when (playState) {