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
This commit is contained in:
damontecres 2025-12-05 16:55:46 -05:00 committed by GitHub
parent a197f6911b
commit ac10517028
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 116 additions and 0 deletions

View file

@ -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()
}

View file

@ -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()
}
}
}
}
}

View file

@ -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)

View file

@ -140,6 +140,23 @@ object SubtitleSettings {
},
)
val EdgeThickness =
AppSliderPreference<AppPreferences>(
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<AppPreferences, Color>(
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")

View file

@ -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

View file

@ -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")
}
}
}

View file

@ -121,6 +121,7 @@ message SubtitlePreferences{
bool font_bold = 9;
bool font_italic = 10;
int32 margin = 11;
int32 edge_thickness = 12;
}
message InterfacePreferences {

View file

@ -349,6 +349,7 @@
<string name="image_type">Image type</string>
<string name="cast_and_crew"><![CDATA[Cast & Crew]]></string>
<string name="guest_stars">Guest stars</string>
<string name="edge_size">Edge size</string>
<string-array name="theme_song_volume">

View file

@ -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)
}
}