mirror of
https://github.com/jellyfin/jellyfin-androidtv.git
synced 2024-11-27 08:00:28 +00:00
Rewrite queuing
This commit is contained in:
parent
a8c9f054ab
commit
0f38bd1691
@ -252,7 +252,7 @@ public class AudioNowPlayingFragment extends Fragment {
|
||||
|
||||
@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()) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
onPlaybackStateChange(
|
||||
when (playState) {
|
||||
PlayState.STOPPED -> PlaybackController.PlaybackState.IDLE
|
||||
PlayState.PLAYING -> PlaybackController.PlaybackState.PLAYING
|
||||
PlayState.PAUSED -> PlaybackController.PlaybackState.PAUSED
|
||||
PlayState.ERROR -> PlaybackController.PlaybackState.ERROR
|
||||
}, currentAudioItem)
|
||||
}, 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<BaseItemDto>, 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<BaseItemDto>()
|
||||
|
||||
override val size: Int
|
||||
|
@ -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<PlayerService>,
|
||||
mediaStreamResolvers: List<MediaStreamResolver>,
|
||||
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) {
|
||||
|
@ -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<PlayerService>()
|
||||
val mediaStreamResolvers = mutableListOf<MediaStreamResolver>()
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<PlayState>
|
||||
val speed: StateFlow<Float>
|
||||
@ -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) {
|
||||
|
@ -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<MediaStreamResolver>,
|
||||
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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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.
|
||||
*/
|
||||
interface Queue {
|
||||
/**
|
||||
* The total size of the queue.
|
||||
*/
|
||||
val size: Int
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.jellyfin.playback.core.queue.supplier.QueueSupplier
|
||||
|
||||
suspend fun getItem(index: Int): QueueEntry?
|
||||
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<Int>
|
||||
|
||||
/**
|
||||
* Currently playing entry or null.
|
||||
*/
|
||||
val entry: StateFlow<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<QueueEntry>
|
||||
}
|
||||
|
@ -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<Queue>
|
||||
val entryIndex: StateFlow<Int>
|
||||
val entry: StateFlow<QueueEntry?>
|
||||
|
||||
// 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?
|
||||
|
||||
// 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<QueueEntry>
|
||||
}
|
||||
|
||||
class DefaultPlayerQueueState(
|
||||
private val state: PlayerState,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
backendService: BackendService,
|
||||
) : PlayerQueueState {
|
||||
private val _current = MutableStateFlow<Queue>(EmptyQueue)
|
||||
override val current: StateFlow<Queue> get() = _current.asStateFlow()
|
||||
|
||||
private val _entryIndex = MutableStateFlow(PlayerQueueState.INDEX_NONE)
|
||||
override val entryIndex: StateFlow<Int> get() = _entryIndex.asStateFlow()
|
||||
|
||||
private val _entry = MutableStateFlow<QueueEntry?>(null)
|
||||
override val entry: StateFlow<QueueEntry?> get() = _entry.asStateFlow()
|
||||
class QueueService internal constructor() : PlayerService(), Queue {
|
||||
private val suppliers = mutableListOf<QueueSupplier>()
|
||||
private var currentSupplierIndex = 0
|
||||
private var currentSupplierItemIndex = 0
|
||||
private val fetchedItems: MutableList<QueueEntry> = mutableListOf()
|
||||
|
||||
private var defaultOrderIndexProvider = DefaultOrderIndexProvider()
|
||||
private var orderIndexProvider: OrderIndexProvider = defaultOrderIndexProvider
|
||||
private var currentQueueIndicesPlayed = mutableListOf<Int>()
|
||||
|
||||
init {
|
||||
override val estimatedSize get() = max(fetchedItems.size, suppliers.sumOf { it.size })
|
||||
|
||||
private val _entryIndex = MutableStateFlow(Queue.INDEX_NONE)
|
||||
override val entryIndex: StateFlow<Int> get() = _entryIndex.asStateFlow()
|
||||
|
||||
private val _entry = MutableStateFlow<QueueEntry?>(null)
|
||||
override val entry: StateFlow<QueueEntry?> get() = _entry.asStateFlow()
|
||||
|
||||
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)
|
||||
|
||||
if (_entryIndex.value == Queue.INDEX_NONE) {
|
||||
coroutineScope.launch { 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()
|
||||
}
|
||||
|
||||
setIndex(0)
|
||||
}
|
||||
}
|
||||
// Preloading
|
||||
|
||||
private fun getNextIndices(amount: Int, usePlaybackOrder: Boolean, useRepeatMode: Boolean): Collection<Int> {
|
||||
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<QueueEntry> {
|
||||
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<QueueService>())
|
@ -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<Int>,
|
||||
currentIndex: Int,
|
||||
): Collection<Int> {
|
||||
// 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()
|
||||
|
@ -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<Int>,
|
||||
currentIndex: Int,
|
||||
): Collection<Int>
|
||||
|
@ -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<Int>,
|
||||
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
|
||||
}
|
||||
|
@ -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<Int>,
|
||||
currentIndex: Int,
|
||||
): Collection<Int> {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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<QueueEntry> = 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<QueueEntry>
|
||||
var pageOffset = buffer.size
|
@ -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?
|
||||
}
|
@ -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
|
||||
}
|
@ -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<QueueItem> {
|
||||
// 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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
@ -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,
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
|
@ -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!!)
|
||||
|
Loading…
Reference in New Issue
Block a user