From deb346b778643eccb35de318217db2edf59b34a6 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 4 Aug 2024 17:27:40 +0200 Subject: [PATCH] Rewrite queuing --- .../ui/playback/AudioNowPlayingFragment.java | 2 +- .../rewrite/PlaybackRewriteFragment.kt | 12 +- .../playback/rewrite/RewriteMediaManager.kt | 87 +++++------ .../core/src/main/kotlin/PlaybackManager.kt | 9 +- .../src/main/kotlin/PlaybackManagerBuilder.kt | 9 +- playback/core/src/main/kotlin/PlayerState.kt | 18 +-- ...iaStreamState.kt => MediaStreamService.kt} | 22 ++- .../core/src/main/kotlin/queue/EmptyQueue.kt | 6 - playback/core/src/main/kotlin/queue/Queue.kt | 85 ++++++++++- .../{PlayerQueueState.kt => QueueService.kt} | 137 +++++++++--------- .../queue/order/DefaultOrderIndexProvider.kt | 5 +- .../kotlin/queue/order/OrderIndexProvider.kt | 9 +- .../queue/order/RandomOrderIndexProvider.kt | 5 +- .../queue/order/ShuffleOrderIndexProvider.kt | 7 +- .../PagedQueueSupplier.kt} | 14 +- .../kotlin/queue/supplier/QueueSupplier.kt | 16 ++ .../SequenceQueueSupplier.kt} | 6 +- .../kotlin/playsession/PlaySessionService.kt | 9 +- .../playsession/PlaySessionSocketService.kt | 5 +- ...bumQueue.kt => AudioAlbumQueueSupplier.kt} | 6 +- ...eue.kt => AudioInstantMixQueueSupplier.kt} | 6 +- ...ackQueue.kt => AudioTrackQueueSupplier.kt} | 6 +- ...pisodeQueue.kt => EpisodeQueueSupplier.kt} | 6 +- .../src/main/kotlin/MediaSessionPlayer.kt | 17 ++- .../src/main/kotlin/MediaSessionService.kt | 3 +- 25 files changed, 291 insertions(+), 216 deletions(-) rename playback/core/src/main/kotlin/mediastream/{MediaStreamState.kt => MediaStreamService.kt} (75%) delete mode 100644 playback/core/src/main/kotlin/queue/EmptyQueue.kt rename playback/core/src/main/kotlin/queue/{PlayerQueueState.kt => QueueService.kt} (55%) rename playback/core/src/main/kotlin/queue/{PagedQueue.kt => supplier/PagedQueueSupplier.kt} (67%) create mode 100644 playback/core/src/main/kotlin/queue/supplier/QueueSupplier.kt rename playback/core/src/main/kotlin/queue/{SequenceQueue.kt => supplier/SequenceQueueSupplier.kt} (78%) rename playback/jellyfin/src/main/kotlin/queue/{AudioAlbumQueue.kt => AudioAlbumQueueSupplier.kt} (90%) rename playback/jellyfin/src/main/kotlin/queue/{AudioInstantMixQueue.kt => AudioInstantMixQueueSupplier.kt} (89%) rename playback/jellyfin/src/main/kotlin/queue/{AudioTrackQueue.kt => AudioTrackQueueSupplier.kt} (84%) rename playback/jellyfin/src/main/kotlin/queue/{EpisodeQueue.kt => EpisodeQueueSupplier.kt} (90%) diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/AudioNowPlayingFragment.java b/app/src/main/java/org/jellyfin/androidtv/ui/playback/AudioNowPlayingFragment.java index a1211980c5..cf7eef060e 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/AudioNowPlayingFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/AudioNowPlayingFragment.java @@ -252,7 +252,7 @@ public void onProgress(long pos) { @Override public void onQueueStatusChanged(boolean hasQueue) { - Timber.d("Queue status changed"); + Timber.d("Queue status changed (hasQueue=%s)", hasQueue); if (hasQueue) { loadItem(); if (mediaManager.getValue().isAudioPlayerInitialized()) { diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/PlaybackRewriteFragment.kt b/app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/PlaybackRewriteFragment.kt index 2a2bb63db3..051b2181e4 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/PlaybackRewriteFragment.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/PlaybackRewriteFragment.kt @@ -16,6 +16,7 @@ import org.jellyfin.androidtv.ui.ScreensaverViewModel import org.jellyfin.androidtv.ui.playback.VideoQueueManager import org.jellyfin.playback.core.PlaybackManager import org.jellyfin.playback.core.model.PlayState +import org.jellyfin.playback.core.queue.queue import org.jellyfin.playback.core.ui.PlayerSubtitleView import org.jellyfin.playback.core.ui.PlayerSurfaceView import org.jellyfin.sdk.api.client.ApiClient @@ -43,17 +44,18 @@ class PlaybackRewriteFragment : Fragment() { super.onCreate(savedInstanceState) // Create a queue from the items added to the legacy video queue - val queue = RewriteMediaManager.BaseItemQueue(api) - queue.items.addAll(videoQueueManager.getCurrentVideoQueue()) - Timber.i("Created a queue with ${queue.items.size} items") - playbackManager.state.queue.replaceQueue(queue) + val queueSupplier = RewriteMediaManager.BaseItemQueueSupplier(api) + queueSupplier.items.addAll(videoQueueManager.getCurrentVideoQueue()) + Timber.i("Created a queue with ${queueSupplier.items.size} items") + playbackManager.queue.clear() + playbackManager.queue.addSupplier(queueSupplier) // Set position val position = arguments?.getInt(EXTRA_POSITION) ?: 0 if (position != 0) { lifecycleScope.launch { Timber.i("Skipping to queue item $position") - playbackManager.state.queue.setIndex(position, false) + playbackManager.queue.setIndex(position, false) } } diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/RewriteMediaManager.kt b/app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/RewriteMediaManager.kt index 2020686337..9f4ce8f7fa 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/RewriteMediaManager.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/RewriteMediaManager.kt @@ -23,8 +23,9 @@ import org.jellyfin.playback.core.PlaybackManager import org.jellyfin.playback.core.model.PlayState import org.jellyfin.playback.core.model.PlaybackOrder import org.jellyfin.playback.core.model.RepeatMode -import org.jellyfin.playback.core.queue.Queue import org.jellyfin.playback.core.queue.QueueEntry +import org.jellyfin.playback.core.queue.queue +import org.jellyfin.playback.core.queue.supplier.QueueSupplier import org.jellyfin.playback.jellyfin.queue.baseItem import org.jellyfin.playback.jellyfin.queue.createBaseItemQueueEntry import org.jellyfin.sdk.api.client.ApiClient @@ -39,7 +40,7 @@ class RewriteMediaManager( private val navigationRepository: NavigationRepository, private val playbackManager: PlaybackManager, ) : MediaManager { - private val queue = BaseItemQueue(api) + private val queueSupplier = BaseItemQueueSupplier(api) override fun hasAudioQueueItems(): Boolean = currentAudioQueue.size() > 0 && currentAudioItem != null @@ -47,7 +48,7 @@ class RewriteMediaManager( get() = currentAudioQueue.size() override val currentAudioQueuePosition: Int - get() = if ((playbackManager.state.queue.entryIndex.value) >= 0) 0 else -1 + get() = if ((playbackManager.queue.entryIndex.value) >= 0) 0 else -1 override val currentAudioPosition: Long get() = playbackManager.state.positionInfo.active.inWholeMilliseconds @@ -56,11 +57,10 @@ class RewriteMediaManager( get() = (currentAudioQueuePosition + 1).toString() override val currentAudioQueueDisplaySize: String - get() = ((playbackManager.state.queue.current.value as? BaseItemQueue)?.items?.size - ?: currentAudioQueue.size()).toString() + get() = playbackManager.queue.estimatedSize.toString() override val currentAudioItem: BaseItemDto? - get() = playbackManager.state.queue.entry.value?.baseItem + get() = playbackManager.queue.entry.value?.baseItem ?.takeIf { it.mediaType == MediaType.AUDIO } override fun toggleRepeat(): Boolean { @@ -108,12 +108,14 @@ class RewriteMediaManager( val firstItem = currentAudioQueue.get(0) as? AudioQueueBaseRowItem firstItem?.playing = playState == PlayState.PLAYING - onPlaybackStateChange(when (playState) { - PlayState.STOPPED -> PlaybackController.PlaybackState.IDLE - PlayState.PLAYING -> PlaybackController.PlaybackState.PLAYING - PlayState.PAUSED -> PlaybackController.PlaybackState.PAUSED - PlayState.ERROR -> PlaybackController.PlaybackState.ERROR - }, currentAudioItem) + onPlaybackStateChange( + when (playState) { + PlayState.STOPPED -> PlaybackController.PlaybackState.IDLE + PlayState.PLAYING -> PlaybackController.PlaybackState.PLAYING + PlayState.PAUSED -> PlaybackController.PlaybackState.PAUSED + PlayState.ERROR -> PlaybackController.PlaybackState.ERROR + }, currentAudioItem + ) } }.launchIn(this) @@ -126,27 +128,27 @@ class RewriteMediaManager( } } - playbackManager.state.queue.current.onEach { + playbackManager.queue.entry.onEach { entry -> notifyListeners { - onQueueStatusChanged(hasAudioQueueItems()) + onQueueStatusChanged(entry != null) } }.launchIn(this) - playbackManager.state.queue.entry.onEach { updateAdapter() }.launchIn(this) + playbackManager.queue.entry.onEach { updateAdapter() }.launchIn(this) } private fun updateAdapter() { // Get all items as BaseRowItem - val items = queue + val items = queueSupplier .items // Map to audio queue items .mapIndexed { index, item -> AudioQueueBaseRowItem(item).apply { - playing = playbackManager.state.queue.entryIndex.value == index + playing = playbackManager.queue.entryIndex.value == index } } // Remove items before currently playing item - .drop(max(0, playbackManager.state.queue.entryIndex.value)) + .drop(max(0, playbackManager.queue.entryIndex.value)) // Update item row currentAudioQueue.replaceAll( @@ -186,31 +188,30 @@ class RewriteMediaManager( if (items.isEmpty()) return val addIndex = when (playbackManager.state.playState.value) { - PlayState.PLAYING -> playbackManager.state.queue.entryIndex.value + 1 + PlayState.PLAYING -> playbackManager.queue.entryIndex.value + 1 else -> 0 } - queue.items.addAll(addIndex, items) + queueSupplier.items.addAll(addIndex, items) - if ( - playbackManager.state.queue.current.value != queue || - playbackManager.state.playState.value != PlayState.PLAYING - ) { + if (playbackManager.state.playState.value != PlayState.PLAYING) { playbackManager.state.setPlaybackOrder(if (isShuffleMode) PlaybackOrder.SHUFFLE else PlaybackOrder.DEFAULT) - playbackManager.state.play(queue) + playbackManager.queue.clear() + playbackManager.queue.addSupplier(queueSupplier) + playbackManager.state.play() } updateAdapter() } override fun removeFromAudioQueue(item: BaseItemDto) { - val index = queue.items.indexOf(item) + val index = queueSupplier.items.indexOf(item) if (index == -1) return // Disallow removing currently playing item (legacy UI cannot keep up) - if (playbackManager.state.queue.entryIndex.value == index) return + if (playbackManager.queue.entryIndex.value == index) return - queue.items.removeAt(index) + queueSupplier.items.removeAt(index) updateAdapter() } @@ -219,19 +220,21 @@ class RewriteMediaManager( override fun playNow(context: Context, items: List, position: Int, shuffle: Boolean) { val filteredItems = items.drop(position) - queue.items.clear() - queue.items.addAll(filteredItems) + queueSupplier.items.clear() + queueSupplier.items.addAll(filteredItems) playbackManager.state.setPlaybackOrder(if (shuffle) PlaybackOrder.SHUFFLE else PlaybackOrder.DEFAULT) - playbackManager.state.play(queue) + playbackManager.queue.clear() + playbackManager.queue.addSupplier(queueSupplier) + playbackManager.state.play() navigationRepository.navigate(Destinations.nowPlaying) } override fun playFrom(item: BaseItemDto): Boolean { - val index = queue.items.indexOf(item) + val index = queueSupplier.items.indexOf(item) if (index == -1) return false return runBlocking { - playbackManager.state.queue.setIndex(index) != null + playbackManager.queue.setIndex(index) != null } } @@ -245,23 +248,23 @@ class RewriteMediaManager( } override fun hasNextAudioItem(): Boolean = runBlocking { - playbackManager.state.queue.peekNext() != null + playbackManager.queue.peekNext() != null } - override fun hasPrevAudioItem(): Boolean = playbackManager.state.queue.entryIndex.value > 0 + override fun hasPrevAudioItem(): Boolean = playbackManager.queue.entryIndex.value > 0 override fun nextAudioItem(): Int { - runBlocking { playbackManager.state.queue.next() } + runBlocking { playbackManager.queue.next() } notifyListeners { onQueueStatusChanged(hasAudioQueueItems()) } - return playbackManager.state.queue.entryIndex.value + return playbackManager.queue.entryIndex.value } override fun prevAudioItem(): Int { - runBlocking { playbackManager.state.queue.previous() } + runBlocking { playbackManager.queue.previous() } notifyListeners { onQueueStatusChanged(hasAudioQueueItems()) } - return playbackManager.state.queue.entryIndex.value + return playbackManager.queue.entryIndex.value } override fun stopAudio(releasePlayer: Boolean) { @@ -275,12 +278,12 @@ class RewriteMediaManager( } /** - * A simple [Queue] implementation for compatibility with existing UI/playback code. It contains + * A simple [QueueSupplier] implementation for compatibility with existing UI/playback code. It contains * a mutable BaseItemDto list that is used to retrieve items from. */ - class BaseItemQueue( + class BaseItemQueueSupplier( private val api: ApiClient, - ) : Queue { + ) : QueueSupplier { val items = mutableListOf() override val size: Int diff --git a/playback/core/src/main/kotlin/PlaybackManager.kt b/playback/core/src/main/kotlin/PlaybackManager.kt index bb1f67ac22..4bbff624ac 100644 --- a/playback/core/src/main/kotlin/PlaybackManager.kt +++ b/playback/core/src/main/kotlin/PlaybackManager.kt @@ -1,13 +1,10 @@ package org.jellyfin.playback.core -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob 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 @@ -15,7 +12,6 @@ import kotlin.reflect.KClass class PlaybackManager internal constructor( val backend: PlayerBackend, private val services: MutableList, - mediaStreamResolvers: List, val options: PlaybackManagerOptions, parentJob: Job? = null, ) { @@ -26,15 +22,12 @@ class PlaybackManager internal constructor( private val job = SupervisorJob(parentJob) val state: PlayerState = MutablePlayerState( options = options, - scope = CoroutineScope(Job(job)), backendService = backendService, + queue = getService() ) 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/PlaybackManagerBuilder.kt b/playback/core/src/main/kotlin/PlaybackManagerBuilder.kt index 24ebed74e1..031af0a939 100644 --- a/playback/core/src/main/kotlin/PlaybackManagerBuilder.kt +++ b/playback/core/src/main/kotlin/PlaybackManagerBuilder.kt @@ -5,8 +5,10 @@ import android.os.Build import androidx.core.content.getSystemService import org.jellyfin.playback.core.backend.PlayerBackend import org.jellyfin.playback.core.mediastream.MediaStreamResolver +import org.jellyfin.playback.core.mediastream.MediaStreamService import org.jellyfin.playback.core.plugin.PlaybackPlugin import org.jellyfin.playback.core.plugin.PlayerService +import org.jellyfin.playback.core.queue.QueueService import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -28,6 +30,7 @@ class PlaybackManagerBuilder(context: Context) { val services = mutableListOf() val mediaStreamResolvers = mutableListOf() + // Add plugins val installContext = object : PlaybackPlugin.InstallContext { override fun provide(backend: PlayerBackend) { backends.add(backend) @@ -44,6 +47,10 @@ class PlaybackManagerBuilder(context: Context) { for (factory in factories) factory.install(installContext) + // Add default services + services.add(QueueService()) + services.add(MediaStreamService(mediaStreamResolvers)) + // Only support a single backend right now require(backends.size == 1) val options = PlaybackManagerOptions( @@ -51,7 +58,7 @@ class PlaybackManagerBuilder(context: Context) { defaultRewindAmount = defaultRewindAmount ?: { 10.seconds }, defaultFastForwardAmount = defaultFastForwardAmount ?: { 10.seconds }, ) - return PlaybackManager(backends.first(), services, mediaStreamResolvers, options) + return PlaybackManager(backends.first(), services, options) } } diff --git a/playback/core/src/main/kotlin/PlayerState.kt b/playback/core/src/main/kotlin/PlayerState.kt index 2d2040389f..17e160b771 100644 --- a/playback/core/src/main/kotlin/PlayerState.kt +++ b/playback/core/src/main/kotlin/PlayerState.kt @@ -1,6 +1,5 @@ package org.jellyfin.playback.core -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -12,14 +11,10 @@ import org.jellyfin.playback.core.model.PlaybackOrder import org.jellyfin.playback.core.model.PositionInfo import org.jellyfin.playback.core.model.RepeatMode import org.jellyfin.playback.core.model.VideoSize -import org.jellyfin.playback.core.queue.DefaultPlayerQueueState -import org.jellyfin.playback.core.queue.EmptyQueue -import org.jellyfin.playback.core.queue.PlayerQueueState -import org.jellyfin.playback.core.queue.Queue +import org.jellyfin.playback.core.queue.QueueService import kotlin.time.Duration interface PlayerState { - val queue: PlayerQueueState val volume: PlayerVolumeState val playState: StateFlow val speed: StateFlow @@ -35,7 +30,7 @@ interface PlayerState { val positionInfo: PositionInfo // Queue management - fun play(playQueue: Queue) + fun play() fun stop() // Pausing @@ -60,10 +55,9 @@ interface PlayerState { class MutablePlayerState( private val options: PlaybackManagerOptions, - scope: CoroutineScope, private val backendService: BackendService, + private val queue: QueueService?, ) : PlayerState { - override val queue: PlayerQueueState override val volume: PlayerVolumeState private val _playState = MutableStateFlow(PlayState.STOPPED) @@ -97,12 +91,10 @@ class MutablePlayerState( override fun onMediaStreamEnd(mediaStream: PlayableMediaStream) = Unit }) - queue = DefaultPlayerQueueState(this, scope, backendService) volume = options.playerVolumeState } - override fun play(playQueue: Queue) { - queue.replaceQueue(playQueue) + override fun play() { backendService.backend?.play() } @@ -117,7 +109,7 @@ class MutablePlayerState( override fun stop() { backendService.backend?.stop() - queue.replaceQueue(EmptyQueue) + queue?.clear() } override fun seek(to: Duration) { diff --git a/playback/core/src/main/kotlin/mediastream/MediaStreamState.kt b/playback/core/src/main/kotlin/mediastream/MediaStreamService.kt similarity index 75% rename from playback/core/src/main/kotlin/mediastream/MediaStreamState.kt rename to playback/core/src/main/kotlin/mediastream/MediaStreamService.kt index 2ac9f1479e..a0aa691838 100644 --- a/playback/core/src/main/kotlin/mediastream/MediaStreamState.kt +++ b/playback/core/src/main/kotlin/mediastream/MediaStreamService.kt @@ -1,26 +1,22 @@ package org.jellyfin.playback.core.mediastream -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach 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 org.jellyfin.playback.core.plugin.PlayerService import org.jellyfin.playback.core.queue.QueueEntry +import org.jellyfin.playback.core.queue.queue import timber.log.Timber -internal class MediaStreamState( - state: PlayerState, - coroutineScope: CoroutineScope, +internal class MediaStreamService( private val mediaStreamResolvers: Collection, - private val backendService: BackendService, -) { - init { - state.queue.entry.onEach { entry -> +) : PlayerService() { + override suspend fun onInitialize() { + manager.queue.entry.onEach { entry -> Timber.d("Queue entry changed to $entry") - val backend = requireNotNull(backendService.backend) + val backend = requireNotNull(manager.backend) if (entry == null) { backend.setCurrent(null) @@ -33,8 +29,8 @@ internal class MediaStreamState( 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) + if (manager.queue.peekNext() != null) { + manager.queue.next(usePlaybackOrder = true, useRepeatMode = false) } else { backend.setCurrent(null) } diff --git a/playback/core/src/main/kotlin/queue/EmptyQueue.kt b/playback/core/src/main/kotlin/queue/EmptyQueue.kt deleted file mode 100644 index 4eccc929a5..0000000000 --- a/playback/core/src/main/kotlin/queue/EmptyQueue.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.jellyfin.playback.core.queue - -data object EmptyQueue : Queue { - override val size: Int = 0 - override suspend fun getItem(index: Int): QueueEntry? = null -} diff --git a/playback/core/src/main/kotlin/queue/Queue.kt b/playback/core/src/main/kotlin/queue/Queue.kt index bda05e217f..47e6187762 100644 --- a/playback/core/src/main/kotlin/queue/Queue.kt +++ b/playback/core/src/main/kotlin/queue/Queue.kt @@ -1,14 +1,85 @@ package org.jellyfin.playback.core.queue -/** - * A queue contains all items in the current playback session. This includes already played items, - * the currently playing item and future items. - */ +import kotlinx.coroutines.flow.StateFlow +import org.jellyfin.playback.core.queue.supplier.QueueSupplier + interface Queue { + companion object { + const val INDEX_NONE = -1 + } + + /** + * Get an estimated size of the queue. This may be off when the used suppliers are guessing their size. + */ + val estimatedSize: Int + + /** + * Index of the currently playing entry, or -1 if none. + */ + val entryIndex: StateFlow + /** - * The total size of the queue. + * Currently playing entry or null. */ - val size: Int + val entry: StateFlow - suspend fun getItem(index: Int): QueueEntry? + /** + * Add a supplier of queue items to the end of the queue. Will automatically fetch the first item if there is no current entry. + */ + fun addSupplier(supplier: QueueSupplier) + + /** + * Clear all queue state, including suppliers, entries and currently playing entry. + */ + fun clear() + + /** + * Set the current entry to the previously played entry. Does nothing if there is no previous entry. + */ + suspend fun previous(): QueueEntry? + + /** + * Play the next entry in the queue. + * + * @param usePlaybackOrder Whether to use the playback order from the [PlayerState]. Default to true. + * @param useRepeatMode Whether to use the repeat mode from the [PlayerState]. Default to false. + */ + suspend fun next(usePlaybackOrder: Boolean = true, useRepeatMode: Boolean = false): QueueEntry? + + /** + * Skip to the given index. + * + * @param index The index of the entry to play + * @param saveHistory Whether to save the current entry to the play history + */ + suspend fun setIndex(index: Int, saveHistory: Boolean = false): QueueEntry? + + /** + * Get the previously playing entry or null if none. + */ + suspend fun peekPrevious(): QueueEntry? + + /** + * Get the next entry or null if none. + * + * @param usePlaybackOrder Whether to use the playback order from the [PlayerState]. Default to true. + * @param useRepeatMode Whether to use the repeat mode from the [PlayerState]. Default to false. + */ + suspend fun peekNext( + usePlaybackOrder: Boolean = true, + useRepeatMode: Boolean = false, + ): QueueEntry? + + /** + * Get the next n entries in the queue. Where n is the amount to fetch. The returned collection may be smaller or empty depending on + * the entries in the queue. + * + * @param usePlaybackOrder Whether to use the playback order from the [PlayerState]. Default to true. + * @param useRepeatMode Whether to use the repeat mode from the [PlayerState]. Default to false. + */ + suspend fun peekNext( + amount: Int, + usePlaybackOrder: Boolean = true, + useRepeatMode: Boolean = false, + ): Collection } diff --git a/playback/core/src/main/kotlin/queue/PlayerQueueState.kt b/playback/core/src/main/kotlin/queue/QueueService.kt similarity index 55% rename from playback/core/src/main/kotlin/queue/PlayerQueueState.kt rename to playback/core/src/main/kotlin/queue/QueueService.kt index 690199824f..aae04da267 100644 --- a/playback/core/src/main/kotlin/queue/PlayerQueueState.kt +++ b/playback/core/src/main/kotlin/queue/QueueService.kt @@ -1,71 +1,41 @@ package org.jellyfin.playback.core.queue -import kotlinx.coroutines.CoroutineScope 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.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.PlayableMediaStream -import org.jellyfin.playback.core.model.PlayState +import org.jellyfin.playback.core.PlaybackManager import org.jellyfin.playback.core.model.PlaybackOrder import org.jellyfin.playback.core.model.RepeatMode +import org.jellyfin.playback.core.plugin.PlayerService import org.jellyfin.playback.core.queue.order.DefaultOrderIndexProvider import org.jellyfin.playback.core.queue.order.OrderIndexProvider import org.jellyfin.playback.core.queue.order.RandomOrderIndexProvider import org.jellyfin.playback.core.queue.order.ShuffleOrderIndexProvider -import timber.log.Timber +import org.jellyfin.playback.core.queue.supplier.QueueSupplier +import kotlin.math.max -interface PlayerQueueState { - companion object { - const val INDEX_NONE = -1 - } - - val current: StateFlow - val entryIndex: StateFlow - val entry: StateFlow - - // Queue Management - fun replaceQueue(queue: Queue) - - // Queue Seeking - suspend fun previous(): QueueEntry? - suspend fun next(usePlaybackOrder: Boolean = true, useRepeatMode: Boolean = false): QueueEntry? - suspend fun setIndex(index: Int, saveHistory: Boolean = false): QueueEntry? +class QueueService internal constructor() : PlayerService(), Queue { + private val suppliers = mutableListOf() + private var currentSupplierIndex = 0 + private var currentSupplierItemIndex = 0 + private val fetchedItems: MutableList = mutableListOf() - // Peeking - suspend fun peekPrevious(): QueueEntry? - suspend fun peekNext(usePlaybackOrder: Boolean = true, useRepeatMode: Boolean = false): QueueEntry? - suspend fun peekNext( - amount: Int, - usePlaybackOrder: Boolean = true, - useRepeatMode: Boolean = false - ): Collection -} + private var defaultOrderIndexProvider = DefaultOrderIndexProvider() + private var orderIndexProvider: OrderIndexProvider = defaultOrderIndexProvider + private var currentQueueIndicesPlayed = mutableListOf() -class DefaultPlayerQueueState( - private val state: PlayerState, - private val coroutineScope: CoroutineScope, - backendService: BackendService, -) : PlayerQueueState { - private val _current = MutableStateFlow(EmptyQueue) - override val current: StateFlow get() = _current.asStateFlow() + override val estimatedSize get() = max(fetchedItems.size, suppliers.sumOf { it.size }) - private val _entryIndex = MutableStateFlow(PlayerQueueState.INDEX_NONE) + private val _entryIndex = MutableStateFlow(Queue.INDEX_NONE) override val entryIndex: StateFlow get() = _entryIndex.asStateFlow() private val _entry = MutableStateFlow(null) override val entry: StateFlow get() = _entry.asStateFlow() - private var defaultOrderIndexProvider = DefaultOrderIndexProvider() - private var orderIndexProvider: OrderIndexProvider = defaultOrderIndexProvider - private var currentQueueIndicesPlayed = mutableListOf() - - init { + override suspend fun onInitialize() { // Reset calculated next-up indices when playback order changes state.playbackOrder.onEach { playbackOrder -> orderIndexProvider = when (playbackOrder) { @@ -74,43 +44,65 @@ class DefaultPlayerQueueState( PlaybackOrder.SHUFFLE -> ShuffleOrderIndexProvider() } }.launchIn(coroutineScope) - - backendService.addListener(object : PlayerBackendEventListener { - override fun onPlayStateChange(state: PlayState) = Unit - override fun onVideoSizeChange(width: Int, height: Int) = Unit - - 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) } - } - }) } - override fun replaceQueue(queue: Queue) { - Timber.d("Queue changed, setting index to 0") + // Entry management - coroutineScope.launch { - _current.value = queue - orderIndexProvider.reset() - if (orderIndexProvider != defaultOrderIndexProvider) defaultOrderIndexProvider.reset() + override fun addSupplier(supplier: QueueSupplier) { + suppliers.add(supplier) - currentQueueIndicesPlayed.clear() + if (_entryIndex.value == Queue.INDEX_NONE) { + coroutineScope.launch { setIndex(0) } + } + } - setIndex(0) + private suspend fun getOrSupplyItem(index: Int): QueueEntry? { + // Fetch additional items from suppliers until we reach the desired index + while (index >= fetchedItems.size) { + // No more suppliers to try + if (currentSupplierIndex >= suppliers.size) break + + val supplier = suppliers[currentSupplierIndex] + val nextItem = supplier.getItem(currentSupplierItemIndex) + + if (nextItem != null) { + // Add item to cache and icnrease item index + fetchedItems.add(nextItem) + currentSupplierItemIndex++ + } else { + // Move to the next supplier if current one is exhausted + currentSupplierIndex++ + currentSupplierItemIndex = 0 + } } + + // Return item or null if not found + return if (index < fetchedItems.size) fetchedItems[index] + else null + } + + override fun clear() { + suppliers.clear() + currentSupplierIndex = 0 + currentSupplierItemIndex = 0 + fetchedItems.clear() + _entry.value = null + _entryIndex.value = Queue.INDEX_NONE + currentQueueIndicesPlayed.clear() } + // Preloading + private fun getNextIndices(amount: Int, usePlaybackOrder: Boolean, useRepeatMode: Boolean): Collection { val provider = if (usePlaybackOrder) orderIndexProvider else defaultOrderIndexProvider val repeatMode = if (useRepeatMode) state.repeatMode.value else RepeatMode.NONE return when (repeatMode) { - RepeatMode.NONE -> provider.provideIndices(amount, _current.value, currentQueueIndicesPlayed, entryIndex.value) + RepeatMode.NONE -> provider.provideIndices(amount, estimatedSize, currentQueueIndicesPlayed, entryIndex.value) RepeatMode.REPEAT_ENTRY_ONCE -> buildList { add(entryIndex.value) - addAll(provider.provideIndices(amount, _current.value, currentQueueIndicesPlayed, entryIndex.value)) + addAll(provider.provideIndices(amount, estimatedSize, currentQueueIndicesPlayed, entryIndex.value)) }.take(amount) RepeatMode.REPEAT_ENTRY_INFINITE -> List(amount) { entryIndex.value } @@ -141,13 +133,13 @@ class DefaultPlayerQueueState( if (index < 0) return null // Save previous index - if (saveHistory && _entryIndex.value != PlayerQueueState.INDEX_NONE) { + if (saveHistory && _entryIndex.value != Queue.INDEX_NONE) { currentQueueIndicesPlayed.add(_entryIndex.value) } // Set new index - val currentEntry = _current.value.getItem(index) - _entryIndex.value = if (currentEntry == null) PlayerQueueState.INDEX_NONE else index + val currentEntry = getOrSupplyItem(index) + _entryIndex.value = if (currentEntry == null) Queue.INDEX_NONE else index _entry.value = currentEntry return currentEntry @@ -156,7 +148,7 @@ class DefaultPlayerQueueState( // Peeking override suspend fun peekPrevious(): QueueEntry? = currentQueueIndicesPlayed.lastOrNull()?.let { - _current.value.getItem(it) + getOrSupplyItem(it) } override suspend fun peekNext( @@ -169,8 +161,9 @@ class DefaultPlayerQueueState( usePlaybackOrder: Boolean, useRepeatMode: Boolean, ): Collection { - val queue = _current.value return getNextIndices(amount, usePlaybackOrder, useRepeatMode) - .mapNotNull { index -> queue.getItem(index) } + .mapNotNull { index -> getOrSupplyItem(index) } } } + +val PlaybackManager.queue: Queue get() = requireNotNull(getService()) diff --git a/playback/core/src/main/kotlin/queue/order/DefaultOrderIndexProvider.kt b/playback/core/src/main/kotlin/queue/order/DefaultOrderIndexProvider.kt index 81bdf7f5f5..3a0b5098dc 100644 --- a/playback/core/src/main/kotlin/queue/order/DefaultOrderIndexProvider.kt +++ b/playback/core/src/main/kotlin/queue/order/DefaultOrderIndexProvider.kt @@ -1,17 +1,16 @@ package org.jellyfin.playback.core.queue.order -import org.jellyfin.playback.core.queue.Queue import kotlin.math.min internal class DefaultOrderIndexProvider : OrderIndexProvider { override fun provideIndices( amount: Int, - queue: Queue, + size: Int, playedIndices: Collection, currentIndex: Int, ): Collection { // No need to use currentQueueNextIndices because we can efficiently calculate the next items - val remainingItemsSize = queue.size - currentIndex - 1 + val remainingItemsSize = size - currentIndex - 1 return if (remainingItemsSize <= 0) emptyList() else Array(min(amount, remainingItemsSize)) { i -> currentIndex + i + 1 }.toList() diff --git a/playback/core/src/main/kotlin/queue/order/OrderIndexProvider.kt b/playback/core/src/main/kotlin/queue/order/OrderIndexProvider.kt index 8309452d47..0d0ff971c4 100644 --- a/playback/core/src/main/kotlin/queue/order/OrderIndexProvider.kt +++ b/playback/core/src/main/kotlin/queue/order/OrderIndexProvider.kt @@ -1,7 +1,6 @@ package org.jellyfin.playback.core.queue.order -import org.jellyfin.playback.core.queue.PlayerQueueState -import org.jellyfin.playback.core.queue.Queue +import org.jellyfin.playback.core.queue.QueueService internal interface OrderIndexProvider { /** @@ -13,15 +12,15 @@ internal interface OrderIndexProvider { * Collect the next [amount] of indices to play. * * @param amount The maximum amount of indices to retrieve. May be less if there are none left. - * @param queue The queue to generate indices for. + * @param size The size of the queue to generate indices for. * @param playedIndices The previously played indices, this may include the [currentIndex]. - * @param currentIndex The currently playing index or [PlayerQueueState.INDEX_NONE]. + * @param currentIndex The currently playing index or [QueueService.INDEX_NONE]. * * @return A collection no more than [amount] items of indices to play next. */ fun provideIndices( amount: Int, - queue: Queue, + size: Int, playedIndices: Collection, currentIndex: Int, ): Collection diff --git a/playback/core/src/main/kotlin/queue/order/RandomOrderIndexProvider.kt b/playback/core/src/main/kotlin/queue/order/RandomOrderIndexProvider.kt index 4985d1eca0..fa396ed8ac 100644 --- a/playback/core/src/main/kotlin/queue/order/RandomOrderIndexProvider.kt +++ b/playback/core/src/main/kotlin/queue/order/RandomOrderIndexProvider.kt @@ -1,6 +1,5 @@ package org.jellyfin.playback.core.queue.order -import org.jellyfin.playback.core.queue.Queue import kotlin.random.Random internal class RandomOrderIndexProvider : OrderIndexProvider { @@ -10,14 +9,14 @@ internal class RandomOrderIndexProvider : OrderIndexProvider { override fun provideIndices( amount: Int, - queue: Queue, + size: Int, playedIndices: Collection, currentIndex: Int, ) = List(amount) { i -> if (i <= nextIndices.lastIndex) { nextIndices[i] } else { - val index = Random.nextInt(queue.size) + val index = Random.nextInt(size) nextIndices.add(index) index } diff --git a/playback/core/src/main/kotlin/queue/order/ShuffleOrderIndexProvider.kt b/playback/core/src/main/kotlin/queue/order/ShuffleOrderIndexProvider.kt index 694a6eb94d..4c6d676e8c 100644 --- a/playback/core/src/main/kotlin/queue/order/ShuffleOrderIndexProvider.kt +++ b/playback/core/src/main/kotlin/queue/order/ShuffleOrderIndexProvider.kt @@ -1,6 +1,5 @@ package org.jellyfin.playback.core.queue.order -import org.jellyfin.playback.core.queue.Queue import kotlin.math.min internal class ShuffleOrderIndexProvider : OrderIndexProvider { @@ -10,15 +9,15 @@ internal class ShuffleOrderIndexProvider : OrderIndexProvider { override fun provideIndices( amount: Int, - queue: Queue, + size: Int, playedIndices: Collection, currentIndex: Int, ): Collection { - val remainingItemsSize = queue.size - playedIndices.size + val remainingItemsSize = size - playedIndices.size return if (remainingItemsSize <= 0) { emptyList() } else { - val remainingIndices = (0..queue.size).filterNot { + val remainingIndices = (0..size).filterNot { it in playedIndices || it in nextIndices } diff --git a/playback/core/src/main/kotlin/queue/PagedQueue.kt b/playback/core/src/main/kotlin/queue/supplier/PagedQueueSupplier.kt similarity index 67% rename from playback/core/src/main/kotlin/queue/PagedQueue.kt rename to playback/core/src/main/kotlin/queue/supplier/PagedQueueSupplier.kt index 2ddb97062a..7f9bb2e20c 100644 --- a/playback/core/src/main/kotlin/queue/PagedQueue.kt +++ b/playback/core/src/main/kotlin/queue/supplier/PagedQueueSupplier.kt @@ -1,12 +1,18 @@ -package org.jellyfin.playback.core.queue +package org.jellyfin.playback.core.queue.supplier -abstract class PagedQueue( +import org.jellyfin.playback.core.queue.QueueEntry + +abstract class PagedQueueSupplier( private val pageSize: Int = 10, -) : Queue { +) : QueueSupplier { + companion object { + const val MAX_SIZE = 100 + } + private val buffer: MutableList = mutableListOf() override suspend fun getItem(index: Int): QueueEntry? { - require(index in 0 until SequenceQueue.MAX_SIZE) + require(index in 0 until MAX_SIZE) var page: Collection var pageOffset = buffer.size diff --git a/playback/core/src/main/kotlin/queue/supplier/QueueSupplier.kt b/playback/core/src/main/kotlin/queue/supplier/QueueSupplier.kt new file mode 100644 index 0000000000..bb4a1afa8f --- /dev/null +++ b/playback/core/src/main/kotlin/queue/supplier/QueueSupplier.kt @@ -0,0 +1,16 @@ +package org.jellyfin.playback.core.queue.supplier + +import org.jellyfin.playback.core.queue.QueueEntry + +/** + * A queue contains all items in the current playback session. This includes already played items, + * the currently playing item and future items. + */ +interface QueueSupplier { + /** + * The total size of the queue. + */ + val size: Int + + suspend fun getItem(index: Int): QueueEntry? +} diff --git a/playback/core/src/main/kotlin/queue/SequenceQueue.kt b/playback/core/src/main/kotlin/queue/supplier/SequenceQueueSupplier.kt similarity index 78% rename from playback/core/src/main/kotlin/queue/SequenceQueue.kt rename to playback/core/src/main/kotlin/queue/supplier/SequenceQueueSupplier.kt index b66f37cd12..e20a6d4e11 100644 --- a/playback/core/src/main/kotlin/queue/SequenceQueue.kt +++ b/playback/core/src/main/kotlin/queue/supplier/SequenceQueueSupplier.kt @@ -1,6 +1,8 @@ -package org.jellyfin.playback.core.queue +package org.jellyfin.playback.core.queue.supplier -abstract class SequenceQueue : Queue { +import org.jellyfin.playback.core.queue.QueueEntry + +abstract class SequenceQueueSupplier : QueueSupplier { companion object { const val MAX_SIZE = 100 } diff --git a/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt b/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt index a3ed3dd448..4ecedaae28 100644 --- a/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt +++ b/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt @@ -10,6 +10,7 @@ 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 +import org.jellyfin.playback.core.queue.queue import org.jellyfin.playback.jellyfin.queue.baseItem import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.playStateApi @@ -58,14 +59,14 @@ class PlaySessionService( private suspend fun getQueue(): List { // The queues are lazy loaded so we only load a small amount of items to set as queue on the // backend. - return state.queue + return manager.queue .peekNext(15) .mapNotNull { it.baseItem } .map { QueueItem(id = it.id, playlistItemId = it.playlistItemId) } } private suspend fun sendStreamStart() { - val entry = state.queue.entry.value ?: return + val entry = manager.queue.entry.value ?: return val stream = entry.mediaStream ?: return val item = entry.baseItem ?: return @@ -93,7 +94,7 @@ class PlaySessionService( } private suspend fun sendStreamUpdate() { - val entry = state.queue.entry.value ?: return + val entry = manager.queue.entry.value ?: return val stream = entry.mediaStream ?: return val item = entry.baseItem ?: return @@ -121,7 +122,7 @@ class PlaySessionService( } private suspend fun sendStreamStop() { - val entry = state.queue.entry.value ?: return + val entry = manager.queue.entry.value ?: return val stream = entry.mediaStream ?: return val item = entry.baseItem ?: return diff --git a/playback/jellyfin/src/main/kotlin/playsession/PlaySessionSocketService.kt b/playback/jellyfin/src/main/kotlin/playsession/PlaySessionSocketService.kt index 82c09ed548..d25e9a59c6 100644 --- a/playback/jellyfin/src/main/kotlin/playsession/PlaySessionSocketService.kt +++ b/playback/jellyfin/src/main/kotlin/playsession/PlaySessionSocketService.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.jellyfin.playback.core.model.PlayState import org.jellyfin.playback.core.plugin.PlayerService +import org.jellyfin.playback.core.queue.queue import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.sockets.subscribe import org.jellyfin.sdk.api.sockets.subscribeGeneralCommand @@ -28,8 +29,8 @@ class PlaySessionSocketService( PlaystateCommand.STOP -> state.stop() PlaystateCommand.PAUSE -> state.pause() PlaystateCommand.UNPAUSE -> state.unpause() - PlaystateCommand.NEXT_TRACK -> state.queue.next() - PlaystateCommand.PREVIOUS_TRACK -> state.queue.previous() + PlaystateCommand.NEXT_TRACK -> manager.queue.next() + PlaystateCommand.PREVIOUS_TRACK -> manager.queue.previous() PlaystateCommand.SEEK -> { val to = message.data?.seekPositionTicks?.ticks ?: Duration.ZERO state.seek(to) diff --git a/playback/jellyfin/src/main/kotlin/queue/AudioAlbumQueue.kt b/playback/jellyfin/src/main/kotlin/queue/AudioAlbumQueueSupplier.kt similarity index 90% rename from playback/jellyfin/src/main/kotlin/queue/AudioAlbumQueue.kt rename to playback/jellyfin/src/main/kotlin/queue/AudioAlbumQueueSupplier.kt index 6fe1cf8966..d953390cb9 100644 --- a/playback/jellyfin/src/main/kotlin/queue/AudioAlbumQueue.kt +++ b/playback/jellyfin/src/main/kotlin/queue/AudioAlbumQueueSupplier.kt @@ -1,7 +1,7 @@ package org.jellyfin.playback.jellyfin.queue -import org.jellyfin.playback.core.queue.PagedQueue import org.jellyfin.playback.core.queue.QueueEntry +import org.jellyfin.playback.core.queue.supplier.PagedQueueSupplier import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.model.api.BaseItemDto @@ -10,10 +10,10 @@ import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.MediaType -class AudioAlbumQueue( +class AudioAlbumQueueSupplier( private val album: BaseItemDto, private val api: ApiClient, -) : PagedQueue() { +) : PagedQueueSupplier() { init { require(album.type == BaseItemKind.MUSIC_ALBUM) } diff --git a/playback/jellyfin/src/main/kotlin/queue/AudioInstantMixQueue.kt b/playback/jellyfin/src/main/kotlin/queue/AudioInstantMixQueueSupplier.kt similarity index 89% rename from playback/jellyfin/src/main/kotlin/queue/AudioInstantMixQueue.kt rename to playback/jellyfin/src/main/kotlin/queue/AudioInstantMixQueueSupplier.kt index 6e1a51d973..85c7e12f58 100644 --- a/playback/jellyfin/src/main/kotlin/queue/AudioInstantMixQueue.kt +++ b/playback/jellyfin/src/main/kotlin/queue/AudioInstantMixQueueSupplier.kt @@ -1,17 +1,17 @@ package org.jellyfin.playback.jellyfin.queue -import org.jellyfin.playback.core.queue.PagedQueue import org.jellyfin.playback.core.queue.QueueEntry +import org.jellyfin.playback.core.queue.supplier.PagedQueueSupplier import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.instantMixApi import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.ItemFields -class AudioInstantMixQueue( +class AudioInstantMixQueueSupplier( private val item: BaseItemDto, private val api: ApiClient, -) : PagedQueue() { +) : PagedQueueSupplier() { companion object { val instantMixableItems = arrayOf( BaseItemKind.MUSIC_GENRE, diff --git a/playback/jellyfin/src/main/kotlin/queue/AudioTrackQueue.kt b/playback/jellyfin/src/main/kotlin/queue/AudioTrackQueueSupplier.kt similarity index 84% rename from playback/jellyfin/src/main/kotlin/queue/AudioTrackQueue.kt rename to playback/jellyfin/src/main/kotlin/queue/AudioTrackQueueSupplier.kt index fed1c53875..60a8bc3cfc 100644 --- a/playback/jellyfin/src/main/kotlin/queue/AudioTrackQueue.kt +++ b/playback/jellyfin/src/main/kotlin/queue/AudioTrackQueueSupplier.kt @@ -1,16 +1,16 @@ package org.jellyfin.playback.jellyfin.queue -import org.jellyfin.playback.core.queue.PagedQueue import org.jellyfin.playback.core.queue.QueueEntry +import org.jellyfin.playback.core.queue.supplier.PagedQueueSupplier import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind -class AudioTrackQueue( +class AudioTrackQueueSupplier( private val item: BaseItemDto, private val api: ApiClient, -) : PagedQueue() { +) : PagedQueueSupplier() { init { require(item.type == BaseItemKind.AUDIO) } diff --git a/playback/jellyfin/src/main/kotlin/queue/EpisodeQueue.kt b/playback/jellyfin/src/main/kotlin/queue/EpisodeQueueSupplier.kt similarity index 90% rename from playback/jellyfin/src/main/kotlin/queue/EpisodeQueue.kt rename to playback/jellyfin/src/main/kotlin/queue/EpisodeQueueSupplier.kt index 81660a217b..ebb6548a11 100644 --- a/playback/jellyfin/src/main/kotlin/queue/EpisodeQueue.kt +++ b/playback/jellyfin/src/main/kotlin/queue/EpisodeQueueSupplier.kt @@ -1,7 +1,7 @@ package org.jellyfin.playback.jellyfin.queue -import org.jellyfin.playback.core.queue.PagedQueue import org.jellyfin.playback.core.queue.QueueEntry +import org.jellyfin.playback.core.queue.supplier.PagedQueueSupplier import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.model.api.BaseItemDto @@ -10,10 +10,10 @@ import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.MediaType -class EpisodeQueue( +class EpisodeQueueSupplier( private val episode: BaseItemDto, private val api: ApiClient, -) : PagedQueue() { +) : PagedQueueSupplier() { init { require(episode.type == BaseItemKind.EPISODE) } diff --git a/playback/media3/session/src/main/kotlin/MediaSessionPlayer.kt b/playback/media3/session/src/main/kotlin/MediaSessionPlayer.kt index d4dc755f06..5ae0f5092f 100644 --- a/playback/media3/session/src/main/kotlin/MediaSessionPlayer.kt +++ b/playback/media3/session/src/main/kotlin/MediaSessionPlayer.kt @@ -23,6 +23,7 @@ import org.jellyfin.playback.core.model.PlayState import org.jellyfin.playback.core.model.PlaybackOrder import org.jellyfin.playback.core.model.RepeatMode import org.jellyfin.playback.core.queue.metadata +import org.jellyfin.playback.core.queue.queue import timber.log.Timber import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @@ -36,7 +37,7 @@ internal class MediaSessionPlayer( ) : SimpleBasePlayer(looper) { init { // Invalidate mediasession state when certain player state changes - state.queue.entry.invalidateStateOnEach(scope) + manager.queue.entry.invalidateStateOnEach(scope) state.playState.invalidateStateOnEach(scope) state.videoSize.invalidateStateOnEach(scope) state.speed.invalidateStateOnEach(scope) @@ -57,10 +58,10 @@ internal class MediaSessionPlayer( add(COMMAND_STOP) add(COMMAND_SEEK_TO_DEFAULT_POSITION) add(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM) - val allowPrevious = state.queue.entryIndex.value > 0 + val allowPrevious = manager.queue.entryIndex.value > 0 addIf(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, allowPrevious) addIf(COMMAND_SEEK_TO_PREVIOUS, allowPrevious) - val allowNext = state.queue.entryIndex.value < (state.queue.current.value.size - 1) + val allowNext = manager.queue.entryIndex.value < (manager.queue.estimatedSize - 1) addIf(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, allowNext) addIf(COMMAND_SEEK_TO_NEXT, allowNext) // add(COMMAND_SEEK_TO_MEDIA_ITEM) @@ -88,11 +89,11 @@ internal class MediaSessionPlayer( }.build()) runBlocking { - val current = state.queue.entry.value + val current = manager.queue.entry.value if (current != null) { - val previous = state.queue.peekPrevious() - val next = state.queue.peekNext() + val previous = manager.queue.peekPrevious() + val next = manager.queue.peekNext() val playlist = listOfNotNull(previous, current, next) .distinctBy { it.metadata.mediaId } @@ -162,10 +163,10 @@ internal class MediaSessionPlayer( @Suppress("SwitchIntDef") when (seekCommand) { COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, - COMMAND_SEEK_TO_PREVIOUS -> state.queue.previous() + COMMAND_SEEK_TO_PREVIOUS -> manager.queue.previous() COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, - COMMAND_SEEK_TO_NEXT -> state.queue.next() + COMMAND_SEEK_TO_NEXT -> manager.queue.next() } // Seeking diff --git a/playback/media3/session/src/main/kotlin/MediaSessionService.kt b/playback/media3/session/src/main/kotlin/MediaSessionService.kt index f65458698a..7bd26fe2ce 100644 --- a/playback/media3/session/src/main/kotlin/MediaSessionService.kt +++ b/playback/media3/session/src/main/kotlin/MediaSessionService.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.onEach import org.jellyfin.playback.core.plugin.PlayerService import org.jellyfin.playback.core.queue.QueueEntry import org.jellyfin.playback.core.queue.metadata +import org.jellyfin.playback.core.queue.queue class MediaSessionService( private val androidContext: Context, @@ -34,7 +35,7 @@ class MediaSessionService( setSessionActivity(options.openIntent) }.build() - state.queue.entry.onEach { item -> + manager.queue.entry.onEach { item -> if (item != null) updateNotification(session, item) else if (notifiedNotificationId != null) { notificationManager.cancel(notifiedNotificationId!!)