Skip to content

Commit

Permalink
Send QueueEntry to playback backend
Browse files Browse the repository at this point in the history
  • Loading branch information
nielsvanvelzen committed Jun 10, 2024
1 parent b6a73f7 commit f0554b2
Show file tree
Hide file tree
Showing 6 changed files with 37 additions and 52 deletions.
5 changes: 4 additions & 1 deletion playback/core/src/main/kotlin/PlaybackManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
7 changes: 0 additions & 7 deletions playback/core/src/main/kotlin/PlayerState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,7 +20,6 @@ import kotlin.time.Duration

interface PlayerState {
val queue: PlayerQueueState
val streams: MediaStreamState
val volume: PlayerVolumeState
val playState: StateFlow<PlayState>
val speed: StateFlow<Float>
Expand Down Expand Up @@ -65,11 +61,9 @@ interface PlayerState {
class MutablePlayerState(
private val options: PlaybackManagerOptions,
scope: CoroutineScope,
mediaStreamResolvers: Collection<MediaStreamResolver>,
private val backendService: BackendService,
) : PlayerState {
override val queue: PlayerQueueState
override val streams: MediaStreamState
override val volume: PlayerVolumeState

private val _playState = MutableStateFlow(PlayState.STOPPED)
Expand Down Expand Up @@ -104,7 +98,6 @@ class MutablePlayerState(
})

queue = DefaultPlayerQueueState(this, scope, backendService)
streams = DefaultMediaStreamState(this, scope, mediaStreamResolvers, backendService)
volume = options.playerVolumeState
}

Expand Down
6 changes: 3 additions & 3 deletions playback/core/src/main/kotlin/backend/PlayerBackend.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
Expand Down
58 changes: 21 additions & 37 deletions playback/core/src/main/kotlin/mediastream/MediaStreamState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<PlayableMediaStream?>
val next: StateFlow<PlayableMediaStream?>
}

class DefaultMediaStreamState(
internal class MediaStreamState(
state: PlayerState,
coroutineScope: CoroutineScope,
private val mediaStreamResolvers: Collection<MediaStreamResolver>,
private val backendService: BackendService,
) : MediaStreamState {
private val _current = MutableStateFlow<PlayableMediaStream?>(null)
override val current: StateFlow<PlayableMediaStream?> get() = _current.asStateFlow()

private val _next = MutableStateFlow<PlayableMediaStream?>(null)
override val next: StateFlow<PlayableMediaStream?> get() = _next.asStateFlow()

) {
init {
state.queue.entry.onEach { entry ->
Timber.d("Queue entry changed to $entry")
Expand All @@ -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
Expand All @@ -50,35 +38,31 @@ class DefaultMediaStreamState(
} else {
backend.setCurrent(null)
}
} else {
backend.setCurrent(stream)
}
}
}.launchIn(coroutineScope + Dispatchers.Main)

// 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)
}
}
10 changes: 7 additions & 3 deletions playback/exoplayer/src/main/kotlin/ExoPlayerBackend.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand All @@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down

0 comments on commit f0554b2

Please sign in to comment.