feat(subtitles): optionally anchor text subtitles to the screen bottom

Adds an "Anchor to Screen" toggle under Subtitle Styling (Android +
ExoPlayer only, default off). When enabled, the text SubtitleView is
sized to the full container instead of the letterboxed video rect, so
SRT/VTT/mov_text cues render in the black bars below widescreen video
and font size and the position setting become relative to the physical
screen height. Bitmap (PGS/VOB) and ASS/libass rendering are unchanged;
mpv already places plaintext subtitles in the margins by default.

close #1730
This commit is contained in:
edde746
2026-08-09 17:14:34 +02:00
parent 8ad13e94cf
commit 8e5279a487
35 changed files with 151 additions and 13 deletions
@@ -215,6 +215,7 @@ class ExoPlayerCore(private val activity: Activity) :
@Volatile private var assVideoLatencyFrames = 0 @Volatile private var assVideoLatencyFrames = 0
private var subtitlePositionPercent: Int = 100 private var subtitlePositionPercent: Int = 100
private var subtitleFontSize: Float = 55f private var subtitleFontSize: Float = 55f
private var subtitleAnchorToScreen: Boolean = false
private var lastSubtitleCues: List<Cue> = emptyList() private var lastSubtitleCues: List<Cue> = emptyList()
// Tracks whether a text track was selected on the previous onTracksChanged so we // Tracks whether a text track was selected on the previous onTracksChanged so we
@@ -1757,7 +1758,8 @@ class ExoPlayerCore(private val activity: Activity) :
videoHeight, videoHeight,
pixelRatio, pixelRatio,
resizeMode, resizeMode,
videoZoomScale videoZoomScale,
subtitleAnchorToScreen
) )
val bitmapDimensions = SubtitleViewLayout.bitmapDimensions( val bitmapDimensions = SubtitleViewLayout.bitmapDimensions(
containerWidth, containerWidth,
@@ -3911,7 +3913,8 @@ class ExoPlayerCore(private val activity: Activity) :
bgOpacity: Int, bgOpacity: Int,
subtitlePosition: Int = 100, subtitlePosition: Int = 100,
bold: Boolean = false, bold: Boolean = false,
italic: Boolean = false italic: Boolean = false,
anchorToScreen: Boolean = false
) { ) {
activity.runOnUiThread { activity.runOnUiThread {
// 1. Non-ASS subtitles: CaptionStyleCompat on SubtitleView // 1. Non-ASS subtitles: CaptionStyleCompat on SubtitleView
@@ -3959,6 +3962,18 @@ class ExoPlayerCore(private val activity: Activity) :
subtitlePositionPercent = clampedPosition subtitlePositionPercent = clampedPosition
subtitleFontSize = fontSize subtitleFontSize = fontSize
// Anchor-to-screen (#1730): resize the text SubtitleView to the full
// container so default-placed cues land in the letterbox bars.
val anchorChanged = subtitleAnchorToScreen != anchorToScreen
subtitleAnchorToScreen = anchorToScreen
if (anchorChanged) {
lastVideoSize?.let { vs ->
if (vs.width > 0 && vs.height > 0) {
updateSubtitleViewSize(vs.width, vs.height, vs.pixelWidthHeightRatio)
}
}
}
// Cue-level positioning handles default VTT/SRT placement, whose line // Cue-level positioning handles default VTT/SRT placement, whose line
// numbers bypass SubtitleView bottom padding. Authored VTT line positions // numbers bypass SubtitleView bottom padding. Authored VTT line positions
// are preserved in applySubtitlePosition(). // are preserved in applySubtitlePosition().
@@ -3980,7 +3995,7 @@ class ExoPlayerCore(private val activity: Activity) :
Log.w(TAG, "Failed to set ASS font scale: ${e.message}") Log.w(TAG, "Failed to set ASS font scale: ${e.message}")
} }
Log.d(TAG, "setSubtitleStyle: fontSize=$fontSize, textColor=$textColor, borderSize=$borderSize, bgOpacity=$bgOpacity, position=$subtitlePosition, bold=$bold, italic=$italic, assScale=$scale") Log.d(TAG, "setSubtitleStyle: fontSize=$fontSize, textColor=$textColor, borderSize=$borderSize, bgOpacity=$bgOpacity, position=$subtitlePosition, bold=$bold, italic=$italic, anchorToScreen=$anchorToScreen, assScale=$scale")
} }
} }
@@ -1069,6 +1069,7 @@ class ExoPlayerPlugin :
val subtitlePosition = call.argument<Number>("subtitlePosition")?.toInt() ?: 100 val subtitlePosition = call.argument<Number>("subtitlePosition")?.toInt() ?: 100
val bold = call.argument<Boolean>("bold") ?: false val bold = call.argument<Boolean>("bold") ?: false
val italic = call.argument<Boolean>("italic") ?: false val italic = call.argument<Boolean>("italic") ?: false
val anchorToScreen = call.argument<Boolean>("anchorToScreen") ?: false
if (usingMpvFallback) { if (usingMpvFallback) {
// MPV fallback handles styling via setProperty, no-op here // MPV fallback handles styling via setProperty, no-op here
@@ -1076,7 +1077,7 @@ class ExoPlayerPlugin :
return return
} }
playerCore?.setSubtitleStyle(fontSize, textColor, borderSize, borderColor, bgColor, bgOpacity, subtitlePosition, bold, italic) playerCore?.setSubtitleStyle(fontSize, textColor, borderSize, borderColor, bgColor, bgOpacity, subtitlePosition, bold, italic, anchorToScreen)
result.success(null) result.success(null)
} }
@@ -13,12 +13,18 @@ internal object SubtitleViewLayout {
videoHeight: Int, videoHeight: Int,
pixelRatio: Float, pixelRatio: Float,
resizeMode: Int, resizeMode: Int,
zoomScale: Float zoomScale: Float,
anchorToScreen: Boolean = false
): SubtitleViewDimensions? { ): SubtitleViewDimensions? {
val videoAspect = videoAspect(videoWidth, videoHeight, pixelRatio) ?: return null val videoAspect = videoAspect(videoWidth, videoHeight, pixelRatio) ?: return null
if (containerWidth <= 0 || containerHeight <= 0) return null if (containerWidth <= 0 || containerHeight <= 0) return null
if (resizeMode != AspectRatioFrameLayout.RESIZE_MODE_FIT) { // Anchor-to-screen (#1730): size the text view to the full container so
// media3's fractional text size and bottom-anchored cue placement compute
// against the physical screen, letting subtitles render in the letterbox
// bars instead of inside the video rect. Same geometry the non-FIT modes
// below already use.
if (anchorToScreen || resizeMode != AspectRatioFrameLayout.RESIZE_MODE_FIT) {
return SubtitleViewDimensions(containerWidth, containerHeight) return SubtitleViewDimensions(containerWidth, containerHeight)
} }
@@ -45,9 +45,49 @@ class SubtitleViewLayoutTest {
assertAspectCloseTo16By9(bitmap!!) assertAspectCloseTo16By9(bitmap!!)
} }
@Test
fun anchorToScreenSizesTextToContainerAndKeepsBitmapOnVideoRect() {
val text = textDimensions(resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT, anchorToScreen = true)
val bitmap = bitmapDimensions(resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT)
assertEquals(SubtitleViewDimensions(2424, 1080), text)
assertEquals(SubtitleViewDimensions(1920, 1080), bitmap)
assertAspectCloseTo16By9(bitmap!!)
}
@Test
fun anchorToScreenReachesBelowLetterboxedWideVideo() {
// 2.37:1 video on a 16:9 screen — the #1730 use case. Without the anchor
// the text view stops at the bottom letterbox bar; with it the view spans
// the physical screen height.
val unanchored = SubtitleViewLayout.textDimensions(
containerWidth = 1920,
containerHeight = 1080,
videoWidth = 2560,
videoHeight = 1080,
pixelRatio = 1f,
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT,
zoomScale = 1f
)
val anchored = SubtitleViewLayout.textDimensions(
containerWidth = 1920,
containerHeight = 1080,
videoWidth = 2560,
videoHeight = 1080,
pixelRatio = 1f,
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT,
zoomScale = 1f,
anchorToScreen = true
)
assertEquals(SubtitleViewDimensions(1920, 810), unanchored)
assertEquals(SubtitleViewDimensions(1920, 1080), anchored)
}
private fun textDimensions( private fun textDimensions(
resizeMode: Int, resizeMode: Int,
zoomScale: Float = 1f zoomScale: Float = 1f,
anchorToScreen: Boolean = false
): SubtitleViewDimensions? = SubtitleViewLayout.textDimensions( ): SubtitleViewDimensions? = SubtitleViewLayout.textDimensions(
containerWidth = 2424, containerWidth = 2424,
containerHeight = 1080, containerHeight = 1080,
@@ -55,7 +95,8 @@ class SubtitleViewLayoutTest {
videoHeight = 1080, videoHeight = 1080,
pixelRatio = 1f, pixelRatio = 1f,
resizeMode = resizeMode, resizeMode = resizeMode,
zoomScale = zoomScale zoomScale = zoomScale,
anchorToScreen = anchorToScreen
) )
private fun bitmapDimensions( private fun bitmapDimensions(
+2
View File
@@ -744,6 +744,8 @@
"overrideStrip": "Formatlaşdırmanı sil", "overrideStrip": "Formatlaşdırmanı sil",
"positionTop": "Yuxarı", "positionTop": "Yuxarı",
"positionBottom": "Aşağı", "positionBottom": "Aşağı",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Qalın", "bold": "Qalın",
"italic": "Kursiv", "italic": "Kursiv",
"renderResolution": "Emal imkanı (Resolution)", "renderResolution": "Emal imkanı (Resolution)",
+2
View File
@@ -744,6 +744,8 @@
"overrideStrip": "Премахване на стиловете", "overrideStrip": "Премахване на стиловете",
"positionTop": "Горе", "positionTop": "Горе",
"positionBottom": "Долу", "positionBottom": "Долу",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Получер", "bold": "Получер",
"italic": "Курсив", "italic": "Курсив",
"renderResolution": "Резолюция на изобразяване", "renderResolution": "Резолюция на изобразяване",
+2
View File
@@ -744,6 +744,8 @@
"overrideStrip": "Fjern formatering", "overrideStrip": "Fjern formatering",
"positionTop": "Øverst", "positionTop": "Øverst",
"positionBottom": "Nederst", "positionBottom": "Nederst",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Fed", "bold": "Fed",
"italic": "Kursiv", "italic": "Kursiv",
"renderResolution": "Gengivelsesopløsning", "renderResolution": "Gengivelsesopløsning",
+2
View File
@@ -744,6 +744,8 @@
"overrideStrip": "Formatierung entfernen", "overrideStrip": "Formatierung entfernen",
"positionTop": "Oben", "positionTop": "Oben",
"positionBottom": "Unten", "positionBottom": "Unten",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Fett", "bold": "Fett",
"italic": "Kursiv", "italic": "Kursiv",
"renderResolution": "Render-Auflösung", "renderResolution": "Render-Auflösung",
+2
View File
@@ -744,6 +744,8 @@
"overrideStrip": "Remove styling", "overrideStrip": "Remove styling",
"positionTop": "Top", "positionTop": "Top",
"positionBottom": "Bottom", "positionBottom": "Bottom",
"anchorToScreen": "Anchor to Screen",
"anchorToScreenDescription": "Show text subtitles in the black bars below widescreen video",
"bold": "Bold", "bold": "Bold",
"italic": "Italic", "italic": "Italic",
"renderResolution": "Render Resolution", "renderResolution": "Render Resolution",
+2
View File
@@ -744,6 +744,8 @@
"overrideStrip": "Quitar estilos", "overrideStrip": "Quitar estilos",
"positionTop": "Arriba", "positionTop": "Arriba",
"positionBottom": "Abajo", "positionBottom": "Abajo",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Negrita", "bold": "Negrita",
"italic": "Cursiva", "italic": "Cursiva",
"renderResolution": "Resolución de renderizado", "renderResolution": "Resolución de renderizado",
+2
View File
@@ -744,6 +744,8 @@
"overrideStrip": "Supprimer le style", "overrideStrip": "Supprimer le style",
"positionTop": "Haut", "positionTop": "Haut",
"positionBottom": "Bas", "positionBottom": "Bas",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Gras", "bold": "Gras",
"italic": "Italique", "italic": "Italique",
"renderResolution": "Résolution de rendu", "renderResolution": "Résolution de rendu",
+2
View File
@@ -744,6 +744,8 @@
"overrideStrip": "Stílus eltávolítása", "overrideStrip": "Stílus eltávolítása",
"positionTop": "Fent", "positionTop": "Fent",
"positionBottom": "Lent", "positionBottom": "Lent",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Félkövér", "bold": "Félkövér",
"italic": "Dőlt", "italic": "Dőlt",
"renderResolution": "Renderelési felbontás", "renderResolution": "Renderelési felbontás",
+2
View File
@@ -744,6 +744,8 @@
"overrideStrip": "Rimuovi stile", "overrideStrip": "Rimuovi stile",
"positionTop": "In alto", "positionTop": "In alto",
"positionBottom": "In basso", "positionBottom": "In basso",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Grassetto", "bold": "Grassetto",
"italic": "Corsivo", "italic": "Corsivo",
"renderResolution": "Risoluzione di rendering", "renderResolution": "Risoluzione di rendering",
+2
View File
@@ -740,6 +740,8 @@
"overrideStrip": "スタイルを削除", "overrideStrip": "スタイルを削除",
"positionTop": "上", "positionTop": "上",
"positionBottom": "下", "positionBottom": "下",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "太字", "bold": "太字",
"italic": "斜体", "italic": "斜体",
"renderResolution": "レンダリング解像度", "renderResolution": "レンダリング解像度",
+2
View File
@@ -744,6 +744,8 @@
"overrideStrip": "Форматтауды жою", "overrideStrip": "Форматтауды жою",
"positionTop": "Жоғары", "positionTop": "Жоғары",
"positionBottom": "Төмен", "positionBottom": "Төмен",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Қалың", "bold": "Қалың",
"italic": "Көлбеу", "italic": "Көлбеу",
"renderResolution": "Рендеринг ажыратымдылығы", "renderResolution": "Рендеринг ажыратымдылығы",
+2
View File
@@ -740,6 +740,8 @@
"overrideStrip": "스타일 제거", "overrideStrip": "스타일 제거",
"positionTop": "위", "positionTop": "위",
"positionBottom": "아래", "positionBottom": "아래",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "굵게", "bold": "굵게",
"italic": "기울임꼴", "italic": "기울임꼴",
"renderResolution": "렌더링 해상도", "renderResolution": "렌더링 해상도",
+2
View File
@@ -744,6 +744,8 @@
"overrideStrip": "Fjern formatering", "overrideStrip": "Fjern formatering",
"positionTop": "Øverst", "positionTop": "Øverst",
"positionBottom": "Nederst", "positionBottom": "Nederst",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Fet", "bold": "Fet",
"italic": "Kursiv", "italic": "Kursiv",
"renderResolution": "Gjengivelsesoppløsning", "renderResolution": "Gjengivelsesoppløsning",
+2
View File
@@ -744,6 +744,8 @@
"overrideStrip": "Opmaak verwijderen", "overrideStrip": "Opmaak verwijderen",
"positionTop": "Bovenaan", "positionTop": "Bovenaan",
"positionBottom": "Onderaan", "positionBottom": "Onderaan",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Vet", "bold": "Vet",
"italic": "Cursief", "italic": "Cursief",
"renderResolution": "Renderresolutie", "renderResolution": "Renderresolutie",
+2
View File
@@ -752,6 +752,8 @@
"overrideStrip": "Usuń style", "overrideStrip": "Usuń style",
"positionTop": "Góra", "positionTop": "Góra",
"positionBottom": "Dół", "positionBottom": "Dół",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Pogrubienie", "bold": "Pogrubienie",
"italic": "Kursywa", "italic": "Kursywa",
"renderResolution": "Rozdzielczość renderowania", "renderResolution": "Rozdzielczość renderowania",
+2
View File
@@ -744,6 +744,8 @@
"overrideStrip": "Remover estilo", "overrideStrip": "Remover estilo",
"positionTop": "Superior", "positionTop": "Superior",
"positionBottom": "Inferior", "positionBottom": "Inferior",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Negrito", "bold": "Negrito",
"italic": "Itálico", "italic": "Itálico",
"renderResolution": "Resolução de renderização", "renderResolution": "Resolução de renderização",
+2
View File
@@ -752,6 +752,8 @@
"overrideStrip": "Удалить стили", "overrideStrip": "Удалить стили",
"positionTop": "Сверху", "positionTop": "Сверху",
"positionBottom": "Снизу", "positionBottom": "Снизу",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Жирный", "bold": "Жирный",
"italic": "Курсив", "italic": "Курсив",
"renderResolution": "Разрешение отрисовки", "renderResolution": "Разрешение отрисовки",
+1 -1
View File
@@ -4,7 +4,7 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 22 /// Locales: 22
/// Strings: 38352 (1743 per locale) /// Strings: 38354 (1743 per locale)
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // ignore_for_file: type=lint, unused_import
+12 -4
View File
@@ -2212,6 +2212,12 @@ class Translations$subtitlingStyling$en {
/// en: 'Bottom' /// en: 'Bottom'
String get positionBottom => 'Bottom'; String get positionBottom => 'Bottom';
/// en: 'Anchor to Screen'
String get anchorToScreen => 'Anchor to Screen';
/// en: 'Show text subtitles in the black bars below widescreen video'
String get anchorToScreenDescription => 'Show text subtitles in the black bars below widescreen video';
/// en: 'Bold' /// en: 'Bold'
String get bold => 'Bold'; String get bold => 'Bold';
@@ -6762,6 +6768,8 @@ extension on Translations {
'subtitlingStyling.overrideStrip' => 'Remove styling', 'subtitlingStyling.overrideStrip' => 'Remove styling',
'subtitlingStyling.positionTop' => 'Top', 'subtitlingStyling.positionTop' => 'Top',
'subtitlingStyling.positionBottom' => 'Bottom', 'subtitlingStyling.positionBottom' => 'Bottom',
'subtitlingStyling.anchorToScreen' => 'Anchor to Screen',
'subtitlingStyling.anchorToScreenDescription' => 'Show text subtitles in the black bars below widescreen video',
'subtitlingStyling.bold' => 'Bold', 'subtitlingStyling.bold' => 'Bold',
'subtitlingStyling.italic' => 'Italic', 'subtitlingStyling.italic' => 'Italic',
'subtitlingStyling.renderResolution' => 'Render Resolution', 'subtitlingStyling.renderResolution' => 'Render Resolution',
@@ -7091,10 +7099,10 @@ extension on Translations {
'explore.stats.favorited' => ({required Object n}) => '${n} favorites', 'explore.stats.favorited' => ({required Object n}) => '${n} favorites',
'explore.stats.dropRate' => ({required Object percent}) => '${percent} dropped it', 'explore.stats.dropRate' => ({required Object percent}) => '${percent} dropped it',
'explore.stats.comments' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: '${n} comment', other: '${n} comments', ), 'explore.stats.comments' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: '${n} comment', other: '${n} comments', ),
'explore.stats.votes' => ({required Object n}) => '${n} votes',
'explore.stats.watching' => ({required Object n}) => '${n} watching it',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'explore.stats.votes' => ({required Object n}) => '${n} votes',
'explore.stats.watching' => ({required Object n}) => '${n} watching it',
'explore.stats.completed' => ({required Object n}) => '${n} completed', 'explore.stats.completed' => ({required Object n}) => '${n} completed',
'explore.stats.onHold' => ({required Object n}) => '${n} on hold', 'explore.stats.onHold' => ({required Object n}) => '${n} on hold',
'explore.stats.dropped' => ({required Object n}) => '${n} dropped', 'explore.stats.dropped' => ({required Object n}) => '${n} dropped',
@@ -7605,10 +7613,10 @@ extension on Translations {
'externalPlayer.useExternalPlayerDescription' => 'Open videos in another app', 'externalPlayer.useExternalPlayerDescription' => 'Open videos in another app',
'externalPlayer.selectPlayer' => 'Select Player', 'externalPlayer.selectPlayer' => 'Select Player',
'externalPlayer.customPlayers' => 'Custom Players', 'externalPlayer.customPlayers' => 'Custom Players',
'externalPlayer.systemDefault' => 'System Default',
'externalPlayer.addCustomPlayer' => 'Add Custom Player',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.systemDefault' => 'System Default',
'externalPlayer.addCustomPlayer' => 'Add Custom Player',
'externalPlayer.playerName' => 'Player Name', 'externalPlayer.playerName' => 'Player Name',
'externalPlayer.playerNameHint' => 'My Player', 'externalPlayer.playerNameHint' => 'My Player',
'externalPlayer.playerCommand' => 'Command', 'externalPlayer.playerCommand' => 'Command',
+2
View File
@@ -744,6 +744,8 @@
"overrideStrip": "Ta bort formatering", "overrideStrip": "Ta bort formatering",
"positionTop": "Överst", "positionTop": "Överst",
"positionBottom": "Nederst", "positionBottom": "Nederst",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Fet", "bold": "Fet",
"italic": "Kursiv", "italic": "Kursiv",
"renderResolution": "Renderingsupplösning", "renderResolution": "Renderingsupplösning",
+2
View File
@@ -744,6 +744,8 @@
"overrideStrip": "Biçimlendirmeyi kaldır", "overrideStrip": "Biçimlendirmeyi kaldır",
"positionTop": "Üst", "positionTop": "Üst",
"positionBottom": "Alt", "positionBottom": "Alt",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Kalın", "bold": "Kalın",
"italic": "İtalik", "italic": "İtalik",
"renderResolution": "İşleme Çözünürlüğü", "renderResolution": "İşleme Çözünürlüğü",
+2
View File
@@ -744,6 +744,8 @@
"overrideStrip": "Formatlashni olib tashlash", "overrideStrip": "Formatlashni olib tashlash",
"positionTop": "Yuqori", "positionTop": "Yuqori",
"positionBottom": "Pastki", "positionBottom": "Pastki",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "Qalin", "bold": "Qalin",
"italic": "Qiya", "italic": "Qiya",
"renderResolution": "Renderlash oʻlchamlari", "renderResolution": "Renderlash oʻlchamlari",
+2
View File
@@ -740,6 +740,8 @@
"overrideStrip": "移除樣式", "overrideStrip": "移除樣式",
"positionTop": "頂部", "positionTop": "頂部",
"positionBottom": "底部", "positionBottom": "底部",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "粗體", "bold": "粗體",
"italic": "斜體", "italic": "斜體",
"renderResolution": "渲染解析度", "renderResolution": "渲染解析度",
+2
View File
@@ -740,6 +740,8 @@
"overrideStrip": "移除样式", "overrideStrip": "移除样式",
"positionTop": "顶部", "positionTop": "顶部",
"positionBottom": "底部", "positionBottom": "底部",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"bold": "粗体", "bold": "粗体",
"italic": "斜体", "italic": "斜体",
"renderResolution": "渲染分辨率", "renderResolution": "渲染分辨率",
@@ -542,6 +542,7 @@ class PlayerAndroid extends PlayerBase {
int subtitlePosition = 100, int subtitlePosition = 100,
bool bold = false, bool bold = false,
bool italic = false, bool italic = false,
bool anchorToScreen = false,
}) async { }) async {
if (disposed || !initialized) return; if (disposed || !initialized) return;
await invoke('setSubtitleStyle', { await invoke('setSubtitleStyle', {
@@ -554,6 +555,7 @@ class PlayerAndroid extends PlayerBase {
'subtitlePosition': subtitlePosition, 'subtitlePosition': subtitlePosition,
'bold': bold, 'bold': bold,
'italic': italic, 'italic': italic,
'anchorToScreen': anchorToScreen,
}); });
} }
+1
View File
@@ -314,6 +314,7 @@ abstract class Player {
int subtitlePosition = 100, int subtitlePosition = 100,
bool bold = false, bool bold = false,
bool italic = false, bool italic = false,
bool anchorToScreen = false,
}); });
/// Apply the box-fit mode to the native video layer /// Apply the box-fit mode to the native video layer
+1
View File
@@ -1097,6 +1097,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
int subtitlePosition = 100, int subtitlePosition = 100,
bool bold = false, bool bold = false,
bool italic = false, bool italic = false,
bool anchorToScreen = false,
}) async {} }) async {}
@override @override
@@ -42,6 +42,9 @@ class SubtitleStylingScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Anchor-to-screen is an ExoPlayer text-subtitle knob; mpv already places
// plaintext subtitles in the letterbox margins by default.
final exoActive = Platform.isAndroid && SettingsService.instance.read(SettingsService.useExoPlayer);
return SettingsPage( return SettingsPage(
title: Text(t.screens.subtitleStyling), title: Text(t.screens.subtitleStyling),
children: [ children: [
@@ -108,6 +111,13 @@ class SubtitleStylingScreen extends StatelessWidget {
min: 0, min: 0,
max: 100, max: 100,
), ),
if (exoActive)
SettingSwitchTile(
pref: SettingsService.subtitleAnchorToScreen,
icon: Symbols.fit_screen_rounded,
title: t.subtitlingStyling.anchorToScreen,
subtitle: t.subtitlingStyling.anchorToScreenDescription,
),
SettingSwitchTile( SettingSwitchTile(
pref: SettingsService.subtitleBold, pref: SettingsService.subtitleBold,
icon: Symbols.format_bold_rounded, icon: Symbols.format_bold_rounded,
@@ -432,6 +432,7 @@ extension _VideoPlayerOpenMethods on VideoPlayerScreenState {
subtitlePosition: settingsService.read(SettingsService.subtitlePosition), subtitlePosition: settingsService.read(SettingsService.subtitlePosition),
bold: settingsService.read(SettingsService.subtitleBold), bold: settingsService.read(SettingsService.subtitleBold),
italic: settingsService.read(SettingsService.subtitleItalic), italic: settingsService.read(SettingsService.subtitleItalic),
anchorToScreen: settingsService.read(SettingsService.subtitleAnchorToScreen),
); );
} }
+7
View File
@@ -377,6 +377,12 @@ class SettingsService extends BaseSharedPreferencesService {
); );
static const subtitleBold = BoolPref('subtitle_bold'); static const subtitleBold = BoolPref('subtitle_bold');
static const subtitleItalic = BoolPref('subtitle_italic'); static const subtitleItalic = BoolPref('subtitle_italic');
/// Render text subtitles (SRT/VTT/mov_text) anchored to the physical screen
/// instead of the video rect, so they land in the letterbox bars of
/// widescreen video (#1730). ExoPlayer backend only; mpv already places
/// plaintext subtitles in the margins by default (sub-use-margins=yes).
static const subtitleAnchorToScreen = BoolPref('subtitle_anchor_to_screen');
static const cleanedOldImageCache = BoolPref('cleaned_old_image_cache'); static const cleanedOldImageCache = BoolPref('cleaned_old_image_cache');
static const rememberTrackSelections = BoolPref('remember_track_selections', defaultValue: true); static const rememberTrackSelections = BoolPref('remember_track_selections', defaultValue: true);
@@ -935,6 +941,7 @@ class SettingsService extends BaseSharedPreferencesService {
maxVolume, maxVolume,
downmixCenterBoost, downmixCenterBoost,
subtitlePosition, subtitlePosition,
subtitleAnchorToScreen,
defaultPlaybackSpeed, defaultPlaybackSpeed,
defaultBoxFitMode, defaultBoxFitMode,
themeMode, themeMode,
@@ -368,6 +368,7 @@ class FakePlayer implements Player {
int subtitlePosition = 100, int subtitlePosition = 100,
bool bold = false, bool bold = false,
bool italic = false, bool italic = false,
bool anchorToScreen = false,
}) async {} }) async {}
@override @override