Skip to content

Commit

Permalink
Rewrite queuing
Browse files Browse the repository at this point in the history
  • Loading branch information
nielsvanvelzen committed Aug 5, 2024
1 parent a8c9f05 commit 0f38bd1
Show file tree
Hide file tree
Showing 25 changed files with 291 additions and 216 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,15 +40,15 @@ 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

override val currentAudioQueueSize: Int
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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)

Expand All @@ -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(
Expand Down Expand Up @@ -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()
}

Expand All @@ -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
}
}

Expand All @@ -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) {
Expand All @@ -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
Expand Down
9 changes: 1 addition & 8 deletions playback/core/src/main/kotlin/PlaybackManager.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
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

class PlaybackManager internal constructor(
val backend: PlayerBackend,
private val services: MutableList<PlayerService>,
mediaStreamResolvers: List<MediaStreamResolver>,
val options: PlaybackManagerOptions,
parentJob: Job? = null,
) {
Expand All @@ -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) {
Expand Down
9 changes: 8 additions & 1 deletion playback/core/src/main/kotlin/PlaybackManagerBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -44,14 +47,18 @@ 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(
playerVolumeState = volumeState,
defaultRewindAmount = defaultRewindAmount ?: { 10.seconds },
defaultFastForwardAmount = defaultFastForwardAmount ?: { 10.seconds },
)
return PlaybackManager(backends.first(), services, mediaStreamResolvers, options)
return PlaybackManager(backends.first(), services, options)
}
}

Expand Down
18 changes: 5 additions & 13 deletions playback/core/src/main/kotlin/PlayerState.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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>
Expand All @@ -35,7 +30,7 @@ interface PlayerState {
val positionInfo: PositionInfo

// Queue management
fun play(playQueue: Queue)
fun play()
fun stop()

// Pausing
Expand All @@ -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)
Expand Down Expand Up @@ -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()
}

Expand All @@ -117,7 +109,7 @@ class MutablePlayerState(

override fun stop() {
backendService.backend?.stop()
queue.replaceQueue(EmptyQueue)
queue?.clear()
}

override fun seek(to: Duration) {
Expand Down
Loading

0 comments on commit 0f38bd1

Please sign in to comment.