From ac1051702898a1dc87bf32c9b97b642e2708b63d Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:55:46 -0500 Subject: [PATCH] Add setting for subtitle outline size (#375) Adds a settings allowing for adjusting the thickness of unstyled subtitle outlines, if outline is enabled. This is applied for both ExoPlayer & MPV Closes #179 ![outline Large](https://github.com/user-attachments/assets/dfef74a5-a4d4-49db-a8d2-231e46ab151e) Dev note: the ExoPlayer implementation is bad code, don't write code like this. It works though --- .../preferences/AppPreferencesSerializer.kt | 1 + .../wholphin/services/AppUpgradeHandler.kt | 10 ++++ .../wholphin/ui/playback/PlaybackPage.kt | 6 ++ .../preferences/subtitle/SubtitleSettings.kt | 22 ++++++++ .../preferences/subtitle/SubtitleStylePage.kt | 5 ++ .../wholphin/util/Media3SubtitleOverride.kt | 56 +++++++++++++++++++ app/src/main/proto/WholphinDataStore.proto | 1 + app/src/main/res/values/strings.xml | 1 + .../test/TestMedia3SubtitleOverride.kt | 14 +++++ 9 files changed, 116 insertions(+) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/util/Media3SubtitleOverride.kt create mode 100644 app/src/test/java/com/github/damontecres/wholphin/test/TestMedia3SubtitleOverride.kt 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 079aedd0..247a5e49 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 @@ -170,4 +170,5 @@ fun SubtitlePreferences.Builder.resetSubtitles() { backgroundOpacity = SubtitleSettings.BackgroundOpacity.defaultValue.toInt() backgroundStyle = SubtitleSettings.BackgroundStylePref.defaultValue margin = SubtitleSettings.Margin.defaultValue.toInt() + edgeThickness = SubtitleSettings.EdgeThickness.defaultValue.toInt() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index 44fd82f5..eda96fa3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -163,4 +163,14 @@ suspend fun upgradeApp( } } } + + if (previous.isEqualOrBefore(Version.fromString("0.3.5-0-g0"))) { + appPreferences.updateData { + it.updateSubtitlePreferences { + if (edgeThickness < 1) { + edgeThickness = SubtitleSettings.EdgeThickness.defaultValue.toInt() + } + } + } + } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt index 5c291695..c697a5b0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt @@ -73,12 +73,14 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.applyToMpv +import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.calculateEdgeSize import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.toSubtitleStyle import com.github.damontecres.wholphin.ui.seasonEpisode import com.github.damontecres.wholphin.ui.skipStringRes import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState +import com.github.damontecres.wholphin.util.Media3SubtitleOverride import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -329,6 +331,10 @@ fun PlaybackPage( }, update = { it.setCues(cues) + Media3SubtitleOverride( + preferences.appPreferences.interfacePreferences.subtitlesPreferences + .calculateEdgeSize(density), + ).apply(it) }, onReset = { it.setCues(null) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleSettings.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleSettings.kt index 9daee6fe..69a66a7a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleSettings.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleSettings.kt @@ -140,6 +140,23 @@ object SubtitleSettings { }, ) + val EdgeThickness = + AppSliderPreference( + title = R.string.edge_size, + defaultValue = 4, + min = 1, + max = 32, + interval = 1, + getter = { + it.interfacePreferences.subtitlesPreferences.edgeThickness + .toLong() + }, + setter = { prefs, value -> + prefs.updateSubtitlePreferences { edgeThickness = value.toInt() } + }, + summarizer = { value -> value?.let { "${it / 2.0}" } }, + ) + val BackgroundColor = AppChoicePreference( title = R.string.background_color, @@ -230,6 +247,7 @@ object SubtitleSettings { listOf( EdgeStylePref, EdgeColor, + EdgeThickness, ), ), PreferenceGroup( @@ -278,6 +296,8 @@ object SubtitleSettings { ) } + fun SubtitlePreferences.calculateEdgeSize(density: Density): Float = with(density) { (edgeThickness / 2f).dp.toPx() } + fun SubtitlePreferences.applyToMpv( configuration: Configuration, density: Density, @@ -316,6 +336,8 @@ object SubtitleSettings { MPVLib.setPropertyDouble("sub-outline-size", 0.0) } } + val outlineSizePx = calculateEdgeSize(density) * .8 + MPVLib.setPropertyDouble("sub-outline-size", outlineSizePx) // if (fontBold) { // MPVLib.setPropertyString("sub-font", "Roboto Bold") diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleStylePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleStylePage.kt index e53e1cfe..b0bc6426 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleStylePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleStylePage.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView @@ -32,7 +33,9 @@ import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption import com.github.damontecres.wholphin.ui.preferences.PreferencesContent import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel +import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.calculateEdgeSize import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.toSubtitleStyle +import com.github.damontecres.wholphin.util.Media3SubtitleOverride @OptIn(UnstableApi::class) @Composable @@ -41,6 +44,7 @@ fun SubtitleStylePage( modifier: Modifier = Modifier, viewModel: PreferencesViewModel = hiltViewModel(), ) { + val density = LocalDensity.current var preferences by remember { mutableStateOf(initialPreferences) } LaunchedEffect(Unit) { viewModel.preferenceDataStore.data.collect { @@ -95,6 +99,7 @@ fun SubtitleStylePage( Cue.Builder().setText(text).build(), ), ) + Media3SubtitleOverride(prefs.calculateEdgeSize(density)).apply(it) }, modifier = Modifier diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/Media3SubtitleOverride.kt b/app/src/main/java/com/github/damontecres/wholphin/util/Media3SubtitleOverride.kt new file mode 100644 index 00000000..c618f6e5 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/util/Media3SubtitleOverride.kt @@ -0,0 +1,56 @@ +package com.github.damontecres.wholphin.util + +import androidx.media3.ui.SubtitleView +import timber.log.Timber +import java.lang.reflect.Field + +/** + * Utility class to override private fields in media3's [SubtitleView] + */ +class Media3SubtitleOverride( + val outlineThicknessPx: Float, +) { + companion object { + private lateinit var outputField: Field + private lateinit var classCanvasSubtitleOutput: Class<*> + private lateinit var paintersField: Field + private lateinit var classSubtitlePainter: Class<*> + private lateinit var outlineWidthField: Field + + var initialized: Boolean + private set + + init { + try { + // This is bad times + outputField = SubtitleView::class.java.getDeclaredField("output") + classCanvasSubtitleOutput = Class.forName("androidx.media3.ui.CanvasSubtitleOutput") + paintersField = classCanvasSubtitleOutput.getDeclaredField("painters") + classSubtitlePainter = Class.forName("androidx.media3.ui.SubtitlePainter") + outlineWidthField = classSubtitlePainter.getDeclaredField("outlineWidth") + outputField.isAccessible = true + outlineWidthField.isAccessible = true + paintersField.isAccessible = true + initialized = true + } catch (ex: Exception) { + Timber.w(ex, "Error initializing") + initialized = false + } + } + } + + fun apply(subtitleView: SubtitleView) { + try { + if (initialized) { + // Basically hijack the field that controls the subtitle outline size + val canvasSubtitleOutput = outputField.get(subtitleView) + val painters = paintersField.get(canvasSubtitleOutput) as List<*> + painters.forEach { painter -> + outlineWidthField.set(painter, outlineThicknessPx) + } + } + } catch (ex: Exception) { + Timber.w(ex, "Failed to apply") + } + } +} diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index 5345086e..f5abed11 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -121,6 +121,7 @@ message SubtitlePreferences{ bool font_bold = 9; bool font_italic = 10; int32 margin = 11; + int32 edge_thickness = 12; } message InterfacePreferences { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c86d0d24..40a88eda 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -349,6 +349,7 @@ Image type Guest stars + Edge size diff --git a/app/src/test/java/com/github/damontecres/wholphin/test/TestMedia3SubtitleOverride.kt b/app/src/test/java/com/github/damontecres/wholphin/test/TestMedia3SubtitleOverride.kt new file mode 100644 index 00000000..ccc48cb3 --- /dev/null +++ b/app/src/test/java/com/github/damontecres/wholphin/test/TestMedia3SubtitleOverride.kt @@ -0,0 +1,14 @@ +package com.github.damontecres.wholphin.test + +import com.github.damontecres.wholphin.util.Media3SubtitleOverride +import org.junit.Assert +import org.junit.Test + +class TestMedia3SubtitleOverride { + @Test + fun test() { + // This tests whether the class and field names exist + Media3SubtitleOverride(2f) + Assert.assertTrue(Media3SubtitleOverride.Companion.initialized) + } +}