Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite media queuing #3833

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading