From 083d4e4fa82d252564964566c7d4bbd732490319 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Sat, 27 Jun 2026 14:29:37 -0400 Subject: [PATCH] Add experimental settings & video tunneling (#1624) ## Description This PR introduces the concept of "experimental" settings or features. These may cause instabilities or may need further testing. They may be changed or removed any time. Also, the developers won't provide lots of support for them. The goal is expose some specialized settings that might only apply to a small subset of users or test out tweaks to playback. The first experimental feature is a toggle to enable [video tunneling](https://developer.android.com/media/media3/exoplayer/track-selection#tunneling). ### Related issues Related to #1283 ### Testing Emulator ## Screenshots N/A ## AI or LLM usage None --- .github/ISSUE_TEMPLATE/00-bug.yml | 12 +++- .github/ISSUE_TEMPLATE/01-playback.yml | 22 +++++-- DEVELOPMENT.md | 6 ++ .../wholphin/preferences/AppPreference.kt | 8 +++ .../preferences/AppPreferencesSerializer.kt | 13 +++++ .../preferences/ExperimentalPreference.kt | 58 +++++++++++++++++++ .../wholphin/services/PlayerFactory.kt | 17 ++++-- .../wholphin/ui/playback/PlaybackViewModel.kt | 2 +- .../ui/preferences/PreferenceUtils.kt | 1 + .../ui/preferences/PreferencesContent.kt | 46 +++++++++++++++ .../ui/slideshow/SlideshowViewModel.kt | 2 +- app/src/main/proto/WholphinDataStore.proto | 6 ++ app/src/main/res/values/strings.xml | 4 ++ 13 files changed, 183 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/preferences/ExperimentalPreference.kt diff --git a/.github/ISSUE_TEMPLATE/00-bug.yml b/.github/ISSUE_TEMPLATE/00-bug.yml index 1aab31da..27e43e4b 100644 --- a/.github/ISSUE_TEMPLATE/00-bug.yml +++ b/.github/ISSUE_TEMPLATE/00-bug.yml @@ -1,6 +1,6 @@ name: "Bug Report" description: Report a bug -title: "" +title: "" labels: [ "bug" ] @@ -23,6 +23,16 @@ body: 3. See error validations: required: false + - type: dropdown + id: experimental + attributes: + label: Experimental settings + description: Do you have experiment settings turn on? + options: + - "Yes" + - "No" + validations: + required: true - type: input id: app-version attributes: diff --git a/.github/ISSUE_TEMPLATE/01-playback.yml b/.github/ISSUE_TEMPLATE/01-playback.yml index cbb287b5..00ee97f3 100644 --- a/.github/ISSUE_TEMPLATE/01-playback.yml +++ b/.github/ISSUE_TEMPLATE/01-playback.yml @@ -1,6 +1,6 @@ name: "Playback Problem" description: Report an issue with playback -title: "<title>" +title: "" labels: [ "bug", "playback" ] @@ -12,12 +12,14 @@ body: description: Please enter a description of the playback issue validations: required: true - - type: textarea - id: mediainfo + - type: dropdown + id: experimental attributes: - label: "Media info" - description: Please enter details about what media you are trying to play (video or audio codecs, container, etc) - placeholder: Use mediainfo or "Send media info to server" in the app to gather this + label: Experimental settings + description: Do you have experiment settings turn on? + options: + - "Yes" + - "No" validations: required: true - type: dropdown @@ -32,6 +34,14 @@ body: - External App (please specify) validations: required: true + - type: textarea + id: mediainfo + attributes: + label: "Media info" + description: Please enter details about what media you are trying to play (video or audio codecs, container, etc) + placeholder: Use mediainfo or "Send media info to server" in the app to gather this + validations: + required: true - type: input id: app-version attributes: diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index bdc7dfa5..0d818e21 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -97,3 +97,9 @@ Some of the models, such as `GetItemsFilter` or `ViewOptions`, are stored as JSO These settings are similar to Room ones and are usually stored per user. These can be accessed via `KeyValueService`. The values are `@Serializable` objects saved as JSON. This should be used sparingly. + +#### Experimental + +Some settings are considered "experimental". These may cause instabilities or may need further testing. They may be changed or removed any time. + +These are stored in the `ExperimentalPreferences` object in `AppPreferences`. Accessing them should always be gated with `ExperimentalPreferences.enabled`. There are a few helper functions for this purpose. diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt index 8149f8b6..f573c4a7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt @@ -1266,8 +1266,16 @@ val advancedPreferences = AppPreference.DebugLogging, AppPreference.ImageDiskCacheSize, AppPreference.ClearImageCache, + ExperimentalPreference.Enable, AppPreference.OssLicenseInfo, ), + conditionalPreferences = + listOf( + ConditionalPreferences( + condition = { it.experimentalPreferences.enabled }, + preferences = listOf(ExperimentalPreference.ExperimentalSettings), + ), + ), ), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt index ddac936a..1e84663d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt @@ -174,6 +174,14 @@ class AppPreferencesSerializer showLyrics = true showAlbumArt = true }.build() + + experimentalPreferences = + ExperimentalPreferences + .newBuilder() + .apply { + enabled = false + videoTunnelingEnabled = false + }.build() }.build() override suspend fun readFrom(input: InputStream): AppPreferences { @@ -252,6 +260,11 @@ inline fun AppPreferences.updateMusicPreferences(block: MusicPreferences.Builder musicPreferences = musicPreferences.toBuilder().apply(block).build() } +inline fun AppPreferences.updateExperimentalPreferences(block: ExperimentalPreferences.Builder.() -> Unit): AppPreferences = + update { + experimentalPreferences = experimentalPreferences.toBuilder().apply(block).build() + } + fun SubtitlePreferences.Builder.resetSubtitles() { fontSize = SubtitleSettings.FontSize.defaultValue.toInt() fontColor = SubtitleSettings.FontColor.defaultValue.toArgb() diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/ExperimentalPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/ExperimentalPreference.kt new file mode 100644 index 00000000..18e38f79 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/ExperimentalPreference.kt @@ -0,0 +1,58 @@ +package com.github.damontecres.wholphin.preferences + +import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.ui.nav.Destination +import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup +import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption + +object ExperimentalPreference { + val Enable = + AppSwitchPreference<AppPreferences>( + title = R.string.experimental_settings, + defaultValue = false, + getter = { it.experimentalPreferences.enabled }, + setter = { prefs, value -> + prefs.updateExperimentalPreferences { enabled = value } + }, + summaryOn = R.string.enabled, + summaryOff = R.string.disabled, + ) + + val ExperimentalSettings = + AppDestinationPreference<AppPreferences>( + title = R.string.experimental_settings, + destination = Destination.Settings(PreferenceScreenOption.EXPERIMENTAL), + ) + + val VideoTunneling = + AppSwitchPreference<AppPreferences>( + title = R.string.video_tunneling, + defaultValue = false, + getter = { it.experimentalPreferences.videoTunnelingEnabled }, + setter = { prefs, value -> + prefs.updateExperimentalPreferences { videoTunnelingEnabled = value } + }, + summaryOn = R.string.enabled, + summaryOff = R.string.disabled, + ) +} + +val experimentalPreferences = + buildList { + add( + PreferenceGroup( + title = R.string.experimental_settings, + preferences = + listOf( + ExperimentalPreference.VideoTunneling, + ), + ), + ) + } + +/** + * Get a value from [ExperimentalPreference] or null if not enabled + */ +fun <T> ExperimentalPreferences.get(block: ExperimentalPreferences.() -> T): T? = if (enabled) block.invoke(this) else null + +fun ExperimentalPreferences.enabled(block: ExperimentalPreferences.() -> Boolean): Boolean = enabled && block.invoke(this) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt index 01b82777..24736015 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt @@ -26,10 +26,11 @@ import androidx.media3.exoplayer.video.VideoRendererEventListener import androidx.media3.extractor.DefaultExtractorsFactory import androidx.media3.session.MediaSession import com.github.damontecres.wholphin.mpv.MpvPlayer +import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AssPlaybackMode import com.github.damontecres.wholphin.preferences.MediaExtensionStatus -import com.github.damontecres.wholphin.preferences.PlaybackPreferences import com.github.damontecres.wholphin.preferences.PlayerBackend +import com.github.damontecres.wholphin.preferences.get import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient import com.github.damontecres.wholphin.util.WholphinDispatchers import dagger.hilt.android.qualifiers.ApplicationContext @@ -61,8 +62,9 @@ class PlayerFactory suspend fun createVideoPlayer( backend: PlayerBackend, - prefs: PlaybackPreferences, + appPreferences: AppPreferences, ): PlayerCreation { + val prefs = appPreferences.playbackPreferences withContext(WholphinDispatchers.Main) { if (currentPlayer?.isReleased == false) { Timber.w("Player was not released before trying to create a new one!") @@ -105,6 +107,7 @@ class PlayerFactory WholphinRenderersFactory(context, decodeAv1) .setEnableDecoderFallback(true) .setExtensionRendererMode(rendererMode) + val mediaSourceFactory = if (useLibAss) { val renderType = @@ -129,7 +132,9 @@ class PlayerFactory extractorsFactory, ) } - val trackSelector = createTrackSelector() + val tunneling = + appPreferences.experimentalPreferences.get { videoTunnelingEnabled } + val trackSelector = createTrackSelector(tunneling) ExoPlayer .Builder(context) @@ -200,11 +205,13 @@ class PlayerFactory .setConstantBitrateSeekingEnabled(true) .setConstantBitrateSeekingAlwaysEnabled(true) - private fun createTrackSelector() = + private fun createTrackSelector(tunneling: Boolean? = null) = DefaultTrackSelector(context).apply { setParameters( buildUponParameters() - .setAudioOffloadPreferences( + .apply { + tunneling?.let { setTunnelingEnabled(tunneling) } + }.setAudioOffloadPreferences( AudioOffloadPreferences .Builder() .setAudioOffloadMode(AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index b297c0c6..37d3e5ad 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -253,7 +253,7 @@ class PlaybackViewModel val playerCreation = playerFactory.createVideoPlayer( playerBackend, - preferences.appPreferences.playbackPreferences, + preferences.appPreferences, ) this.player = playerCreation.player currentPlayer.update { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferenceUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferenceUtils.kt index 06399fe9..0a843dcb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferenceUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferenceUtils.kt @@ -37,6 +37,7 @@ enum class PreferenceScreenOption { MPV, SCREENSAVER, SKIP_SEGMENTS, + EXPERIMENTAL, ; companion object { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt index 982e2537..b08e9508 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt @@ -48,13 +48,16 @@ import coil3.imageLoader import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreferences +import com.github.damontecres.wholphin.preferences.AppSwitchPreference import com.github.damontecres.wholphin.preferences.ExoPlayerPreferences +import com.github.damontecres.wholphin.preferences.ExperimentalPreference import com.github.damontecres.wholphin.preferences.MpvPreferences import com.github.damontecres.wholphin.preferences.PlayerBackend import com.github.damontecres.wholphin.preferences.ScreensaverPreference import com.github.damontecres.wholphin.preferences.SkipSegmentPreferences import com.github.damontecres.wholphin.preferences.advancedPreferences import com.github.damontecres.wholphin.preferences.basicPreferences +import com.github.damontecres.wholphin.preferences.experimentalPreferences import com.github.damontecres.wholphin.preferences.screensaverPreferences import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences import com.github.damontecres.wholphin.services.Release @@ -148,6 +151,7 @@ fun PreferencesContent( PreferenceScreenOption.MPV -> MpvPreferences PreferenceScreenOption.SCREENSAVER -> screensaverPreferences PreferenceScreenOption.SKIP_SEGMENTS -> SkipSegmentPreferences + PreferenceScreenOption.EXPERIMENTAL -> experimentalPreferences } val screenTitle = when (preferenceScreenOption) { @@ -157,6 +161,7 @@ fun PreferencesContent( PreferenceScreenOption.MPV -> R.string.mpv_options PreferenceScreenOption.SCREENSAVER -> R.string.screensaver_settings PreferenceScreenOption.SKIP_SEGMENTS -> R.string.skip_behavior + PreferenceScreenOption.EXPERIMENTAL -> R.string.experimental_settings } var visible by remember { mutableStateOf(false) } @@ -569,6 +574,46 @@ fun PreferencesContent( ) } + ExperimentalPreference.Enable -> { + var showConfirm by remember { mutableStateOf(false) } + pref as AppSwitchPreference<AppPreferences> + val value = pref.getter.invoke(preferences) + SwitchPreference( + title = stringResource(pref.title), + value = value, + onClick = { + if (value) { + scope.launch(ExceptionHandler()) { + preferences = + viewModel.preferenceDataStore.updateData { prefs -> + pref.setter.invoke(prefs, false) + } + } + } else { + showConfirm = true + } + }, + summaryOn = pref.summaryOn?.let { stringResource(pref.summaryOn) }, + summaryOff = pref.summaryOff?.let { stringResource(pref.summaryOff) }, + ) + if (showConfirm) { + ConfirmDialog( + title = stringResource(R.string.confirm_enable_experimental_title), + body = stringResource(R.string.confirm_enable_experimental_body), + onCancel = { showConfirm = false }, + onConfirm = { + showConfirm = false + scope.launch(ExceptionHandler()) { + preferences = + viewModel.preferenceDataStore.updateData { prefs -> + pref.setter.invoke(prefs, true) + } + } + }, + ) + } + } + else -> { val value = pref.getter.invoke(preferences) ComposablePreference( @@ -762,6 +807,7 @@ fun PreferencesPage( PreferenceScreenOption.MPV, PreferenceScreenOption.SCREENSAVER, PreferenceScreenOption.SKIP_SEGMENTS, + PreferenceScreenOption.EXPERIMENTAL, -> { PreferencesContent( initialPreferences, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/slideshow/SlideshowViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/slideshow/SlideshowViewModel.kt index 8db01751..c81ffcb7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/slideshow/SlideshowViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/slideshow/SlideshowViewModel.kt @@ -111,7 +111,7 @@ class SlideshowViewModel val playerCreation = playerFactory.createVideoPlayer( backend = PlayerBackend.EXO_PLAYER, - appPreferences.playbackPreferences, + appPreferences, ) player = playerCreation.player player.addListener(this@SlideshowViewModel) diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index d2443c60..2eb47cef 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -212,6 +212,11 @@ message MusicPreferences { bool show_backdrop = 4; } +message ExperimentalPreferences { + bool enabled = 1; + bool video_tunneling_enabled = 2; +} + message AppPreferences { // The currently signed in server and user IDs, mostly for restoring a session string current_server_id = 1; @@ -229,4 +234,5 @@ message AppPreferences { bool sign_in_automatically = 11; PhotoPreferences photo_preferences = 12; MusicPreferences music_preferences = 13; + ExperimentalPreferences experimental_preferences = 14; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dd999158..312f78b2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -829,4 +829,8 @@ <string name="popularity">Popularity</string> <string name="production_status">Production status</string> <string name="different_for_hdr">Use different style for HDR</string> + <string name="experimental_settings">Experimental settings</string> + <string name="confirm_enable_experimental_title">Enable experimental settings?</string> + <string name="confirm_enable_experimental_body">Experimental settings may be unstable!\n\n These may removed or changed in any release.</string> + <string name="video_tunneling">Video tunneling</string> </resources>