diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt index 0985842b..eb2a81df 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt @@ -215,6 +215,7 @@ class ExoPlayerCore(private val activity: Activity) : @Volatile private var assVideoLatencyFrames = 0 private var subtitlePositionPercent: Int = 100 private var subtitleFontSize: Float = 55f + private var subtitleAnchorToScreen: Boolean = false private var lastSubtitleCues: List = emptyList() // Tracks whether a text track was selected on the previous onTracksChanged so we @@ -1757,7 +1758,8 @@ class ExoPlayerCore(private val activity: Activity) : videoHeight, pixelRatio, resizeMode, - videoZoomScale + videoZoomScale, + subtitleAnchorToScreen ) val bitmapDimensions = SubtitleViewLayout.bitmapDimensions( containerWidth, @@ -3911,7 +3913,8 @@ class ExoPlayerCore(private val activity: Activity) : bgOpacity: Int, subtitlePosition: Int = 100, bold: Boolean = false, - italic: Boolean = false + italic: Boolean = false, + anchorToScreen: Boolean = false ) { activity.runOnUiThread { // 1. Non-ASS subtitles: CaptionStyleCompat on SubtitleView @@ -3959,6 +3962,18 @@ class ExoPlayerCore(private val activity: Activity) : subtitlePositionPercent = clampedPosition 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 // numbers bypass SubtitleView bottom padding. Authored VTT line positions // 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.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") } } diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt index d84919e2..4d47155c 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt @@ -1069,6 +1069,7 @@ class ExoPlayerPlugin : val subtitlePosition = call.argument("subtitlePosition")?.toInt() ?: 100 val bold = call.argument("bold") ?: false val italic = call.argument("italic") ?: false + val anchorToScreen = call.argument("anchorToScreen") ?: false if (usingMpvFallback) { // MPV fallback handles styling via setProperty, no-op here @@ -1076,7 +1077,7 @@ class ExoPlayerPlugin : 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) } diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/SubtitleViewLayout.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/SubtitleViewLayout.kt index f56084e9..95c2c9f6 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/SubtitleViewLayout.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/SubtitleViewLayout.kt @@ -13,12 +13,18 @@ internal object SubtitleViewLayout { videoHeight: Int, pixelRatio: Float, resizeMode: Int, - zoomScale: Float + zoomScale: Float, + anchorToScreen: Boolean = false ): SubtitleViewDimensions? { val videoAspect = videoAspect(videoWidth, videoHeight, pixelRatio) ?: 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) } diff --git a/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/SubtitleViewLayoutTest.kt b/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/SubtitleViewLayoutTest.kt index 9edf8266..518a5b3e 100644 --- a/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/SubtitleViewLayoutTest.kt +++ b/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/SubtitleViewLayoutTest.kt @@ -45,9 +45,49 @@ class SubtitleViewLayoutTest { 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( resizeMode: Int, - zoomScale: Float = 1f + zoomScale: Float = 1f, + anchorToScreen: Boolean = false ): SubtitleViewDimensions? = SubtitleViewLayout.textDimensions( containerWidth = 2424, containerHeight = 1080, @@ -55,7 +95,8 @@ class SubtitleViewLayoutTest { videoHeight = 1080, pixelRatio = 1f, resizeMode = resizeMode, - zoomScale = zoomScale + zoomScale = zoomScale, + anchorToScreen = anchorToScreen ) private fun bitmapDimensions( diff --git a/lib/i18n/az.i18n.json b/lib/i18n/az.i18n.json index 8bbcd7e7..541686b1 100644 --- a/lib/i18n/az.i18n.json +++ b/lib/i18n/az.i18n.json @@ -744,6 +744,8 @@ "overrideStrip": "Formatlaşdırmanı sil", "positionTop": "Yuxarı", "positionBottom": "Aşağı", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Qalın", "italic": "Kursiv", "renderResolution": "Emal imkanı (Resolution)", diff --git a/lib/i18n/bg.i18n.json b/lib/i18n/bg.i18n.json index 7555e0f5..83e693f2 100644 --- a/lib/i18n/bg.i18n.json +++ b/lib/i18n/bg.i18n.json @@ -744,6 +744,8 @@ "overrideStrip": "Премахване на стиловете", "positionTop": "Горе", "positionBottom": "Долу", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Получер", "italic": "Курсив", "renderResolution": "Резолюция на изобразяване", diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index 9e72c094..478827e9 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -744,6 +744,8 @@ "overrideStrip": "Fjern formatering", "positionTop": "Øverst", "positionBottom": "Nederst", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Fed", "italic": "Kursiv", "renderResolution": "Gengivelsesopløsning", diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index ac591d80..467423bd 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -744,6 +744,8 @@ "overrideStrip": "Formatierung entfernen", "positionTop": "Oben", "positionBottom": "Unten", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Fett", "italic": "Kursiv", "renderResolution": "Render-Auflösung", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 0e49cc54..eacde9f7 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -744,6 +744,8 @@ "overrideStrip": "Remove styling", "positionTop": "Top", "positionBottom": "Bottom", + "anchorToScreen": "Anchor to Screen", + "anchorToScreenDescription": "Show text subtitles in the black bars below widescreen video", "bold": "Bold", "italic": "Italic", "renderResolution": "Render Resolution", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index 7e05f7fd..2d044bcb 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -744,6 +744,8 @@ "overrideStrip": "Quitar estilos", "positionTop": "Arriba", "positionBottom": "Abajo", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Negrita", "italic": "Cursiva", "renderResolution": "Resolución de renderizado", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index 9348a495..8c9c3649 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -744,6 +744,8 @@ "overrideStrip": "Supprimer le style", "positionTop": "Haut", "positionBottom": "Bas", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Gras", "italic": "Italique", "renderResolution": "Résolution de rendu", diff --git a/lib/i18n/hu.i18n.json b/lib/i18n/hu.i18n.json index edac4ca7..7c894239 100644 --- a/lib/i18n/hu.i18n.json +++ b/lib/i18n/hu.i18n.json @@ -744,6 +744,8 @@ "overrideStrip": "Stílus eltávolítása", "positionTop": "Fent", "positionBottom": "Lent", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Félkövér", "italic": "Dőlt", "renderResolution": "Renderelési felbontás", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 4dbe9513..408bb34d 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -744,6 +744,8 @@ "overrideStrip": "Rimuovi stile", "positionTop": "In alto", "positionBottom": "In basso", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Grassetto", "italic": "Corsivo", "renderResolution": "Risoluzione di rendering", diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index 5f427482..e9b90705 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -740,6 +740,8 @@ "overrideStrip": "スタイルを削除", "positionTop": "上", "positionBottom": "下", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "太字", "italic": "斜体", "renderResolution": "レンダリング解像度", diff --git a/lib/i18n/kk.i18n.json b/lib/i18n/kk.i18n.json index cd71353f..93dc2225 100644 --- a/lib/i18n/kk.i18n.json +++ b/lib/i18n/kk.i18n.json @@ -744,6 +744,8 @@ "overrideStrip": "Форматтауды жою", "positionTop": "Жоғары", "positionBottom": "Төмен", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Қалың", "italic": "Көлбеу", "renderResolution": "Рендеринг ажыратымдылығы", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 327b9734..ebec7516 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -740,6 +740,8 @@ "overrideStrip": "스타일 제거", "positionTop": "위", "positionBottom": "아래", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "굵게", "italic": "기울임꼴", "renderResolution": "렌더링 해상도", diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index fda8aba3..f7f364c4 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -744,6 +744,8 @@ "overrideStrip": "Fjern formatering", "positionTop": "Øverst", "positionBottom": "Nederst", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Fet", "italic": "Kursiv", "renderResolution": "Gjengivelsesoppløsning", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index d3aee80c..d9a23895 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -744,6 +744,8 @@ "overrideStrip": "Opmaak verwijderen", "positionTop": "Bovenaan", "positionBottom": "Onderaan", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Vet", "italic": "Cursief", "renderResolution": "Renderresolutie", diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index b9d28d14..fc65a781 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -752,6 +752,8 @@ "overrideStrip": "Usuń style", "positionTop": "Góra", "positionBottom": "Dół", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Pogrubienie", "italic": "Kursywa", "renderResolution": "Rozdzielczość renderowania", diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index 9e13a913..372648a9 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -744,6 +744,8 @@ "overrideStrip": "Remover estilo", "positionTop": "Superior", "positionBottom": "Inferior", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Negrito", "italic": "Itálico", "renderResolution": "Resolução de renderização", diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index c0ec2f39..8b375099 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -752,6 +752,8 @@ "overrideStrip": "Удалить стили", "positionTop": "Сверху", "positionBottom": "Снизу", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Жирный", "italic": "Курсив", "renderResolution": "Разрешение отрисовки", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 6891ea51..c4ebdd6f 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 22 -/// Strings: 38352 (1743 per locale) +/// Strings: 38354 (1743 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index b73e359b..b9b07be5 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -2212,6 +2212,12 @@ class Translations$subtitlingStyling$en { /// en: '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' String get bold => 'Bold'; @@ -6762,6 +6768,8 @@ extension on Translations { 'subtitlingStyling.overrideStrip' => 'Remove styling', 'subtitlingStyling.positionTop' => 'Top', 'subtitlingStyling.positionBottom' => 'Bottom', + 'subtitlingStyling.anchorToScreen' => 'Anchor to Screen', + 'subtitlingStyling.anchorToScreenDescription' => 'Show text subtitles in the black bars below widescreen video', 'subtitlingStyling.bold' => 'Bold', 'subtitlingStyling.italic' => 'Italic', 'subtitlingStyling.renderResolution' => 'Render Resolution', @@ -7091,10 +7099,10 @@ extension on Translations { 'explore.stats.favorited' => ({required Object n}) => '${n} favorites', '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.votes' => ({required Object n}) => '${n} votes', - 'explore.stats.watching' => ({required Object n}) => '${n} watching it', _ => null, } ?? 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.onHold' => ({required Object n}) => '${n} on hold', 'explore.stats.dropped' => ({required Object n}) => '${n} dropped', @@ -7605,10 +7613,10 @@ extension on Translations { 'externalPlayer.useExternalPlayerDescription' => 'Open videos in another app', 'externalPlayer.selectPlayer' => 'Select Player', 'externalPlayer.customPlayers' => 'Custom Players', - 'externalPlayer.systemDefault' => 'System Default', - 'externalPlayer.addCustomPlayer' => 'Add Custom Player', _ => null, } ?? switch (path) { + 'externalPlayer.systemDefault' => 'System Default', + 'externalPlayer.addCustomPlayer' => 'Add Custom Player', 'externalPlayer.playerName' => 'Player Name', 'externalPlayer.playerNameHint' => 'My Player', 'externalPlayer.playerCommand' => 'Command', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 56e53251..14cd935b 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -744,6 +744,8 @@ "overrideStrip": "Ta bort formatering", "positionTop": "Överst", "positionBottom": "Nederst", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Fet", "italic": "Kursiv", "renderResolution": "Renderingsupplösning", diff --git a/lib/i18n/tr.i18n.json b/lib/i18n/tr.i18n.json index a1189f05..9ed18da5 100644 --- a/lib/i18n/tr.i18n.json +++ b/lib/i18n/tr.i18n.json @@ -744,6 +744,8 @@ "overrideStrip": "Biçimlendirmeyi kaldır", "positionTop": "Üst", "positionBottom": "Alt", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Kalın", "italic": "İtalik", "renderResolution": "İşleme Çözünürlüğü", diff --git a/lib/i18n/uz.i18n.json b/lib/i18n/uz.i18n.json index 2c417c75..f75bb381 100644 --- a/lib/i18n/uz.i18n.json +++ b/lib/i18n/uz.i18n.json @@ -744,6 +744,8 @@ "overrideStrip": "Formatlashni olib tashlash", "positionTop": "Yuqori", "positionBottom": "Pastki", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "Qalin", "italic": "Qiya", "renderResolution": "Renderlash oʻlchamlari", diff --git a/lib/i18n/zh-Hant.i18n.json b/lib/i18n/zh-Hant.i18n.json index 2d7feaa0..65d60575 100644 --- a/lib/i18n/zh-Hant.i18n.json +++ b/lib/i18n/zh-Hant.i18n.json @@ -740,6 +740,8 @@ "overrideStrip": "移除樣式", "positionTop": "頂部", "positionBottom": "底部", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "粗體", "italic": "斜體", "renderResolution": "渲染解析度", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index 89960b3a..d2ecf128 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -740,6 +740,8 @@ "overrideStrip": "移除样式", "positionTop": "顶部", "positionBottom": "底部", + "anchorToScreen": "", + "anchorToScreenDescription": "", "bold": "粗体", "italic": "斜体", "renderResolution": "渲染分辨率", diff --git a/lib/mpv/player/platform/player_android.dart b/lib/mpv/player/platform/player_android.dart index ecd1dc65..fcd30caa 100644 --- a/lib/mpv/player/platform/player_android.dart +++ b/lib/mpv/player/platform/player_android.dart @@ -542,6 +542,7 @@ class PlayerAndroid extends PlayerBase { int subtitlePosition = 100, bool bold = false, bool italic = false, + bool anchorToScreen = false, }) async { if (disposed || !initialized) return; await invoke('setSubtitleStyle', { @@ -554,6 +555,7 @@ class PlayerAndroid extends PlayerBase { 'subtitlePosition': subtitlePosition, 'bold': bold, 'italic': italic, + 'anchorToScreen': anchorToScreen, }); } diff --git a/lib/mpv/player/player.dart b/lib/mpv/player/player.dart index a07fed4c..c05d26d4 100644 --- a/lib/mpv/player/player.dart +++ b/lib/mpv/player/player.dart @@ -314,6 +314,7 @@ abstract class Player { int subtitlePosition = 100, bool bold = false, bool italic = false, + bool anchorToScreen = false, }); /// Apply the box-fit mode to the native video layer diff --git a/lib/mpv/player/player_base.dart b/lib/mpv/player/player_base.dart index 4100a69b..58487d5a 100644 --- a/lib/mpv/player/player_base.dart +++ b/lib/mpv/player/player_base.dart @@ -1097,6 +1097,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { int subtitlePosition = 100, bool bold = false, bool italic = false, + bool anchorToScreen = false, }) async {} @override diff --git a/lib/screens/settings/subtitle_styling_screen.dart b/lib/screens/settings/subtitle_styling_screen.dart index 2ae4c00a..e89722f7 100644 --- a/lib/screens/settings/subtitle_styling_screen.dart +++ b/lib/screens/settings/subtitle_styling_screen.dart @@ -42,6 +42,9 @@ class SubtitleStylingScreen extends StatelessWidget { @override 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( title: Text(t.screens.subtitleStyling), children: [ @@ -108,6 +111,13 @@ class SubtitleStylingScreen extends StatelessWidget { min: 0, max: 100, ), + if (exoActive) + SettingSwitchTile( + pref: SettingsService.subtitleAnchorToScreen, + icon: Symbols.fit_screen_rounded, + title: t.subtitlingStyling.anchorToScreen, + subtitle: t.subtitlingStyling.anchorToScreenDescription, + ), SettingSwitchTile( pref: SettingsService.subtitleBold, icon: Symbols.format_bold_rounded, diff --git a/lib/screens/video_player/parts/playback_open.dart b/lib/screens/video_player/parts/playback_open.dart index 84bc7e64..c2c98c20 100644 --- a/lib/screens/video_player/parts/playback_open.dart +++ b/lib/screens/video_player/parts/playback_open.dart @@ -432,6 +432,7 @@ extension _VideoPlayerOpenMethods on VideoPlayerScreenState { subtitlePosition: settingsService.read(SettingsService.subtitlePosition), bold: settingsService.read(SettingsService.subtitleBold), italic: settingsService.read(SettingsService.subtitleItalic), + anchorToScreen: settingsService.read(SettingsService.subtitleAnchorToScreen), ); } diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index c045dfd6..42de9ad0 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -377,6 +377,12 @@ class SettingsService extends BaseSharedPreferencesService { ); static const subtitleBold = BoolPref('subtitle_bold'); 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 rememberTrackSelections = BoolPref('remember_track_selections', defaultValue: true); @@ -935,6 +941,7 @@ class SettingsService extends BaseSharedPreferencesService { maxVolume, downmixCenterBoost, subtitlePosition, + subtitleAnchorToScreen, defaultPlaybackSpeed, defaultBoxFitMode, themeMode, diff --git a/test/services/music/music_playback_service_test.dart b/test/services/music/music_playback_service_test.dart index cf2e2269..90baa76d 100644 --- a/test/services/music/music_playback_service_test.dart +++ b/test/services/music/music_playback_service_test.dart @@ -368,6 +368,7 @@ class FakePlayer implements Player { int subtitlePosition = 100, bool bold = false, bool italic = false, + bool anchorToScreen = false, }) async {} @override