diff --git a/playback/core/src/main/kotlin/PlayerState.kt b/playback/core/src/main/kotlin/PlayerState.kt index 365d6ba6d9..4fa79cba0a 100644 --- a/playback/core/src/main/kotlin/PlayerState.kt +++ b/playback/core/src/main/kotlin/PlayerState.kt @@ -7,9 +7,9 @@ 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.MediaStream 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 import org.jellyfin.playback.core.model.PositionInfo @@ -100,7 +100,7 @@ class MutablePlayerState( _videoSize.value = VideoSize(width, height) } - override fun onMediaStreamEnd(mediaStream: MediaStream) = Unit + override fun onMediaStreamEnd(mediaStream: PlayableMediaStream) = Unit }) queue = DefaultPlayerQueueState(this, scope, backendService) diff --git a/playback/core/src/main/kotlin/backend/BackendService.kt b/playback/core/src/main/kotlin/backend/BackendService.kt index 3f9f7b09d7..756964eb75 100644 --- a/playback/core/src/main/kotlin/backend/BackendService.kt +++ b/playback/core/src/main/kotlin/backend/BackendService.kt @@ -1,6 +1,6 @@ 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.PlayState /** @@ -42,7 +42,7 @@ class BackendService { callListeners { onVideoSizeChange(width, height) } } - override fun onMediaStreamEnd(mediaStream: MediaStream) { + override fun onMediaStreamEnd(mediaStream: PlayableMediaStream) { callListeners { onMediaStreamEnd(mediaStream) } } } diff --git a/playback/core/src/main/kotlin/backend/PlayerBackend.kt b/playback/core/src/main/kotlin/backend/PlayerBackend.kt index 57d8a0e7ee..bf16aca4ef 100644 --- a/playback/core/src/main/kotlin/backend/PlayerBackend.kt +++ b/playback/core/src/main/kotlin/backend/PlayerBackend.kt @@ -1,6 +1,7 @@ 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.support.PlaySupportReport import kotlin.time.Duration @@ -20,8 +21,8 @@ interface PlayerBackend { // Mutation - fun prepareStream(stream: MediaStream) - fun playStream(stream: MediaStream) + fun prepareStream(stream: PlayableMediaStream) + fun playStream(stream: PlayableMediaStream) fun play() fun pause() diff --git a/playback/core/src/main/kotlin/backend/PlayerBackendEventListener.kt b/playback/core/src/main/kotlin/backend/PlayerBackendEventListener.kt index 85568f8964..72d456e604 100644 --- a/playback/core/src/main/kotlin/backend/PlayerBackendEventListener.kt +++ b/playback/core/src/main/kotlin/backend/PlayerBackendEventListener.kt @@ -1,10 +1,10 @@ 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.PlayState interface PlayerBackendEventListener { fun onPlayStateChange(state: PlayState) fun onVideoSizeChange(width: Int, height: Int) - fun onMediaStreamEnd(mediaStream: MediaStream) + fun onMediaStreamEnd(mediaStream: PlayableMediaStream) } diff --git a/playback/core/src/main/kotlin/mediastream/MediaStream.kt b/playback/core/src/main/kotlin/mediastream/MediaStream.kt index fdd49ca1d0..55d0c264b2 100644 --- a/playback/core/src/main/kotlin/mediastream/MediaStream.kt +++ b/playback/core/src/main/kotlin/mediastream/MediaStream.kt @@ -2,14 +2,40 @@ package org.jellyfin.playback.core.mediastream import org.jellyfin.playback.core.queue.item.QueueEntry -data class MediaStream( - val identifier: String, +interface MediaStream { + val identifier: String + val conversionMethod: MediaConversionMethod + val container: MediaStreamContainer + val tracks: Collection +} + +data class BasicMediaStream( + override val identifier: String, + override val conversionMethod: MediaConversionMethod, + override val container: MediaStreamContainer, + override val tracks: Collection, +) : MediaStream { + fun toPlayableMediaStream( + queueEntry: QueueEntry, + url: String, + ) = PlayableMediaStream( + identifier = identifier, + conversionMethod = conversionMethod, + container = container, + tracks = tracks, + queueEntry = queueEntry, + url = url, + ) +} + +data class PlayableMediaStream( + override val identifier: String, + override val conversionMethod: MediaConversionMethod, + override val container: MediaStreamContainer, + override val tracks: Collection, val queueEntry: QueueEntry, - val conversionMethod: MediaConversionMethod, val url: String, - val container: MediaStreamContainer, - val tracks: Collection, -) +) : MediaStream data class MediaStreamContainer( val format: String, diff --git a/playback/core/src/main/kotlin/mediastream/MediaStreamResolver.kt b/playback/core/src/main/kotlin/mediastream/MediaStreamResolver.kt index c9815a65f3..a4acda3dcb 100644 --- a/playback/core/src/main/kotlin/mediastream/MediaStreamResolver.kt +++ b/playback/core/src/main/kotlin/mediastream/MediaStreamResolver.kt @@ -1,13 +1,17 @@ package org.jellyfin.playback.core.mediastream import org.jellyfin.playback.core.queue.item.QueueEntry +import org.jellyfin.playback.core.support.PlaySupportReport /** * Determine the media stream for a given queue item. */ interface MediaStreamResolver { /** - * @return [MediaStream] or null if no stream can be determined by this resolver + * @return [PlayableMediaStream] or null if no stream can be determined by this resolver */ - suspend fun getStream(queueEntry: QueueEntry): MediaStream? + suspend fun getStream( + queueEntry: QueueEntry, + testStream: (stream: MediaStream) -> PlaySupportReport, + ): PlayableMediaStream? } diff --git a/playback/core/src/main/kotlin/mediastream/MediaStreamState.kt b/playback/core/src/main/kotlin/mediastream/MediaStreamState.kt index 4cd6348ad5..4bc06a321d 100644 --- a/playback/core/src/main/kotlin/mediastream/MediaStreamState.kt +++ b/playback/core/src/main/kotlin/mediastream/MediaStreamState.kt @@ -7,14 +7,15 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.withContext +import kotlinx.coroutines.plus import org.jellyfin.playback.core.PlayerState import org.jellyfin.playback.core.backend.BackendService +import org.jellyfin.playback.core.backend.PlayerBackend import timber.log.Timber interface MediaStreamState { - val current: StateFlow - val next: StateFlow + val current: StateFlow + val next: StateFlow } class DefaultMediaStreamState( @@ -23,63 +24,56 @@ class DefaultMediaStreamState( private val mediaStreamResolvers: Collection, private val backendService: BackendService, ) : MediaStreamState { - private val _current = MutableStateFlow(null) - override val current: StateFlow get() = _current.asStateFlow() + private val _current = MutableStateFlow(null) + override val current: StateFlow get() = _current.asStateFlow() - private val _next = MutableStateFlow(null) - override val next: StateFlow get() = _next.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") + val backend = requireNotNull(backendService.backend) if (entry == null) { - setCurrent(null) + backend.setCurrent(null) } else { - val streamResult = runCatching { - mediaStreamResolvers.firstNotNullOfOrNull { resolver -> resolver.getStream(entry) } + val stream = mediaStreamResolvers.firstNotNullOfOrNull { resolver -> + runCatching { + resolver.getStream(entry, backend::supportsStream) + }.onFailure { + Timber.e(it, "Media stream resolver failed for $entry") + }.getOrNull() } - val stream = streamResult.getOrNull() - when { - streamResult.isFailure -> Timber.e(streamResult.exceptionOrNull(), "Media stream resolver failed for $entry") - stream == null -> Timber.e("Unable to resolve stream for entry $entry") - else -> { - if (!canPlayStream(stream)) { - Timber.w("Playback of the received media stream for $entry is not supported") - } - setCurrent(stream) + if (stream == null) { + Timber.e("Unable to resolve stream for entry $entry") + + // TODO: Somehow notify the user that we skipped an unplayable entry + if (state.queue.peekNext() != null) { + state.queue.next(usePlaybackOrder = true, useRepeatMode = false) + } else { + backend.setCurrent(null) } + } else { + backend.setCurrent(stream) } } - }.launchIn(coroutineScope) + }.launchIn(coroutineScope + Dispatchers.Main) // TODO Register some kind of event when $current item is at -30 seconds to setNext() } - private suspend fun canPlayStream(stream: MediaStream) = withContext(Dispatchers.Main) { - backendService.backend?.supportsStream(stream)?.canPlay == true - } - - private suspend fun setCurrent(stream: MediaStream?) { + private fun PlayerBackend.setCurrent(stream: PlayableMediaStream?) { Timber.d("Current stream changed to $stream") - val backend = requireNotNull(backendService.backend) - _current.value = stream - withContext(Dispatchers.Main) { - if (stream == null) backend.stop() - else backend.playStream(stream) - } + if (stream == null) stop() + else playStream(stream) } - private suspend fun setNext(stream: MediaStream) { - val backend = requireNotNull(backendService.backend) - + private fun PlayerBackend.setNext(stream: PlayableMediaStream) { _current.value = stream - - withContext(Dispatchers.Main) { - backend.prepareStream(stream) - } + prepareStream(stream) } } diff --git a/playback/core/src/main/kotlin/queue/PlayerQueueState.kt b/playback/core/src/main/kotlin/queue/PlayerQueueState.kt index d2fffc918a..77a310127e 100644 --- a/playback/core/src/main/kotlin/queue/PlayerQueueState.kt +++ b/playback/core/src/main/kotlin/queue/PlayerQueueState.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.launch import org.jellyfin.playback.core.PlayerState import org.jellyfin.playback.core.backend.BackendService import org.jellyfin.playback.core.backend.PlayerBackendEventListener -import org.jellyfin.playback.core.mediastream.MediaStream +import org.jellyfin.playback.core.mediastream.PlayableMediaStream import org.jellyfin.playback.core.model.PlayState import org.jellyfin.playback.core.model.PlaybackOrder import org.jellyfin.playback.core.model.RepeatMode @@ -80,7 +80,7 @@ class DefaultPlayerQueueState( override fun onPlayStateChange(state: PlayState) = Unit override fun onVideoSizeChange(width: Int, height: Int) = Unit - override fun onMediaStreamEnd(mediaStream: MediaStream) { + override fun onMediaStreamEnd(mediaStream: PlayableMediaStream) { // TODO: Find position based on $mediaStream instead // TODO: This doesn't work as expected coroutineScope.launch { next(usePlaybackOrder = true, useRepeatMode = true) } diff --git a/playback/exoplayer/src/main/kotlin/ExoPlayerBackend.kt b/playback/exoplayer/src/main/kotlin/ExoPlayerBackend.kt index f9a0fb439b..f5b3002033 100644 --- a/playback/exoplayer/src/main/kotlin/ExoPlayerBackend.kt +++ b/playback/exoplayer/src/main/kotlin/ExoPlayerBackend.kt @@ -10,6 +10,7 @@ import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.video.VideoSize 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.model.PlayState import org.jellyfin.playback.core.model.PositionInfo import org.jellyfin.playback.core.support.PlaySupportReport @@ -23,7 +24,7 @@ import kotlin.time.Duration.Companion.milliseconds class ExoPlayerBackend( private val context: Context, ) : BasePlayerBackend() { - private var currentStream: MediaStream? = null + private var currentStream: PlayableMediaStream? = null private val exoPlayer by lazy { val renderersFactory = DefaultRenderersFactory(context).apply { @@ -74,7 +75,7 @@ class ExoPlayerBackend( stream: MediaStream ): PlaySupportReport = exoPlayer.getPlaySupportReport(stream.toFormat()) - override fun prepareStream(stream: MediaStream) { + override fun prepareStream(stream: PlayableMediaStream) { val mediaItem = MediaItem.Builder().apply { setTag(stream) setMediaId(stream.hashCode().toString()) @@ -89,7 +90,7 @@ class ExoPlayerBackend( exoPlayer.prepare() } - override fun playStream(stream: MediaStream) { + override fun playStream(stream: PlayableMediaStream) { if (currentStream == stream) return currentStream = stream diff --git a/playback/jellyfin/src/main/kotlin/JellyfinPlugin.kt b/playback/jellyfin/src/main/kotlin/JellyfinPlugin.kt index d895c87d8e..8f30a90214 100644 --- a/playback/jellyfin/src/main/kotlin/JellyfinPlugin.kt +++ b/playback/jellyfin/src/main/kotlin/JellyfinPlugin.kt @@ -7,6 +7,9 @@ import org.jellyfin.playback.jellyfin.playsession.PlaySessionSocketService import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.sockets.SocketInstance import org.jellyfin.sdk.model.api.DeviceProfile +import org.jellyfin.sdk.model.api.DlnaProfileType +import org.jellyfin.sdk.model.api.EncodingContext +import org.jellyfin.sdk.model.api.TranscodingProfile fun jellyfinPlugin( api: ApiClient, @@ -20,13 +23,23 @@ fun jellyfinPlugin( responseProfiles = emptyList(), subtitleProfiles = emptyList(), supportedMediaTypes = "", - transcodingProfiles = emptyList(), + // Add at least one transcoding profile so the server returns a value + // for "SupportsTranscoding" based on the user policy + // We don't actually use this profile in the client + transcodingProfiles = listOf( + TranscodingProfile( + type = DlnaProfileType.AUDIO, + context = EncodingContext.STREAMING, + protocol = "hls", + container = "mp3", + audioCodec = "mp3", + videoCodec = "", + conditions = emptyList() + ) + ), xmlRootAttributes = emptyList(), ) - provide(AudioMediaStreamResolver(api, profile).apply { - // TODO: Remove once we have a proper device profile - forceDirectPlay = true - }) + provide(AudioMediaStreamResolver(api, profile)) val playSessionService = PlaySessionService(api) provide(playSessionService) diff --git a/playback/jellyfin/src/main/kotlin/mediastream/AudioMediaStreamResolver.kt b/playback/jellyfin/src/main/kotlin/mediastream/AudioMediaStreamResolver.kt index 673e5aa764..4d6cd90ac3 100644 --- a/playback/jellyfin/src/main/kotlin/mediastream/AudioMediaStreamResolver.kt +++ b/playback/jellyfin/src/main/kotlin/mediastream/AudioMediaStreamResolver.kt @@ -1,13 +1,16 @@ package org.jellyfin.playback.jellyfin.mediastream +import org.jellyfin.playback.core.mediastream.BasicMediaStream import org.jellyfin.playback.core.mediastream.MediaConversionMethod import org.jellyfin.playback.core.mediastream.MediaStream +import org.jellyfin.playback.core.mediastream.MediaStreamContainer +import org.jellyfin.playback.core.mediastream.PlayableMediaStream import org.jellyfin.playback.core.queue.item.QueueEntry +import org.jellyfin.playback.core.support.PlaySupportReport import org.jellyfin.playback.jellyfin.queue.item.BaseItemDtoUserQueueEntry import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.exception.MissingBaseUrlException import org.jellyfin.sdk.api.client.extensions.audioApi -import org.jellyfin.sdk.api.client.util.UrlBuilder +import org.jellyfin.sdk.api.client.extensions.dynamicHlsApi import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.DeviceProfile @@ -15,72 +18,94 @@ class AudioMediaStreamResolver( val api: ApiClient, val profile: DeviceProfile, ) : JellyfinStreamResolver(api, profile) { - /** - * Force direct play when enabled, even when we know it will fail. - */ - var forceDirectPlay = false + companion object { + private val REMUX_CONTAINERS = arrayOf("mp3", "ogg", "mkv") + private const val REMUX_SEGMENT_CONTAINER = "mp3" + } + + private fun MediaInfo.getDirectPlayStream() = BasicMediaStream( + identifier = playSessionId, + conversionMethod = MediaConversionMethod.None, + container = getMediaStreamContainer(), + tracks = getTracks() + ) + + private fun MediaInfo.getRemuxStream(container: String) = BasicMediaStream( + identifier = playSessionId, + conversionMethod = MediaConversionMethod.Remux, + container = MediaStreamContainer( + format = container + ), + tracks = getTracks() + ) - override suspend fun getStream(queueEntry: QueueEntry): MediaStream? { + private fun MediaInfo.getTranscodeStream() = BasicMediaStream( + identifier = playSessionId, + conversionMethod = MediaConversionMethod.Transcode, + // The server doesn't provide us with the transcode information os we return mock data + container = MediaStreamContainer(format = "unknown"), + tracks = emptyList() + ) + + override suspend fun getStream( + queueEntry: QueueEntry, + testStream: (stream: MediaStream) -> PlaySupportReport, + ): PlayableMediaStream? { if (queueEntry !is BaseItemDtoUserQueueEntry) return null if (queueEntry.baseItem.type != BaseItemKind.AUDIO) return null val mediaInfo = getPlaybackInfo(queueEntry.baseItem) - val conversionMethod = when { - // Direct play - mediaInfo.mediaSource.supportsDirectPlay || forceDirectPlay -> MediaConversionMethod.None - // Remux (Direct stream) - mediaInfo.mediaSource.supportsDirectStream -> MediaConversionMethod.Remux - // Transcode - mediaInfo.mediaSource.supportsTranscoding -> MediaConversionMethod.Transcode - else -> error("Unable to find a suitable playback method for media") - } - - val url = when (conversionMethod) { - // Direct play - is MediaConversionMethod.None -> { - api.audioApi.getAudioStreamUrl( + // Test for direct play support + val directPlayStream = mediaInfo.getDirectPlayStream() + if (testStream(directPlayStream).canPlay) { + return directPlayStream.toPlayableMediaStream( + queueEntry = queueEntry, + url = api.audioApi.getAudioStreamUrl( itemId = queueEntry.baseItem.id, mediaSourceId = mediaInfo.mediaSource.id, playSessionId = mediaInfo.playSessionId, static = true, ) - } - // Remux (Direct stream) - is MediaConversionMethod.Remux -> { - val container = requireNotNull(mediaInfo.mediaSource.container) { - "MediaSource supports direct stream but container is null" + ) + } + + // Try remuxing + if (mediaInfo.mediaSource.supportsDirectStream) { + for (container in REMUX_CONTAINERS) { + val remuxStream = mediaInfo.getRemuxStream(container) + if (testStream(remuxStream).canPlay) { + return remuxStream.toPlayableMediaStream( + queueEntry = queueEntry, + url = api.audioApi.getAudioStreamByContainerUrl( + itemId = queueEntry.baseItem.id, + mediaSourceId = mediaInfo.mediaSource.id, + playSessionId = mediaInfo.playSessionId, + container = container, + ) + ) } + } + } + + // Fallback to provided transcode + if (mediaInfo.mediaSource.supportsTranscoding) { + val transcodeStream = mediaInfo.getTranscodeStream() - api.audioApi.getAudioStreamByContainerUrl( + // Skip testing transcode stream because we lack the information to do so + return transcodeStream.toPlayableMediaStream( + queueEntry = queueEntry, + url = api.dynamicHlsApi.getMasterHlsAudioPlaylistUrl( itemId = queueEntry.baseItem.id, - mediaSourceId = mediaInfo.mediaSource.id, + mediaSourceId = requireNotNull(mediaInfo.mediaSource.id), playSessionId = mediaInfo.playSessionId, - container = container, + tag = mediaInfo.mediaSource.eTag, + segmentContainer = REMUX_SEGMENT_CONTAINER, ) - } - // Transcode - is MediaConversionMethod.Transcode -> { - val url = requireNotNull(mediaInfo.mediaSource.transcodingUrl) { - "MediaSource supports transcoding but transcodingUrl is null" - } - - // TODO Use api.createUrl() with SDK 1.5 - UrlBuilder.buildUrl( - api.baseUrl ?: throw MissingBaseUrlException(), - url, - ignorePathParameters = true, - ) - } + ) } - return MediaStream( - identifier = mediaInfo.playSessionId, - queueEntry = queueEntry, - conversionMethod = conversionMethod, - url = url, - container = mediaInfo.getMediaStreamContainer(), - tracks = mediaInfo.getTracks() - ) + // Unable to find a suitable stream, return + return null } } diff --git a/playback/jellyfin/src/main/kotlin/mediastream/UniversalAudioMediaStreamResolver.kt b/playback/jellyfin/src/main/kotlin/mediastream/UniversalAudioMediaStreamResolver.kt index c53e08f669..031642193a 100644 --- a/playback/jellyfin/src/main/kotlin/mediastream/UniversalAudioMediaStreamResolver.kt +++ b/playback/jellyfin/src/main/kotlin/mediastream/UniversalAudioMediaStreamResolver.kt @@ -2,7 +2,9 @@ package org.jellyfin.playback.jellyfin.mediastream import org.jellyfin.playback.core.mediastream.MediaConversionMethod import org.jellyfin.playback.core.mediastream.MediaStream +import org.jellyfin.playback.core.mediastream.PlayableMediaStream import org.jellyfin.playback.core.queue.item.QueueEntry +import org.jellyfin.playback.core.support.PlaySupportReport import org.jellyfin.playback.jellyfin.queue.item.BaseItemDtoUserQueueEntry import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.universalAudioApi @@ -13,7 +15,10 @@ class UniversalAudioMediaStreamResolver( val api: ApiClient, val profile: DeviceProfile, ) : JellyfinStreamResolver(api, profile) { - override suspend fun getStream(queueEntry: QueueEntry): MediaStream? { + override suspend fun getStream( + queueEntry: QueueEntry, + testStream: (stream: MediaStream) -> PlaySupportReport, + ): PlayableMediaStream? { if (queueEntry !is BaseItemDtoUserQueueEntry) return null if (queueEntry.baseItem.type != BaseItemKind.AUDIO) return null @@ -35,13 +40,13 @@ class UniversalAudioMediaStreamResolver( audioCodec = "mp3", ) - return MediaStream( + return PlayableMediaStream( identifier = mediaInfo.playSessionId, queueEntry = queueEntry, conversionMethod = MediaConversionMethod.None, url = url, container = mediaInfo.getMediaStreamContainer(), tracks = mediaInfo.getTracks() - ) + ).takeIf { stream -> testStream(stream).canPlay } } } diff --git a/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt b/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt index 4c856242fd..9932ec0b0c 100644 --- a/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt +++ b/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.playback.core.mediastream.MediaConversionMethod -import org.jellyfin.playback.core.mediastream.MediaStream +import org.jellyfin.playback.core.mediastream.PlayableMediaStream import org.jellyfin.playback.core.model.PlayState import org.jellyfin.playback.core.model.RepeatMode import org.jellyfin.playback.core.plugin.PlayerService @@ -25,7 +25,7 @@ import org.jellyfin.sdk.model.api.RepeatMode as SdkRepeatMode class PlaySessionService( private val api: ApiClient, ) : PlayerService() { - private var reportedStream: MediaStream? = null + private var reportedStream: PlayableMediaStream? = null override suspend fun onInitialize() { state.streams.current.onEach { stream -> onMediaStreamChange(stream) }.launchIn(coroutineScope) @@ -40,7 +40,7 @@ class PlaySessionService( }.launchIn(coroutineScope) } - private val MediaStream.baseItem + private val PlayableMediaStream.baseItem get() = when (val entry = queueEntry) { is BaseItemDtoUserQueueEntry -> entry.baseItem else -> null @@ -66,7 +66,7 @@ class PlaySessionService( } } - private fun onMediaStreamChange(stream: MediaStream?) { + private fun onMediaStreamChange(stream: PlayableMediaStream?) { reportedStream = stream onStart() } @@ -98,7 +98,7 @@ class PlaySessionService( .map { QueueItem(id = it.baseItem.id, playlistItemId = it.baseItem.playlistItemId) } } - private suspend fun sendStreamStart(stream: MediaStream) { + private suspend fun sendStreamStart(stream: PlayableMediaStream) { val item = stream.baseItem ?: return api.playStateApi.reportPlaybackStart(PlaybackStartInfo( itemId = item.id, @@ -116,7 +116,7 @@ class PlaySessionService( )) } - private suspend fun sendStreamUpdate(stream: MediaStream) { + private suspend fun sendStreamUpdate(stream: PlayableMediaStream) { val item = stream.baseItem ?: return api.playStateApi.reportPlaybackProgress(PlaybackProgressInfo( itemId = item.id, @@ -134,7 +134,7 @@ class PlaySessionService( )) } - private suspend fun sendStreamStop(stream: MediaStream) { + private suspend fun sendStreamStop(stream: PlayableMediaStream) { val item = stream.baseItem ?: return api.playStateApi.reportPlaybackStopped(PlaybackStopInfo( itemId = item.id,