diff --git a/app/src/main/java/org/jellyfin/mobile/player/PlayerViewModel.kt b/app/src/main/java/org/jellyfin/mobile/player/PlayerViewModel.kt index 26dd8251..388b6d24 100644 --- a/app/src/main/java/org/jellyfin/mobile/player/PlayerViewModel.kt +++ b/app/src/main/java/org/jellyfin/mobile/player/PlayerViewModel.kt @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.analytics.DefaultAnalyticsCollector import com.google.android.exoplayer2.mediacodec.MediaCodecDecoderException import com.google.android.exoplayer2.mediacodec.MediaCodecInfo import com.google.android.exoplayer2.mediacodec.MediaCodecSelector +import com.google.android.exoplayer2.source.MediaSource import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.util.Clock import com.google.android.exoplayer2.util.EventLogger @@ -88,9 +89,9 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application), // Media source handling private val trackSelector = DefaultTrackSelector(getApplication()) val trackSelectionHelper = TrackSelectionHelper(this, trackSelector) - val mediaQueueManager = QueueManager(this) + val queueManager = QueueManager(this) val mediaSourceOrNull: JellyfinMediaSource? - get() = mediaQueueManager.mediaQueue.value?.jellyfinMediaSource + get() = queueManager.currentMediaSourceOrNull // ExoPlayer private val _player = MutableLiveData() @@ -239,22 +240,22 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application), _player.value = null } - fun load(queueItem: QueueManager.QueueItem.Loaded, playWhenReady: Boolean) { + fun load(jellyfinMediaSource: JellyfinMediaSource, exoMediaSource: MediaSource, playWhenReady: Boolean) { val player = playerOrNull ?: return - player.setMediaSource(queueItem.exoMediaSource) + player.setMediaSource(exoMediaSource) player.prepare() initialTracksSelected.set(false) - val startTime = queueItem.jellyfinMediaSource.startTimeMs + val startTime = jellyfinMediaSource.startTimeMs if (startTime > 0) player.seekTo(startTime) player.playWhenReady = playWhenReady - mediaSession.setMetadata(queueItem.jellyfinMediaSource.toMediaMetadata()) + mediaSession.setMetadata(jellyfinMediaSource.toMediaMetadata()) viewModelScope.launch { - player.reportPlaybackStart(queueItem.jellyfinMediaSource) + player.reportPlaybackStart(jellyfinMediaSource) } } @@ -286,8 +287,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application), } analyticsCollector = buildAnalyticsCollector() setupPlayer() - mediaQueueManager.mediaQueue.value?.jellyfinMediaSource?.startTimeMs = playedTime - mediaQueueManager.tryRestartPlayback() + queueManager.currentMediaSourceOrNull?.startTimeMs = playedTime + queueManager.tryRestartPlayback() } private suspend fun Player.reportPlaybackStart(mediaSource: JellyfinMediaSource) { @@ -409,7 +410,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application), // Skip to previous element player.currentPosition <= Constants.MAX_SKIP_TO_PREV_MS -> viewModelScope.launch { pause() - if (!mediaQueueManager.previous()) { + if (!queueManager.previous()) { // Skip to previous failed, go to start of video anyway playerOrNull?.seekTo(0) play() @@ -422,7 +423,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application), fun skipToNext() { viewModelScope.launch { - mediaQueueManager.next() + queueManager.next() } } @@ -441,7 +442,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application), } suspend fun changeBitrate(bitrate: Int?): Boolean { - return mediaQueueManager.changeBitrate(bitrate) + return queueManager.changeBitrate(bitrate) } /** @@ -499,9 +500,11 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application), // Update media session var playbackActions = SUPPORTED_VIDEO_PLAYER_PLAYBACK_ACTIONS - mediaQueueManager.mediaQueue.value?.let { queueItem -> - if (queueItem.hasPrevious()) playbackActions = playbackActions or PlaybackState.ACTION_SKIP_TO_PREVIOUS - if (queueItem.hasNext()) playbackActions = playbackActions or PlaybackState.ACTION_SKIP_TO_NEXT + if (queueManager.hasPrevious()) { + playbackActions = playbackActions or PlaybackState.ACTION_SKIP_TO_PREVIOUS + } + if (queueManager.hasNext()) { + playbackActions = playbackActions or PlaybackState.ACTION_SKIP_TO_NEXT } mediaSession.setPlaybackState(player, playbackActions) @@ -513,7 +516,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application), } Player.STATE_ENDED -> { reportPlaybackStop() - if (!mediaQueueManager.next()) { + if (!queueManager.next()) { releasePlayer() } } @@ -530,7 +533,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application), } fallbackPreferExtensionRenderers = true setupPlayer() - mediaQueueManager.tryRestartPlayback() + queueManager.tryRestartPlayback() } else { _error.postValue(error.localizedMessage.orEmpty()) } diff --git a/app/src/main/java/org/jellyfin/mobile/player/TrackSelectionHelper.kt b/app/src/main/java/org/jellyfin/mobile/player/TrackSelectionHelper.kt index c93e8e4b..af35b265 100644 --- a/app/src/main/java/org/jellyfin/mobile/player/TrackSelectionHelper.kt +++ b/app/src/main/java/org/jellyfin/mobile/player/TrackSelectionHelper.kt @@ -40,7 +40,7 @@ class TrackSelectionHelper( // For transcoding and external streams, we need to restart playback if (mediaSource.playMethod == PlayMethod.TRANSCODE || selectedMediaStream.isExternal) { - return viewModel.mediaQueueManager.selectAudioStreamAndRestartPlayback(selectedMediaStream) + return viewModel.queueManager.selectAudioStreamAndRestartPlayback(selectedMediaStream) } return selectPlayerAudioTrack(mediaSource, selectedMediaStream, initial = false).also { success -> @@ -95,7 +95,7 @@ class TrackSelectionHelper( selectedMediaStream?.deliveryMethod == SubtitleDeliveryMethod.ENCODE || mediaSource.selectedSubtitleStream?.deliveryMethod == SubtitleDeliveryMethod.ENCODE ) { - return viewModel.mediaQueueManager.selectSubtitleStreamAndRestartPlayback(selectedMediaStream) + return viewModel.queueManager.selectSubtitleStreamAndRestartPlayback(selectedMediaStream) } return selectSubtitleTrack(mediaSource, selectedMediaStream, initial = false).also { success -> diff --git a/app/src/main/java/org/jellyfin/mobile/player/interaction/PlayerNotificationHelper.kt b/app/src/main/java/org/jellyfin/mobile/player/interaction/PlayerNotificationHelper.kt index 769719a0..dfa99340 100644 --- a/app/src/main/java/org/jellyfin/mobile/player/interaction/PlayerNotificationHelper.kt +++ b/app/src/main/java/org/jellyfin/mobile/player/interaction/PlayerNotificationHelper.kt @@ -66,8 +66,9 @@ class PlayerNotificationHelper(private val viewModel: PlayerViewModel) : KoinCom fun postNotification() { val nm = notificationManager ?: return val player = viewModel.playerOrNull ?: return - val queueItem = viewModel.mediaQueueManager.mediaQueue.value ?: return - val mediaSource = queueItem.jellyfinMediaSource + val currentMediaSource = viewModel.queueManager.currentMediaSourceOrNull ?: return + val hasPrevious = viewModel.queueManager.hasPrevious() + val hasNext = viewModel.queueManager.hasNext() val playbackState = player.playbackState if (playbackState != Player.STATE_READY && playbackState != Player.STATE_BUFFERING) return @@ -76,7 +77,7 @@ class PlayerNotificationHelper(private val viewModel: PlayerViewModel) : KoinCom viewModel.viewModelScope.launch { val mediaIcon: Bitmap? = withContext(Dispatchers.IO) { - loadImage(mediaSource) + loadImage(currentMediaSource) } val style = Notification.MediaStyle().apply { @@ -93,12 +94,12 @@ class PlayerNotificationHelper(private val viewModel: PlayerViewModel) : KoinCom } setSmallIcon(R.drawable.ic_notification) mediaIcon?.let(::setLargeIcon) - setContentTitle(mediaSource.name) - mediaSource.item?.artists?.joinToString()?.let(::setContentText) + setContentTitle(currentMediaSource.name) + currentMediaSource.item?.artists?.joinToString()?.let(::setContentText) setStyle(style) setVisibility(Notification.VISIBILITY_PUBLIC) when { - queueItem.hasPrevious() -> addAction(generateAction(PlayerNotificationAction.PREVIOUS)) + hasPrevious -> addAction(generateAction(PlayerNotificationAction.PREVIOUS)) else -> addAction(generateAction(PlayerNotificationAction.REWIND)) } val playbackAction = when { @@ -107,7 +108,7 @@ class PlayerNotificationHelper(private val viewModel: PlayerViewModel) : KoinCom } addAction(generateAction(playbackAction)) when { - queueItem.hasNext() -> addAction(generateAction(PlayerNotificationAction.NEXT)) + hasNext -> addAction(generateAction(PlayerNotificationAction.NEXT)) else -> addAction(generateAction(PlayerNotificationAction.FAST_FORWARD)) } setContentIntent(buildContentIntent()) diff --git a/app/src/main/java/org/jellyfin/mobile/player/queue/QueueManager.kt b/app/src/main/java/org/jellyfin/mobile/player/queue/QueueManager.kt index bf229d14..91e59311 100644 --- a/app/src/main/java/org/jellyfin/mobile/player/queue/QueueManager.kt +++ b/app/src/main/java/org/jellyfin/mobile/player/queue/QueueManager.kt @@ -39,13 +39,16 @@ class QueueManager( private val mediaSourceResolver: MediaSourceResolver by inject() private val deviceProfileBuilder: DeviceProfileBuilder by inject() private val deviceProfile = deviceProfileBuilder.getDeviceProfile() - private val _mediaQueue: MutableLiveData = MutableLiveData() - val mediaQueue: LiveData get() = _mediaQueue - private val currentMediaSource: JellyfinMediaSource? - get() = _mediaQueue.value?.jellyfinMediaSource + private var currentQueue: List = emptyList() + private var currentQueueIndex: Int = 0 - private var currentPlayOptions: PlayOptions? = null + private val _currentMediaSource: MutableLiveData = MutableLiveData() + val currentMediaSource: LiveData + get() = _currentMediaSource + + inline val currentMediaSourceOrNull: JellyfinMediaSource? + get() = currentMediaSource.value /** * Handle initial playback options from fragment. @@ -53,36 +56,63 @@ class QueueManager( * * @return an error of type [PlayerException] or null on success. */ - suspend fun startPlayback(playOptions: PlayOptions, playWhenReady: Boolean): PlayerException? { - if (playOptions != currentPlayOptions) { - val itemId = playOptions.run { - ids.getOrNull(startIndex) ?: mediaSourceId?.toUUIDOrNull() // fallback if ids is empty - } ?: return PlayerException.InvalidPlayOptions() + suspend fun initializePlaybackQueue(playOptions: PlayOptions): PlayerException? { + currentQueue = playOptions.ids + currentQueueIndex = playOptions.startIndex - mediaSourceResolver.resolveMediaSource( - itemId = itemId, - mediaSourceId = playOptions.mediaSourceId, - deviceProfile = deviceProfile, - maxStreamingBitrate = playOptions.maxBitrate, - startTimeTicks = playOptions.startPositionTicks, - audioStreamIndex = playOptions.audioStreamIndex, - subtitleStreamIndex = playOptions.subtitleStreamIndex, - ).onSuccess { jellyfinMediaSource -> - // Ensure transcoding of the current element is stopped - currentMediaSource?.let { oldMediaSource -> - viewModel.stopTranscoding(oldMediaSource) - } + val itemId = when { + currentQueue.isNotEmpty() -> currentQueue[currentQueueIndex] + else -> playOptions.mediaSourceId?.toUUIDOrNull() + } ?: return PlayerException.InvalidPlayOptions() - // Apply new queue - val previous = QueueItem.Stub(playOptions.ids.take(playOptions.startIndex)) - val next = QueueItem.Stub(playOptions.ids.drop(playOptions.startIndex + 1)) - val new = createQueueItem(jellyfinMediaSource, previous, next) - currentPlayOptions = playOptions - new.play(playWhenReady) - }.onFailure { error -> - // Should always be of this type, other errors are silently dropped - return error as? PlayerException + startPlayback( + itemId = itemId, + mediaSourceId = playOptions.mediaSourceId, + maxStreamingBitrate = playOptions.maxBitrate, + startTimeTicks = playOptions.startPositionTicks, + audioStreamIndex = playOptions.audioStreamIndex, + subtitleStreamIndex = playOptions.subtitleStreamIndex, + playWhenReady = true, + ) + + return null + } + + /** + * Play a specific media item specified by [itemId] and [mediaSourceId]. + * + * @return an error of type [PlayerException] or null on success. + */ + private suspend fun startPlayback( + itemId: UUID, + mediaSourceId: String?, + maxStreamingBitrate: Int?, + startTimeTicks: Long? = null, + audioStreamIndex: Int? = null, + subtitleStreamIndex: Int? = null, + playWhenReady: Boolean = true, + ): PlayerException? { + mediaSourceResolver.resolveMediaSource( + itemId = itemId, + mediaSourceId = mediaSourceId, + deviceProfile = deviceProfile, + maxStreamingBitrate = maxStreamingBitrate, + startTimeTicks = startTimeTicks, + audioStreamIndex = audioStreamIndex, + subtitleStreamIndex = subtitleStreamIndex, + ).onSuccess { jellyfinMediaSource -> + // Ensure transcoding of the current element is stopped + currentMediaSourceOrNull?.let { oldMediaSource -> + viewModel.stopTranscoding(oldMediaSource) } + + _currentMediaSource.value = jellyfinMediaSource + + // Load new media source + viewModel.load(jellyfinMediaSource, prepareStreams(jellyfinMediaSource), playWhenReady) + }.onFailure { error -> + // Should always be of this type, other errors are silently dropped + return error as? PlayerException } return null } @@ -91,70 +121,60 @@ class QueueManager( * Reinitialize current media source without changing settings */ fun tryRestartPlayback() { - _mediaQueue.value?.play() + val currentMediaSource = currentMediaSourceOrNull ?: return + + viewModel.load(currentMediaSource, prepareStreams(currentMediaSource), playWhenReady = true) } /** * Change the maximum bitrate to the specified value. */ suspend fun changeBitrate(bitrate: Int?): Boolean { - val currentPlayOptions = currentPlayOptions ?: return false + val currentMediaSource = currentMediaSourceOrNull ?: return false // Bitrate didn't change, ignore - if (currentPlayOptions.maxBitrate == bitrate) return true + if (currentMediaSource.maxStreamingBitrate == bitrate) return true val currentPlayState = viewModel.getStateAndPause() ?: return false - val playOptions = currentPlayOptions.copy( - startPositionTicks = currentPlayState.position * Constants.TICKS_PER_MILLISECOND, - maxBitrate = bitrate, - ) - return startPlayback(playOptions, currentPlayState.playWhenReady) == null + return startPlayback( + itemId = currentMediaSource.itemId, + mediaSourceId = currentMediaSource.id, + maxStreamingBitrate = bitrate, + startTimeTicks = currentPlayState.position * Constants.TICKS_PER_MILLISECOND, + audioStreamIndex = currentMediaSource.selectedAudioStreamIndex, + subtitleStreamIndex = currentMediaSource.selectedSubtitleStreamIndex, + playWhenReady = currentPlayState.playWhenReady, + ) == null } - @CheckResult - private fun createQueueItem(jellyfinMediaSource: JellyfinMediaSource, previous: QueueItem, next: QueueItem): QueueItem.Loaded { - val exoMediaSource = prepareStreams(jellyfinMediaSource) - return QueueItem.Loaded(jellyfinMediaSource, exoMediaSource, previous, next) - } + fun hasPrevious(): Boolean = currentQueue.isNotEmpty() && currentQueueIndex > 0 + + fun hasNext(): Boolean = currentQueue.isNotEmpty() && currentQueueIndex < currentQueue.lastIndex suspend fun previous(): Boolean { - val current = _mediaQueue.value ?: return false - when (val previous = current.previous) { - is QueueItem.Loaded -> { - previous.play() - } - is QueueItem.Stub -> { - val previousId = previous.ids.lastOrNull() ?: return false - val jellyfinMediaSource = mediaSourceResolver.resolveMediaSource( - itemId = previousId, - deviceProfile = deviceProfile, - ).getOrNull() ?: return false + if (!hasPrevious()) return false - val previousPrevious = QueueItem.Stub(previous.ids.dropLast(1)) - createQueueItem(jellyfinMediaSource, previousPrevious, current).play() - } - } + val currentMediaSource = currentMediaSourceOrNull ?: return false + + startPlayback( + itemId = currentQueue[--currentQueueIndex], + mediaSourceId = null, + maxStreamingBitrate = currentMediaSource.maxStreamingBitrate, + ) return true } suspend fun next(): Boolean { - val current = _mediaQueue.value ?: return false - when (val next = current.next) { - is QueueItem.Loaded -> { - next.play() - } - is QueueItem.Stub -> { - val nextId = next.ids.firstOrNull() ?: return false - val jellyfinMediaSource = mediaSourceResolver.resolveMediaSource( - itemId = nextId, - deviceProfile = deviceProfile, - ).getOrNull() ?: return false + if (!hasNext()) return false - val nextNext = QueueItem.Stub(next.ids.drop(1)) - createQueueItem(jellyfinMediaSource, current, nextNext).play() - } - } + val currentMediaSource = currentMediaSourceOrNull ?: return false + + startPlayback( + itemId = currentQueue[++currentQueueIndex], + mediaSourceId = null, + maxStreamingBitrate = currentMediaSource.maxStreamingBitrate, + ) return true } @@ -268,15 +288,18 @@ class QueueManager( */ suspend fun selectAudioStreamAndRestartPlayback(stream: MediaStream): Boolean { require(stream.type == MediaStreamType.AUDIO) - val currentPlayOptions = currentPlayOptions ?: return false + val currentMediaSource = currentMediaSourceOrNull ?: return false val currentPlayState = viewModel.getStateAndPause() ?: return false - val playOptions = currentPlayOptions.copy( - startPositionTicks = currentPlayState.position * Constants.TICKS_PER_MILLISECOND, + startPlayback( + itemId = currentMediaSource.itemId, + mediaSourceId = currentMediaSource.id, + maxStreamingBitrate = currentMediaSource.maxStreamingBitrate, + startTimeTicks = currentPlayState.position * Constants.TICKS_PER_MILLISECOND, audioStreamIndex = stream.index, + subtitleStreamIndex = currentMediaSource.selectedSubtitleStreamIndex, + playWhenReady = currentPlayState.playWhenReady, ) - - startPlayback(playOptions, currentPlayState.playWhenReady) return true } @@ -289,43 +312,18 @@ class QueueManager( */ suspend fun selectSubtitleStreamAndRestartPlayback(stream: MediaStream?): Boolean { require(stream == null || stream.type == MediaStreamType.SUBTITLE) - val currentPlayOptions = currentPlayOptions ?: return false + val currentMediaSource = currentMediaSourceOrNull ?: return false val currentPlayState = viewModel.getStateAndPause() ?: return false - val playOptions = currentPlayOptions.copy( - startPositionTicks = currentPlayState.position * Constants.TICKS_PER_MILLISECOND, + startPlayback( + itemId = currentMediaSource.itemId, + mediaSourceId = currentMediaSource.id, + maxStreamingBitrate = currentMediaSource.maxStreamingBitrate, + startTimeTicks = currentPlayState.position * Constants.TICKS_PER_MILLISECOND, + audioStreamIndex = currentMediaSource.selectedAudioStreamIndex, subtitleStreamIndex = stream?.index ?: -1, // -1 disables subtitles, null would select the default subtitle + playWhenReady = currentPlayState.playWhenReady, ) - - startPlayback(playOptions, currentPlayState.playWhenReady) return true } - - sealed class QueueItem { - class Loaded( - val jellyfinMediaSource: JellyfinMediaSource, - val exoMediaSource: MediaSource, - val previous: QueueItem, - val next: QueueItem, - ) : QueueItem() { - fun hasPrevious(): Boolean = when (previous) { - is Loaded -> true - is Stub -> previous.ids.isNotEmpty() - } - - fun hasNext(): Boolean = when (next) { - is Loaded -> true - is Stub -> next.ids.isNotEmpty() - } - } - - class Stub( - val ids: List, - ) : QueueItem() - } - - private fun QueueItem.Loaded.play(playWhenReady: Boolean = true) { - _mediaQueue.value = this - viewModel.load(this, playWhenReady) - } } diff --git a/app/src/main/java/org/jellyfin/mobile/player/source/JellyfinMediaSource.kt b/app/src/main/java/org/jellyfin/mobile/player/source/JellyfinMediaSource.kt index 773c618f..8a1031b3 100644 --- a/app/src/main/java/org/jellyfin/mobile/player/source/JellyfinMediaSource.kt +++ b/app/src/main/java/org/jellyfin/mobile/player/source/JellyfinMediaSource.kt @@ -50,6 +50,13 @@ class JellyfinMediaSource( var selectedSubtitleStream: MediaStream? = null private set + val selectedAudioStreamIndex: Int? + get() = selectedAudioStream?.index + val selectedSubtitleStreamIndex: Int + // -1 disables subtitles, null would select the default subtitle + // If the default should be played, it would be explicitly set above + get() = selectedSubtitleStream?.index ?: -1 + init { // Classify MediaStreams val audio = ArrayList() diff --git a/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerFragment.kt b/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerFragment.kt index 46bce7ab..24175ba8 100644 --- a/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerFragment.kt +++ b/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerFragment.kt @@ -101,10 +101,8 @@ class PlayerFragment : Fragment() { val safeMessage = message.ifEmpty { requireContext().getString(R.string.player_error_unspecific_exception) } requireContext().toast(safeMessage) } - viewModel.mediaQueueManager.mediaQueue.observe(this) { queueItem -> - val jellyfinMediaSource = queueItem.jellyfinMediaSource - - if (jellyfinMediaSource.selectedVideoStream?.isLandscape == false) { + viewModel.queueManager.currentMediaSource.observe(this) { mediaSource -> + if (mediaSource.selectedVideoStream?.isLandscape == false) { // For portrait videos, immediately enable fullscreen playerFullscreenHelper.enableFullscreen() } else if (appPreferences.exoPlayerStartLandscapeVideoInLandscape) { @@ -113,8 +111,8 @@ class PlayerFragment : Fragment() { } // Update title and player menus - toolbar.title = jellyfinMediaSource.name - playerMenus?.onQueueItemChanged(queueItem) + toolbar.title = mediaSource.name + playerMenus?.onQueueItemChanged(mediaSource, viewModel.queueManager.hasNext()) } // Handle fragment arguments, extract playback options and start playback @@ -125,7 +123,7 @@ class PlayerFragment : Fragment() { context.toast(R.string.player_error_invalid_play_options) return@launch } - when (viewModel.mediaQueueManager.startPlayback(playOptions, playWhenReady = true)) { + when (viewModel.queueManager.initializePlaybackQueue(playOptions)) { is PlayerException.InvalidPlayOptions -> context.toast(R.string.player_error_invalid_play_options) is PlayerException.NetworkFailure -> context.toast(R.string.player_error_network_failure) is PlayerException.UnsupportedContent -> context.toast(R.string.player_error_unsupported_content) diff --git a/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerMenus.kt b/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerMenus.kt index b10c3c13..cd75e34d 100644 --- a/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerMenus.kt +++ b/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerMenus.kt @@ -15,7 +15,7 @@ import org.jellyfin.mobile.R import org.jellyfin.mobile.databinding.ExoPlayerControlViewBinding import org.jellyfin.mobile.databinding.FragmentPlayerBinding import org.jellyfin.mobile.player.qualityoptions.QualityOptionsProvider -import org.jellyfin.mobile.player.queue.QueueManager +import org.jellyfin.mobile.player.source.JellyfinMediaSource import org.jellyfin.sdk.model.api.MediaStream import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -101,10 +101,9 @@ class PlayerMenus( } } - fun onQueueItemChanged(queueItem: QueueManager.QueueItem.Loaded) { - nextButton.isEnabled = queueItem.hasNext() - - val mediaSource = queueItem.jellyfinMediaSource + fun onQueueItemChanged(mediaSource: JellyfinMediaSource, hasNext: Boolean) { + // previousButton is always enabled and will rewind if at the start of the queue + nextButton.isEnabled = hasNext val videoStream = mediaSource.selectedVideoStream