Propagate max streaming bitrate from web apsettings to native player

This commit is contained in:
Niels van Velzen authored and Niels van Velzen committed 2026-09-13 16:33:42 +02:00
1 parent 83dcc6ece4
commit bc53439b8f
8 files changed
+48 -10

No files matched your search

@@ -1,9 +1,10 @@
export class ExoPlayerPlugin {
constructor({ events, playbackManager, loading }) {
constructor({ events, playbackManager, appSettings, loading }) {
window['ExoPlayer'] = this;
this.events = events;
this.playbackManager = playbackManager;
this.appSettings = appSettings;
this.loading = loading;
this.name = 'ExoPlayer';
@@ -26,8 +27,13 @@ export class ExoPlayerPlugin {
options.ids = options.items.map(item => item.Id);
delete options.items;
const preferences = {
maxStreamingBitrateLocal: this.appSettings.maxStreamingBitrate(true, 'Video'),
maxStreamingBitrateRemote: this.appSettings.maxStreamingBitrate(false, 'Video'),
};
this._paused = false;
this._nativePlayer.loadPlayer(JSON.stringify(options));
this._nativePlayer.loadPlayer(JSON.stringify(options), JSON.stringify(preferences));
this.loading.hide();
}
@@ -2,11 +2,13 @@ package org.jellyfin.mobile.bridge
import android.webkit.JavascriptInterface
import kotlinx.coroutines.channels.Channel
import kotlinx.serialization.json.Json
import org.jellyfin.mobile.app.AppPreferences
import org.jellyfin.mobile.events.ActivityEvent
import org.jellyfin.mobile.events.ActivityEventHandler
import org.jellyfin.mobile.player.interaction.PlayOptions
import org.jellyfin.mobile.player.interaction.PlayerEvent
import org.jellyfin.mobile.player.interaction.PlayerWebPreferences
import org.jellyfin.mobile.settings.VideoPlayerType
import org.jellyfin.sdk.model.extensions.ticks
import kotlin.time.Duration.Companion.milliseconds
@@ -22,10 +24,11 @@ class NativePlayer(
fun isEnabled() = appPreferences.videoPlayerType == VideoPlayerType.EXO_PLAYER
@JavascriptInterface
fun loadPlayer(args: String) {
PlayOptions.fromJson(args)?.let { options ->
activityEventHandler.emit(ActivityEvent.LaunchNativePlayer(options))
}
fun loadPlayer(playOptionsRaw: String, preferencesRaw: String) {
val playOptions = PlayOptions.fromJson(playOptionsRaw) ?: return
val preferences = Json.decodeFromString<PlayerWebPreferences>(preferencesRaw)
activityEventHandler.emit(ActivityEvent.LaunchNativePlayer(playOptions, preferences))
}
@JavascriptInterface
@@ -1,12 +1,13 @@
package org.jellyfin.mobile.events
import org.jellyfin.mobile.player.interaction.PlayOptions
import org.jellyfin.mobile.player.interaction.PlayerWebPreferences
import org.json.JSONArray
import java.util.UUID
sealed class ActivityEvent {
class ChangeFullscreen(val isFullscreen: Boolean) : ActivityEvent()
class LaunchNativePlayer(val playOptions: PlayOptions) : ActivityEvent()
class LaunchNativePlayer(val playOptions: PlayOptions, val preferences: PlayerWebPreferences? = null) : ActivityEvent()
class OpenUrl(val uri: String, val grantReadPermission: Boolean = false) : ActivityEvent()
class DownloadItems(val itemIds: Collection<UUID>) : ActivityEvent()
class CastMessage(val action: String, val args: JSONArray) : ActivityEvent()
@@ -62,6 +62,7 @@ class ActivityEventHandler(
is ActivityEvent.LaunchNativePlayer -> {
val args = Bundle().apply {
putParcelable(Constants.EXTRA_MEDIA_PLAY_OPTIONS, event.playOptions)
putParcelable(Constants.EXTRA_WEB_PREFERENCES, event.preferences)
}
supportFragmentManager.addFragment<PlayerFragment>(args)
}
@@ -0,0 +1,12 @@
package org.jellyfin.mobile.player.interaction
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Serializable
@Parcelize
data class PlayerWebPreferences(
val maxStreamingBitrateLocal: Int,
val maxStreamingBitrateRemote: Int,
) : Parcelable
@@ -17,6 +17,7 @@ import org.jellyfin.mobile.player.PlayerException
import org.jellyfin.mobile.player.PlayerViewModel
import org.jellyfin.mobile.player.deviceprofile.DeviceProfileBuilder
import org.jellyfin.mobile.player.interaction.PlayOptions
import org.jellyfin.mobile.player.interaction.PlayerWebPreferences
import org.jellyfin.mobile.player.source.ExternalSubtitleStream
import org.jellyfin.mobile.player.source.JellyfinMediaSource
import org.jellyfin.mobile.player.source.LocalJellyfinMediaSource
@@ -24,6 +25,7 @@ import org.jellyfin.mobile.player.source.MediaSourceResolver
import org.jellyfin.mobile.player.source.PlaybackDetails
import org.jellyfin.mobile.player.source.RemoteJellyfinMediaSource
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.systemApi
import org.jellyfin.sdk.api.client.extensions.videosApi
import org.jellyfin.sdk.api.operations.VideosApi
import org.jellyfin.sdk.model.api.MediaProtocol
@@ -67,7 +69,7 @@ class QueueManager(
*
* @return an error of type [PlayerException] or null on success.
*/
suspend fun initializePlaybackQueue(playOptions: PlayOptions): PlayerException? {
suspend fun initializePlaybackQueue(playOptions: PlayOptions, preferences: PlayerWebPreferences? = null): PlayerException? {
currentQueue = playOptions.ids
currentQueueIndex = playOptions.startIndex
resetPlaybackFallback()
@@ -77,6 +79,16 @@ class QueueManager(
else -> playOptions.mediaSourceId?.toUUIDOrNull()
} ?: return PlayerException.InvalidPlayOptions()
val maxStreamingBitrate = preferences?.let {
withContext(Dispatchers.IO) {
runCatching {
val endpoint by apiClient.systemApi.getEndpointInfo()
if (endpoint.isInNetwork) it.maxStreamingBitrateLocal
else it.maxStreamingBitrateRemote
}
}.getOrNull()
}
when (playOptions.playFromDownloads) {
true -> playOptions.mediaSourceId?.let {
startDownloadPlayback(
@@ -87,7 +99,7 @@ class QueueManager(
else -> startRemotePlayback(
itemId = itemId,
mediaSourceId = playOptions.mediaSourceId,
maxStreamingBitrate = null,
maxStreamingBitrate = maxStreamingBitrate,
startTime = playOptions.startPosition,
audioStreamIndex = playOptions.audioStreamIndex,
subtitleStreamIndex = playOptions.subtitleStreamIndex,
@@ -38,6 +38,7 @@ import org.jellyfin.mobile.databinding.FragmentPlayerBinding
import org.jellyfin.mobile.player.PlayerException
import org.jellyfin.mobile.player.PlayerViewModel
import org.jellyfin.mobile.player.interaction.PlayOptions
import org.jellyfin.mobile.player.interaction.PlayerWebPreferences
import org.jellyfin.mobile.player.ui.playermenuhelper.PlayerMenuHelper
import org.jellyfin.mobile.utils.AndroidVersion
import org.jellyfin.mobile.utils.BackPressInterceptor
@@ -136,11 +137,12 @@ class PlayerFragment : Fragment(), BackPressInterceptor {
lifecycleScope.launch {
val context = requireContext()
val playOptions = requireArguments().getParcelableCompat<PlayOptions>(Constants.EXTRA_MEDIA_PLAY_OPTIONS)
val preferences = requireArguments().getParcelableCompat<PlayerWebPreferences>(Constants.EXTRA_WEB_PREFERENCES)
if (playOptions == null) {
context.toast(R.string.player_error_invalid_play_options)
return@launch
}
when (viewModel.queueManager.initializePlaybackQueue(playOptions)) {
when (viewModel.queueManager.initializePlaybackQueue(playOptions, preferences)) {
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)
@@ -143,6 +143,7 @@ object Constants {
// Video player intent extras
const val EXTRA_MEDIA_PLAY_OPTIONS = "org.jellyfin.mobile.MEDIA_PLAY_OPTIONS"
const val EXTRA_WEB_PREFERENCES = "org.jellyfin.mobile.WEB_PREFERENCES"
// External player result actions
const val MPV_PLAYER_RESULT_ACTION = "is.xyz.mpv.MPVActivity.result"