Add configurable playback buffer size setting (#5515)

This commit is contained in:
Sergio Fernández authored and GitHub committed 2026-04-27 13:37:38 +02:00
1 parent 3584647f83
commit ab014cefd4
10 files changed
+154

No files matched your search

@@ -12,6 +12,7 @@ import androidx.media3.datasource.okhttp.OkHttpDataSource
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.UserSettingPreferences
import org.jellyfin.androidtv.preference.constant.BufferLength
import org.jellyfin.androidtv.ui.browsing.MainActivity
import org.jellyfin.androidtv.ui.playback.MediaManager
import org.jellyfin.androidtv.ui.playback.PlaybackLauncher
@@ -70,11 +71,16 @@ fun Scope.createPlaybackManager() = playbackManager(androidContext()) {
}
val userPreferences = get<UserPreferences>()
val bufferLength = userPreferences[UserPreferences.bufferLength]
val exoPlayerOptions = ExoPlayerOptions(
preferFfmpeg = userPreferences[UserPreferences.preferExoPlayerFfmpeg],
enableLibass = userPreferences[UserPreferences.assDirectPlay],
enableDebugLogging = userPreferences[UserPreferences.debuggingEnabled],
baseDataSourceFactory = get<HttpDataSource.Factory>(),
minBufferDuration = bufferLength.minBufferDuration,
maxBufferDuration = bufferLength.maxBufferDuration,
bufferForPlaybackDuration = bufferLength.bufferForPlaybackDuration,
bufferForPlaybackAfterRebufferDuration = bufferLength.bufferForPlaybackAfterRebufferDuration,
)
install(exoPlayerPlugin(get(), exoPlayerOptions))
@@ -10,6 +10,7 @@ import org.jellyfin.androidtv.preference.constant.BackdropBehavior
import org.jellyfin.androidtv.preference.constant.ClockBehavior
import org.jellyfin.androidtv.preference.constant.HEVCLevel
import org.jellyfin.androidtv.preference.constant.NextUpBehavior
import org.jellyfin.androidtv.preference.constant.BufferLength
import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior
import org.jellyfin.androidtv.preference.constant.StillWatchingBehavior
import org.jellyfin.androidtv.preference.constant.WatchedIndicatorBehavior
@@ -115,6 +116,11 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
*/
var userHEVCLevel = enumPreference("user_hevc_level", HEVCLevel.AUTO)
/**
* Playback buffer size preset.
*/
var bufferLength = enumPreference("buffer_length", BufferLength.AUTO)
/* Playback - Audio related */
/**
* Preferred behavior for audio streaming.
@@ -0,0 +1,44 @@
package org.jellyfin.androidtv.preference.constant
import org.jellyfin.androidtv.R
import org.jellyfin.preference.PreferenceEnum
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
enum class BufferLength(
override val nameRes: Int,
val minBufferDuration: Duration? = null,
val maxBufferDuration: Duration? = null,
val bufferForPlaybackDuration: Duration? = null,
val bufferForPlaybackAfterRebufferDuration: Duration? = null,
) : PreferenceEnum {
/**
* Use default buffer durations.
*/
AUTO(R.string.playback_buffer_auto),
/**
* Larger buffer, suitable for moderate or variable connections.
*/
@Suppress("MagicNumber")
LARGE(
nameRes = R.string.playback_buffer_large,
minBufferDuration = 50.seconds,
maxBufferDuration = 120.seconds,
bufferForPlaybackDuration = 2500.milliseconds,
bufferForPlaybackAfterRebufferDuration = 5.seconds,
),
/**
* Maximum buffer, intended for slow or satellite connections.
*/
@Suppress("MagicNumber")
EXTRA_LARGE(
nameRes = R.string.playback_buffer_extra_large,
minBufferDuration = 80.seconds,
maxBufferDuration = 240.seconds,
bufferForPlaybackDuration = 5.seconds,
bufferForPlaybackAfterRebufferDuration = 10.seconds,
),
}
@@ -32,6 +32,7 @@ import androidx.media3.common.Tracks;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.datasource.HttpDataSource;
import androidx.media3.exoplayer.DefaultLoadControl;
import androidx.media3.exoplayer.DefaultRenderersFactory;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.analytics.AnalyticsListener;
@@ -48,6 +49,7 @@ import androidx.media3.ui.PlayerView;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.data.compat.StreamInfo;
import org.jellyfin.androidtv.preference.UserPreferences;
import org.jellyfin.androidtv.preference.constant.BufferLength;
import org.jellyfin.androidtv.preference.constant.ZoomMode;
import org.jellyfin.sdk.api.client.ApiClient;
import org.jellyfin.sdk.model.api.MediaStream;
@@ -247,6 +249,21 @@ public class VideoManager {
exoPlayerBuilder.setMediaSourceFactory(new DefaultMediaSourceFactory(dataSourceFactory, extractorsFactory));
}
BufferLength bufferLength = userPreferences.get(UserPreferences.Companion.getBufferLength());
DefaultLoadControl loadControl;
if (bufferLength == BufferLength.LARGE) {
loadControl = new DefaultLoadControl.Builder()
.setBufferDurationsMs(50_000, 120_000, 2_500, 5_000)
.build();
} else if (bufferLength == BufferLength.EXTRA_LARGE) {
loadControl = new DefaultLoadControl.Builder()
.setBufferDurationsMs(80_000, 240_000, 5_000, 10_000)
.build();
} else {
loadControl = new DefaultLoadControl();
}
exoPlayerBuilder.setLoadControl(loadControl);
exoPlayerBuilder.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
@@ -34,6 +34,7 @@ import org.jellyfin.androidtv.ui.settings.screen.livetv.SettingsLiveTvGuideOptio
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackAVCLevelScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackAdvancedScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackAudioBehaviorScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackBufferLengthScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackCodecScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackHEVCLevelScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackInactivityPromptScreen
@@ -97,6 +98,7 @@ object Routes {
const val PLAYBACK_MAX_BITRATE = "/playback/max-bitrate"
const val PLAYBACK_REFRESH_RATE_SWITCHING_BEHAVIOR = "/playback/refresh-rate-switching-behavior"
const val PLAYBACK_ZOOM_MODE = "/playback/zoom-mode"
const val PLAYBACK_BUFFER_LENGTH = "/playback/buffer-length"
const val PLAYBACK_AUDIO_BEHAVIOR = "/playback/audio-behavior"
const val PLAYBACK_CODEC = "/playback/codec"
const val PLAYBACK_AVC_LEVEL = "/playback/codec/avc-level"
@@ -248,6 +250,9 @@ val routes = mapOf<String, RouteComposable>(
Routes.PLAYBACK_ZOOM_MODE to {
SettingsPlaybackZoomModeScreen()
},
Routes.PLAYBACK_BUFFER_LENGTH to {
SettingsPlaybackBufferLengthScreen()
},
Routes.PLAYBACK_AUDIO_BEHAVIOR to {
SettingsPlaybackAudioBehaviorScreen()
},
@@ -22,6 +22,7 @@ import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.constant.getQualityProfiles
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.UserSettingPreferences
import org.jellyfin.androidtv.preference.constant.BufferLength
import org.jellyfin.androidtv.ui.base.Text
import org.jellyfin.androidtv.ui.base.form.Checkbox
import org.jellyfin.androidtv.ui.base.form.RangeControl
@@ -107,6 +108,16 @@ fun SettingsPlaybackAdvancedScreen() {
}
}
item {
var bufferLength by rememberPreference(userPreferences, UserPreferences.bufferLength)
ListButton(
headingContent = { Text(stringResource(R.string.playback_buffer_length)) },
captionContent = { Text(stringResource(bufferLength.nameRes)) },
onClick = { router.push(Routes.PLAYBACK_BUFFER_LENGTH) },
)
}
item { ListSection(headingContent = { Text(stringResource(R.string.pref_video)) }) }
item {
@@ -0,0 +1,45 @@
package org.jellyfin.androidtv.ui.settings.screen.playback
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.BufferLength
import org.jellyfin.androidtv.ui.base.Text
import org.jellyfin.androidtv.ui.base.form.RadioButton
import org.jellyfin.androidtv.ui.base.list.ListButton
import org.jellyfin.androidtv.ui.base.list.ListSection
import org.jellyfin.androidtv.ui.navigation.LocalRouter
import org.jellyfin.androidtv.ui.settings.compat.rememberPreference
import org.jellyfin.androidtv.ui.settings.composable.SettingsColumn
import org.koin.compose.koinInject
@Composable
fun SettingsPlaybackBufferLengthScreen() {
val router = LocalRouter.current
val userPreferences = koinInject<UserPreferences>()
var bufferLength by rememberPreference(userPreferences, UserPreferences.bufferLength)
SettingsColumn {
item {
ListSection(
overlineContent = { Text(stringResource(R.string.pref_playback_advanced).uppercase()) },
headingContent = { Text(stringResource(R.string.playback_buffer_length)) },
)
}
items(BufferLength.entries) { entry ->
ListButton(
headingContent = { Text(stringResource(entry.nameRes)) },
trailingContent = { RadioButton(checked = bufferLength == entry) },
onClick = {
bufferLength = entry
router.back()
},
)
}
}
}
+4
View File
@@ -520,6 +520,10 @@
<string name="prefer_exoplayer_ffmpeg_content">Use FFmpeg to decode audio, even if platform codecs are available.</string>
<string name="default_video_zoom">Default zoom mode</string>
<string name="video_start_delay">Video start delay</string>
<string name="playback_buffer_length">Playback buffer size</string>
<string name="playback_buffer_auto">Auto</string>
<string name="playback_buffer_large">Large</string>
<string name="playback_buffer_extra_large">Extra large</string>
<string name="pref_mediasegment_actions">Media segment actions</string>
<string name="segment_action_ask_to_skip">Ask to skip</string>
<string name="segment_action_nothing">Do nothing</string>
@@ -15,6 +15,7 @@ import androidx.media3.common.VideoSize
import androidx.media3.common.text.CueGroup
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
@@ -109,7 +110,17 @@ class ExoPlayerBackend(
else renderersFactory
}
val loadControl = DefaultLoadControl.Builder()
.setBufferDurationsMs(
exoPlayerOptions.minBufferDuration?.inWholeMilliseconds?.toInt() ?: DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
exoPlayerOptions.maxBufferDuration?.inWholeMilliseconds?.toInt() ?: DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
exoPlayerOptions.bufferForPlaybackDuration?.inWholeMilliseconds?.toInt() ?: DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
exoPlayerOptions.bufferForPlaybackAfterRebufferDuration?.inWholeMilliseconds?.toInt() ?: DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
)
.build()
ExoPlayer.Builder(context)
.setLoadControl(loadControl)
.setRenderersFactory(renderersFactory)
.setTrackSelector(DefaultTrackSelector(context).apply {
setParameters(buildUponParameters().apply {
@@ -2,10 +2,15 @@ package org.jellyfin.playback.media3.exoplayer
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultHttpDataSource
import kotlin.time.Duration
data class ExoPlayerOptions(
val preferFfmpeg: Boolean = false,
val enableDebugLogging: Boolean = false,
val enableLibass: Boolean = false,
val baseDataSourceFactory: DataSource.Factory = DefaultHttpDataSource.Factory(),
val minBufferDuration: Duration? = null,
val maxBufferDuration: Duration? = null,
val bufferForPlaybackDuration: Duration? = null,
val bufferForPlaybackAfterRebufferDuration: Duration? = null,
)