From a663449f05c2c78a88714e2d71d65fed407ba602 Mon Sep 17 00:00:00 2001 From: Ida Delphine Date: Thu, 6 Aug 2026 23:33:43 +0100 Subject: [PATCH] add exoplayer settings --- .../composeResources/values/strings.xml | 22 ++- .../net/newpipe/app/navigation/Destination.kt | 3 + .../net/newpipe/app/navigation/NavModule.kt | 5 + .../app/preferences/ExoPlayerPreferences.kt | 27 ++++ .../settings/ExoPlayerSettingsScreen.kt | 151 ++++++++++++++++++ .../settings/VideoAudioSettingsScreen.kt | 14 +- .../settings/ExoPlayerSettingsViewModel.kt | 68 ++++++++ .../settings/ExoPlayerSettingsScreenTest.kt | 67 ++++++++ .../settings/VideoAudioSettingsScreenTest.kt | 14 ++ 9 files changed, 364 insertions(+), 7 deletions(-) create mode 100644 shared/src/commonMain/kotlin/net/newpipe/app/preferences/ExoPlayerPreferences.kt create mode 100644 shared/src/commonMain/kotlin/net/newpipe/app/screen/settings/ExoPlayerSettingsScreen.kt create mode 100644 shared/src/commonMain/kotlin/net/newpipe/app/viewmodel/settings/ExoPlayerSettingsViewModel.kt create mode 100644 shared/src/commonTest/kotlin/net/newpipe/app/screen/settings/ExoPlayerSettingsScreenTest.kt diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index 0e2ff467c..637d82f64 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -23,7 +23,7 @@ Website Visit the NewPipe Website for more info and news. NewPipe's Privacy Policy - The NewPipe project takes your privacy very seriously. Therefore, the app does not collect any data without your consent.\nNewPipe\'s privacy policy explains in detail what data is sent and stored when you send a crash report. + The NewPipe project takes your privacy very seriously. Therefore, the app does not collect any data without your consent.\nNewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report. Read privacy policy NewPipe's License NewPipe is copyleft libre software: You can use, study, share, and improve it at will. Specifically you can redistribute and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -59,7 +59,7 @@ This option is only available if %1$s is selected for Theme You can select your favorite night theme below Show "Hold to enqueue" tip - Show tip when pressing the background or the popup button in video \"Details:\" + Show tip when pressing the background or the popup button in video "Details:" Tablet mode List view mode Captions @@ -99,7 +99,7 @@ Seekbar thumbnail preview High quality (larger) Low quality (smaller) - Don\'t show + Don't show Behavior @@ -139,7 +139,7 @@ Remember popup properties Remember last size and position of popup Use fast inexact seek - Inexact seek allows the player to seek to positions faster with reduced precision. Seeking for 5, 15 or 25 seconds doesn\'t work with this + Inexact seek allows the player to seek to positions faster with reduced precision. Seeking for 5, 15 or 25 seconds doesn't work with this Fast-forward/-rewind seek duration Due to ExoPlayer constraints the seek duration was set to %1$d seconds Ask for confirmation before clearing a queue @@ -151,5 +151,19 @@ %1$d seconds + + ExoPlayer settings + Manage some ExoPlayer settings. These changes require a player restart to take effect + Playback load interval size + Change the load interval size on progressive contents (currently %1$s). A lower value may speed up their initial loading + ExoPlayer default + Use ExoPlayer's decoder fallback feature + Enable this option if you have decoder initialization issues, which falls back to lower-priority decoders if primary decoders initialization fail. This may result in poor playback performance than when using primary decoders + Disable media tunneling + Disable media tunneling if you experience a black screen or stuttering on video playback. + Media tunneling was disabled by default on your device because your device model is known to not support it. + Always use ExoPlayer's video output surface setting workaround + This workaround releases and re-instantiates video codecs when a surface change occurs, instead of setting the surface to the codec directly. Already used by ExoPlayer on some devices with this issue, this setting has only an effect on Android 6 and higher\n\nEnabling this option may prevent playback errors when switching the current video player or switching to fullscreen + diff --git a/shared/src/commonMain/kotlin/net/newpipe/app/navigation/Destination.kt b/shared/src/commonMain/kotlin/net/newpipe/app/navigation/Destination.kt index 0d0c8c172..4290abadb 100644 --- a/shared/src/commonMain/kotlin/net/newpipe/app/navigation/Destination.kt +++ b/shared/src/commonMain/kotlin/net/newpipe/app/navigation/Destination.kt @@ -15,6 +15,9 @@ import net.newpipe.app.model.License @Serializable sealed interface Destination : NavKey { + @Serializable + data object ExoPlayerSettings : Destination + @Serializable data object VideoAudioSettings : Destination diff --git a/shared/src/commonMain/kotlin/net/newpipe/app/navigation/NavModule.kt b/shared/src/commonMain/kotlin/net/newpipe/app/navigation/NavModule.kt index 1030e1433..aae14584f 100644 --- a/shared/src/commonMain/kotlin/net/newpipe/app/navigation/NavModule.kt +++ b/shared/src/commonMain/kotlin/net/newpipe/app/navigation/NavModule.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateListOf import co.touchlab.kermit.Logger import net.newpipe.app.screen.about.AboutScreen import net.newpipe.app.screen.settings.AppearanceSettingsScreen +import net.newpipe.app.screen.settings.ExoPlayerSettingsScreen import net.newpipe.app.screen.settings.SettingsHomeScreen import net.newpipe.app.screen.settings.VideoAudioSettingsScreen import org.koin.core.annotation.KoinExperimentalAPI @@ -42,6 +43,10 @@ fun navModule() = module { navigation { VideoAudioSettingsScreen() } + + navigation { + ExoPlayerSettingsScreen() + } } /** diff --git a/shared/src/commonMain/kotlin/net/newpipe/app/preferences/ExoPlayerPreferences.kt b/shared/src/commonMain/kotlin/net/newpipe/app/preferences/ExoPlayerPreferences.kt new file mode 100644 index 000000000..2e0691bc7 --- /dev/null +++ b/shared/src/commonMain/kotlin/net/newpipe/app/preferences/ExoPlayerPreferences.kt @@ -0,0 +1,27 @@ +/* +* SPDX-FileCopyrightText: 2026 NewPipe e.V. +* SPDX-License-Identifier: GPL-3.0-or-later +*/ + +package net.newpipe.app.preferences + +object ExoPlayerPreferences { + const val KEY_PROGRESSIVE_LOAD_INTERVAL = "progressive_load_interval" + const val KEY_USE_EXOPLAYER_DECODER_FALLBACK = "use_exoplayer_decoder_fallback_key" + const val KEY_DISABLE_MEDIA_TUNNELING = "disable_media_tunneling_key" + const val KEY_DISABLED_MEDIA_TUNNELING_AUTOMATICALLY = "disabled_media_tunneling_automatically_key" + const val KEY_ALWAYS_USE_SET_OUTPUT_SURFACE_WORKAROUND = "always_use_exoplayer_set_output_surface_workaround_key" + + const val PROGRESSIVE_LOAD_INTERVAL_EXOPLAYER_DEFAULT = "exoplayer_default" + + const val DEFAULT_PROGRESSIVE_LOAD_INTERVAL = "64" + const val DEFAULT_USE_EXOPLAYER_DECODER_FALLBACK = false + const val DEFAULT_DISABLE_MEDIA_TUNNELING = false + const val DEFAULT_ALWAYS_USE_SET_OUTPUT_SURFACE_WORKAROUND = false + + const val MEDIA_TUNNELING_AUTO_DISABLED = 1 + const val MEDIA_TUNNELING_USER_MANAGED = 0 + const val MEDIA_TUNNELING_FLAG_UNSET = -1 + + val PROGRESSIVE_LOAD_INTERVALS = listOf("1", "16", "64", "256", PROGRESSIVE_LOAD_INTERVAL_EXOPLAYER_DEFAULT) +} diff --git a/shared/src/commonMain/kotlin/net/newpipe/app/screen/settings/ExoPlayerSettingsScreen.kt b/shared/src/commonMain/kotlin/net/newpipe/app/screen/settings/ExoPlayerSettingsScreen.kt new file mode 100644 index 000000000..52d6de061 --- /dev/null +++ b/shared/src/commonMain/kotlin/net/newpipe/app/screen/settings/ExoPlayerSettingsScreen.kt @@ -0,0 +1,151 @@ +/* +* SPDX-FileCopyrightText: 2026 NewPipe e.V. +* SPDX-License-Identifier: GPL-3.0-or-later +*/ + +package net.newpipe.app.screen.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.tooling.preview.PreviewWrapper +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import net.newpipe.app.composable.ListPreference +import net.newpipe.app.composable.ListPreferenceEntry +import net.newpipe.app.composable.SwitchPreference +import net.newpipe.app.composable.TopAppBar +import net.newpipe.app.navigation.Navigator +import net.newpipe.app.preferences.ExoPlayerPreferences +import net.newpipe.app.preview.ThemePreviewProvider +import net.newpipe.app.viewmodel.settings.ExoPlayerSettingsViewModel +import newpipe.shared.generated.resources.Res +import newpipe.shared.generated.resources.always_use_exoplayer_set_output_surface_workaround_summary +import newpipe.shared.generated.resources.always_use_exoplayer_set_output_surface_workaround_title +import newpipe.shared.generated.resources.disable_media_tunneling_automatic_info +import newpipe.shared.generated.resources.disable_media_tunneling_summary +import newpipe.shared.generated.resources.disable_media_tunneling_title +import newpipe.shared.generated.resources.progressive_load_interval_exoplayer_default +import newpipe.shared.generated.resources.progressive_load_interval_summary +import newpipe.shared.generated.resources.progressive_load_interval_title +import newpipe.shared.generated.resources.settings_category_exoplayer_title +import newpipe.shared.generated.resources.use_exoplayer_decoder_fallback_summary +import newpipe.shared.generated.resources.use_exoplayer_decoder_fallback_title +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.koinInject +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun ExoPlayerSettingsScreen( + navigator: Navigator = koinInject(), + viewModel: ExoPlayerSettingsViewModel = koinViewModel() +) { + val progressiveLoadInterval by viewModel.progressiveLoadInterval.collectAsStateWithLifecycle() + val useExoplayerDecoderFallback by viewModel.useExoplayerDecoderFallback.collectAsStateWithLifecycle() + val disableMediaTunneling by viewModel.disableMediaTunneling.collectAsStateWithLifecycle() + val mediaTunnelingAutoDisabled by viewModel.mediaTunnelingAutoDisabled.collectAsStateWithLifecycle() + val alwaysUseSetOutputSurfaceWorkaround by viewModel.alwaysUseSetOutputSurfaceWorkaround.collectAsStateWithLifecycle() + + ExoPlayerSettingsScreenContent( + progressiveLoadInterval = progressiveLoadInterval, + useExoplayerDecoderFallback = useExoplayerDecoderFallback, + disableMediaTunneling = disableMediaTunneling, + mediaTunnelingAutoDisabled = mediaTunnelingAutoDisabled, + alwaysUseSetOutputSurfaceWorkaround = alwaysUseSetOutputSurfaceWorkaround, + onProgressiveLoadIntervalChange = viewModel::setProgressiveLoadInterval, + onUseExoplayerDecoderFallbackChange = viewModel::setUseExoplayerDecoderFallback, + onDisableMediaTunnelingChange = viewModel::setDisableMediaTunneling, + onAlwaysUseSetOutputSurfaceWorkaroundChange = viewModel::setAlwaysUseSetOutputSurfaceWorkaround, + onNavigateUp = { navigator.navigateUp() } + ) +} + +@Composable +fun ExoPlayerSettingsScreenContent( + progressiveLoadInterval: String = ExoPlayerPreferences.DEFAULT_PROGRESSIVE_LOAD_INTERVAL, + useExoplayerDecoderFallback: Boolean = ExoPlayerPreferences.DEFAULT_USE_EXOPLAYER_DECODER_FALLBACK, + disableMediaTunneling: Boolean = ExoPlayerPreferences.DEFAULT_DISABLE_MEDIA_TUNNELING, + mediaTunnelingAutoDisabled: Boolean = false, + alwaysUseSetOutputSurfaceWorkaround: Boolean = ExoPlayerPreferences.DEFAULT_ALWAYS_USE_SET_OUTPUT_SURFACE_WORKAROUND, + onProgressiveLoadIntervalChange: (String) -> Unit = {}, + onUseExoplayerDecoderFallbackChange: (Boolean) -> Unit = {}, + onDisableMediaTunnelingChange: (Boolean) -> Unit = {}, + onAlwaysUseSetOutputSurfaceWorkaroundChange: (Boolean) -> Unit = {}, + onNavigateUp: () -> Unit = {} +) { + val exoPlayerDefaultLabel = stringResource(Res.string.progressive_load_interval_exoplayer_default) + + val loadIntervalEntries = ExoPlayerPreferences.PROGRESSIVE_LOAD_INTERVALS.map { value -> + ListPreferenceEntry( + value = value, + title = if (value == ExoPlayerPreferences.PROGRESSIVE_LOAD_INTERVAL_EXOPLAYER_DEFAULT) exoPlayerDefaultLabel else "$value KiB" + ) + } + val selectedLoadIntervalTitle = loadIntervalEntries.firstOrNull { it.value == progressiveLoadInterval }?.title.orEmpty() + + val tunnelingBaseSummary = stringResource(Res.string.disable_media_tunneling_summary) + val tunnelingSummary = if (mediaTunnelingAutoDisabled) { + tunnelingBaseSummary + " " + stringResource(Res.string.disable_media_tunneling_automatic_info) + } else { + tunnelingBaseSummary + } + + Scaffold( + topBar = { + TopAppBar( + title = stringResource(Res.string.settings_category_exoplayer_title), + onNavigateUp = onNavigateUp + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()) + .verticalScroll(rememberScrollState()) + .padding(WindowInsets.navigationBars.asPaddingValues()) + ) { + ListPreference( + title = stringResource(Res.string.progressive_load_interval_title), + entries = loadIntervalEntries, + selectedValue = progressiveLoadInterval, + onValueSelected = onProgressiveLoadIntervalChange, + summary = stringResource(Res.string.progressive_load_interval_summary, selectedLoadIntervalTitle) + ) + SwitchPreference( + title = stringResource(Res.string.use_exoplayer_decoder_fallback_title), + summary = stringResource(Res.string.use_exoplayer_decoder_fallback_summary), + checked = useExoplayerDecoderFallback, + onCheckedChange = onUseExoplayerDecoderFallbackChange + ) + SwitchPreference( + title = stringResource(Res.string.disable_media_tunneling_title), + summary = tunnelingSummary, + checked = disableMediaTunneling, + onCheckedChange = onDisableMediaTunnelingChange + ) + SwitchPreference( + title = stringResource(Res.string.always_use_exoplayer_set_output_surface_workaround_title), + summary = stringResource(Res.string.always_use_exoplayer_set_output_surface_workaround_summary), + checked = alwaysUseSetOutputSurfaceWorkaround, + onCheckedChange = onAlwaysUseSetOutputSurfaceWorkaroundChange + ) + } + } +} + +@PreviewWrapper(ThemePreviewProvider::class) +@PreviewLightDark +@Composable +private fun ExoPlayerSettingsScreenPreview() { + ExoPlayerSettingsScreenContent() +} diff --git a/shared/src/commonMain/kotlin/net/newpipe/app/screen/settings/VideoAudioSettingsScreen.kt b/shared/src/commonMain/kotlin/net/newpipe/app/screen/settings/VideoAudioSettingsScreen.kt index b44233dec..dbfdd2e63 100644 --- a/shared/src/commonMain/kotlin/net/newpipe/app/screen/settings/VideoAudioSettingsScreen.kt +++ b/shared/src/commonMain/kotlin/net/newpipe/app/screen/settings/VideoAudioSettingsScreen.kt @@ -28,8 +28,10 @@ import kotlinx.coroutines.launch import net.newpipe.app.composable.ListPreference import net.newpipe.app.composable.ListPreferenceEntry import net.newpipe.app.composable.PreferenceCategoryTitle +import net.newpipe.app.composable.PreferenceRow import net.newpipe.app.composable.SwitchPreference import net.newpipe.app.composable.TopAppBar +import net.newpipe.app.navigation.Destination import net.newpipe.app.navigation.Navigator import net.newpipe.app.preferences.VideoAudioPreferences import net.newpipe.app.preview.ThemePreviewProvider @@ -86,6 +88,8 @@ import newpipe.shared.generated.resources.right_gesture_control_title import newpipe.shared.generated.resources.seconds import newpipe.shared.generated.resources.seek_duration_title import newpipe.shared.generated.resources.seekbar_preview_thumbnail_title +import newpipe.shared.generated.resources.settings_category_exoplayer_summary +import newpipe.shared.generated.resources.settings_category_exoplayer_title import newpipe.shared.generated.resources.settings_category_player_behavior_title import newpipe.shared.generated.resources.settings_category_player_title import newpipe.shared.generated.resources.settings_category_video_audio_title @@ -206,6 +210,7 @@ fun VideoAudioSettingsScreen( onSeekDurationChange = viewModel::setSeekDuration, onClearQueueConfirmationChange = viewModel::setClearQueueConfirmation, onIgnoreHardwareMediaButtonsChange = viewModel::setIgnoreHardwareMediaButtons, + onOpenExoPlayerSettings = { navigator.navigateTo(Destination.ExoPlayerSettings) }, onNavigateUp = { navigator.navigateUp() } ) } @@ -265,6 +270,7 @@ fun VideoAudioSettingsScreenContent( onSeekDurationChange: (String) -> Unit = {}, onClearQueueConfirmationChange: (Boolean) -> Unit = {}, onIgnoreHardwareMediaButtonsChange: (Boolean) -> Unit = {}, + onOpenExoPlayerSettings: () -> Unit = {}, onNavigateUp: () -> Unit = {} ) { val scope = rememberCoroutineScope() @@ -393,7 +399,11 @@ fun VideoAudioSettingsScreenContent( onCheckedChange = onPreferDescriptiveAudioChange ) - // TODO: ExoPlayer settings sub-screen link goes here once that screen is migrated. + PreferenceRow( + title = stringResource(Res.string.settings_category_exoplayer_title), + summary = stringResource(Res.string.settings_category_exoplayer_summary), + onClick = onOpenExoPlayerSettings + ) PreferenceCategoryTitle(title = stringResource(Res.string.settings_category_player_title)) SwitchPreference( @@ -497,8 +507,6 @@ fun VideoAudioSettingsScreenContent( summary = stringResource(Res.string.use_inexact_seek_summary), checked = useInexactSeek, onCheckedChange = { checked -> - // The view model applies the same adjustment. this only decides whether to - // tell the user about it, mirroring the legacy toast. VideoAudioPreferences.adjustedSeekDurationMs(seekDuration, checked)?.let { adjustedMs -> val newSeconds = VideoAudioPreferences.seekDurationSeconds(adjustedMs) scope.launch { diff --git a/shared/src/commonMain/kotlin/net/newpipe/app/viewmodel/settings/ExoPlayerSettingsViewModel.kt b/shared/src/commonMain/kotlin/net/newpipe/app/viewmodel/settings/ExoPlayerSettingsViewModel.kt new file mode 100644 index 000000000..ef45f6db6 --- /dev/null +++ b/shared/src/commonMain/kotlin/net/newpipe/app/viewmodel/settings/ExoPlayerSettingsViewModel.kt @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.viewmodel.settings + +import androidx.lifecycle.ViewModel +import com.russhwolf.settings.Settings +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import net.newpipe.app.preferences.ExoPlayerPreferences +import org.koin.core.annotation.KoinViewModel + +@KoinViewModel +class ExoPlayerSettingsViewModel(private val settings: Settings) : ViewModel() { + + val progressiveLoadInterval: StateFlow + field = stringFlow(ExoPlayerPreferences.KEY_PROGRESSIVE_LOAD_INTERVAL, ExoPlayerPreferences.DEFAULT_PROGRESSIVE_LOAD_INTERVAL) + + val useExoplayerDecoderFallback: StateFlow + field = booleanFlow(ExoPlayerPreferences.KEY_USE_EXOPLAYER_DECODER_FALLBACK, ExoPlayerPreferences.DEFAULT_USE_EXOPLAYER_DECODER_FALLBACK) + + val disableMediaTunneling: StateFlow + field = booleanFlow(ExoPlayerPreferences.KEY_DISABLE_MEDIA_TUNNELING, ExoPlayerPreferences.DEFAULT_DISABLE_MEDIA_TUNNELING) + + val alwaysUseSetOutputSurfaceWorkaround: StateFlow + field = booleanFlow(ExoPlayerPreferences.KEY_ALWAYS_USE_SET_OUTPUT_SURFACE_WORKAROUND, ExoPlayerPreferences.DEFAULT_ALWAYS_USE_SET_OUTPUT_SURFACE_WORKAROUND) + + val mediaTunnelingAutoDisabled: StateFlow + field = MutableStateFlow( + settings.getInt( + ExoPlayerPreferences.KEY_DISABLED_MEDIA_TUNNELING_AUTOMATICALLY, + ExoPlayerPreferences.MEDIA_TUNNELING_FLAG_UNSET + ) == ExoPlayerPreferences.MEDIA_TUNNELING_AUTO_DISABLED + ) + + fun setProgressiveLoadInterval(value: String) = progressiveLoadInterval.persist(ExoPlayerPreferences.KEY_PROGRESSIVE_LOAD_INTERVAL, value) + + fun setUseExoplayerDecoderFallback(value: Boolean) = useExoplayerDecoderFallback.persist(ExoPlayerPreferences.KEY_USE_EXOPLAYER_DECODER_FALLBACK, value) + + fun setDisableMediaTunneling(value: Boolean) { + disableMediaTunneling.persist(ExoPlayerPreferences.KEY_DISABLE_MEDIA_TUNNELING, value) + if (!value) { + settings.putInt( + ExoPlayerPreferences.KEY_DISABLED_MEDIA_TUNNELING_AUTOMATICALLY, + ExoPlayerPreferences.MEDIA_TUNNELING_USER_MANAGED + ) + mediaTunnelingAutoDisabled.value = false + } + } + + fun setAlwaysUseSetOutputSurfaceWorkaround(value: Boolean) = alwaysUseSetOutputSurfaceWorkaround.persist(ExoPlayerPreferences.KEY_ALWAYS_USE_SET_OUTPUT_SURFACE_WORKAROUND, value) + + private fun stringFlow(key: String, defaultValue: String) = MutableStateFlow(settings.getString(key, defaultValue)) + + private fun booleanFlow(key: String, defaultValue: Boolean) = MutableStateFlow(settings.getBoolean(key, defaultValue)) + + private fun MutableStateFlow.persist(key: String, newValue: String) { + settings.putString(key, newValue) + value = newValue + } + + private fun MutableStateFlow.persist(key: String, newValue: Boolean) { + settings.putBoolean(key, newValue) + value = newValue + } +} diff --git a/shared/src/commonTest/kotlin/net/newpipe/app/screen/settings/ExoPlayerSettingsScreenTest.kt b/shared/src/commonTest/kotlin/net/newpipe/app/screen/settings/ExoPlayerSettingsScreenTest.kt new file mode 100644 index 000000000..8b82827c0 --- /dev/null +++ b/shared/src/commonTest/kotlin/net/newpipe/app/screen/settings/ExoPlayerSettingsScreenTest.kt @@ -0,0 +1,67 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.screen.settings + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.v2.runComposeUiTest +import com.russhwolf.settings.MapSettings +import com.russhwolf.settings.Settings +import kotlin.test.Test +import net.newpipe.app.extensions.withKoin +import newpipe.shared.generated.resources.Res +import newpipe.shared.generated.resources.disable_media_tunneling_automatic_info +import newpipe.shared.generated.resources.progressive_load_interval_summary +import newpipe.shared.generated.resources.progressive_load_interval_title +import newpipe.shared.generated.resources.settings_category_exoplayer_title +import org.jetbrains.compose.resources.getString +import org.koin.dsl.module + +@OptIn(ExperimentalTestApi::class) +class ExoPlayerSettingsScreenTest { + + private val emptySettings = module { + single { MapSettings() } + } + + @Test + fun rendersTitleAndLoadIntervalRow() = runComposeUiTest { + withKoin( + modules = listOf(emptySettings), + content = { ExoPlayerSettingsScreenContent() }, + onContent = { + onNodeWithText(getString(Res.string.settings_category_exoplayer_title)).assertIsDisplayed() + onNodeWithText(getString(Res.string.progressive_load_interval_title)).assertIsDisplayed() + } + ) + } + + @Test + fun loadIntervalSummaryShowsSelectedEntryLabel() = runComposeUiTest { + withKoin( + modules = listOf(emptySettings), + content = { ExoPlayerSettingsScreenContent() }, + onContent = { + onNodeWithText(getString(Res.string.progressive_load_interval_summary, "64 KiB")).assertIsDisplayed() + } + ) + } + + @Test + fun mediaTunnelingInfoShownOnlyWhenAutoDisabled() = runComposeUiTest { + withKoin( + modules = listOf(emptySettings), + content = { ExoPlayerSettingsScreenContent(mediaTunnelingAutoDisabled = true) }, + onContent = { + onNodeWithText(getString(Res.string.disable_media_tunneling_automatic_info), substring = true) + .performScrollTo() + .assertIsDisplayed() + } + ) + } +} diff --git a/shared/src/commonTest/kotlin/net/newpipe/app/screen/settings/VideoAudioSettingsScreenTest.kt b/shared/src/commonTest/kotlin/net/newpipe/app/screen/settings/VideoAudioSettingsScreenTest.kt index 1ab8c6063..755f10aa4 100644 --- a/shared/src/commonTest/kotlin/net/newpipe/app/screen/settings/VideoAudioSettingsScreenTest.kt +++ b/shared/src/commonTest/kotlin/net/newpipe/app/screen/settings/VideoAudioSettingsScreenTest.kt @@ -18,6 +18,7 @@ import net.newpipe.app.preferences.VideoAudioPreferences import newpipe.shared.generated.resources.Res import newpipe.shared.generated.resources.best_resolution import newpipe.shared.generated.resources.default_resolution_title +import newpipe.shared.generated.resources.settings_category_exoplayer_title import newpipe.shared.generated.resources.settings_category_player_title import newpipe.shared.generated.resources.settings_category_video_audio_title import org.jetbrains.compose.resources.getString @@ -67,4 +68,17 @@ class VideoAudioSettingsScreenTest { } ) } + + @Test + fun rendersExoPlayerSettingsLink() = runComposeUiTest { + withKoin( + modules = listOf(emptySettings), + content = { VideoAudioSettingsScreenContent() }, + onContent = { + onNodeWithText(getString(Res.string.settings_category_exoplayer_title)) + .performScrollTo() + .assertIsDisplayed() + } + ) + } }