mirror of
https://github.com/jellyfin/jellyfin-androidtv.git
synced 2026-10-26 16:45:23 +00:00
Add configurable playback buffer size setting (#5515)
This commit is contained in:
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()
|
||||
},
|
||||
|
||||
+11
@@ -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 {
|
||||
|
||||
+45
@@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
Reference in new issue
Block a user