fix(android): subtitles on the picture, not on a black bar
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 15m35s
🏗️ Build and Test JellyTau / Supply Chain (push) Successful in 40s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m34s
Traceability Validation / Check Requirement Traces (push) Successful in 12s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m30s
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 15m35s
🏗️ Build and Test JellyTau / Supply Chain (push) Successful in 40s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m34s
Traceability Validation / Check Requirement Traces (push) Successful in 12s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m30s
Every subtitle line arrived in an opaque black box wide enough to sit across the picture. That box is what Android hands back when the viewer has set no captioning preferences: SubtitleView.setUserDefaultStyle() reads the system style and falls back to media3's DEFAULT, which is white on opaque black. Dropping the box is not the same as replacing the style. Someone who has configured captions in accessibility settings has said something specific about colour, typeface and edges, and overriding all of it to remove a background would answer a question they did not ask. Their style is kept and only the two colours that paint a box — background and window — are cleared. A style specifying no edge gets a black outline, since without a box the text must supply its own contrast or it is unreadable over a bright scene. One that already names an edge keeps it: that viewer has said how they want their captions separated from the picture. Compiles and packages, but NOT yet seen on a device — the tablet was disconnected before it could be deployed, so the requirement is recorded as "Done (pending device verification)" and this must not be tagged into a release until someone has looked at it. TRACES: UR-020 | DR-261
This commit is contained in:
@@ -9,6 +9,17 @@ generated trace matrix lives in [docs/traceability.md](docs/traceability.md).
|
||||
For how long each fixed defect had been shipping before it was found, see
|
||||
[docs/defect-windows.md](docs/defect-windows.md).
|
||||
|
||||
## Unreleased
|
||||
|
||||
### 🐛 Fixes
|
||||
|
||||
- **Subtitles sit on the picture instead of on a black bar.** Every line arrived
|
||||
in an opaque box, which is what Android hands back when no captioning
|
||||
preferences have been set. The box is gone; the text keeps its own outline so
|
||||
it stays readable over a bright scene. If you *have* set up captions in
|
||||
Android's accessibility settings, your colours, typeface and edges are still
|
||||
used — only the background is cleared. (UR-020 → DR-261)
|
||||
|
||||
## v0.11.2
|
||||
|
||||
### 🐛 Fixes
|
||||
|
||||
@@ -454,6 +454,7 @@ Internal architecture, components, and application logic.
|
||||
| DR-258 | An audio-track change is honoured by **re-opening the stream** when the stream cannot carry the track. Jellyfin builds a transcode around one `AudioStreamIndex`, so the alternate tracks are not in it — but the native path only ever called `setAudioTrack(n)`, which indexes ExoPlayer's audio track *groups*. On Android that is the common case, since any source whose default audio codec the device cannot decode is transcoded: ExoPlayer held one audio track while the menu listed every track in the file, so every selection warned `Invalid audio track index` and was dropped, leaving the default track playing with nothing in the UI saying so. `determine_audio_track_switch_strategy` now decides by whether the stream in front of the engine carries the track at all — a direct play still selects in place, a transcode is re-negotiated at the chosen index and resumed. Where it resumes is the player's answer, not the UI's: the native path has no `<video>` element to read, so it sends no position, and defaulting that to zero re-opened the film at the beginning | Player | UR-021, UR-005 | Done |
|
||||
| DR-259 | Subtitle URLs address Jellyfin's route, `/Videos/{item}/{source}/Subtitles/{index}/Stream.{format}`. The `Stream.` segment was missing, which matches no route and 404s, so every sideloaded subtitle failed to fetch. Since media3 1.5 a sideloaded text track only becomes a track group once its file is parsed, so 42 failed fetches left ExoPlayer with no text tracks at all and subtitle selection warned `available: 0` and did nothing. The URL tests that existed asserted the shape of a *mock helper* duplicating the format string rather than the URL the app requests, which is why a route error survived from the first release | Repository | UR-020 | Done |
|
||||
| DR-260 | Subtitle cues are **drawn**. ExoPlayer decodes subtitles and delivers them to a listener; it draws none of them itself, and native video here is a bare `TextureView` the WebView composites over rather than a `PlayerView`, so nothing was holding the cues and a selected track rendered nowhere. The gap was invisible while every subtitle URL 404ed (DR-259) — with no text track to select there was never a cue to drop, so fixing the URL is what exposed it. `media3-ui`'s `SubtitleView` now takes each `CueGroup` from `onCues` and is attached at index 1 of the content view: above the video, still below the WebView, so cues sit over the picture and under the app's own controls. It is fitted to the letterboxed video rect rather than the screen, so cues stay inside the picture and follow it on rotation, and is torn down with the surface it belongs to. Verified on a device | Player | UR-020, UR-003 | Done |
|
||||
| DR-261 | Subtitles are drawn **over the picture, not on a black bar across it**. `SubtitleView.setUserDefaultStyle()` reads Android's captioning preferences and falls back to media3's `DEFAULT` when the viewer has set none — and that default is white on opaque black, so every line arrived in a box as wide as the text. The viewer's own style is kept and only the two colours that paint a box, background and window, are cleared: someone who has configured captions in accessibility settings has said something specific about colour, typeface and edges, and replacing all of it to remove a background would answer a question they did not ask. A style specifying no edge gets a black outline, because without a box the text must supply its own contrast or it is unreadable over a bright scene; a style that already names an edge keeps it | Player | UR-020 | Done (pending device verification) |
|
||||
| DR-198 | The webview runs under a real Content-Security-Policy, and the asset protocol is scoped to the one directory it still serves. `csp` was `null`, which disables CSP entirely: any script that reached the web layer — through a future `{@html}`, a dependency, or a devtools paste — would have inherited the whole IPC surface, and with it the user's session. `script-src 'self'` (Tauri injects a nonce for SvelteKit's inline bootstrap script at build time, so no `'unsafe-inline'` is needed) plus `object-src`/`frame-src 'none'` and `base-uri 'self'` is the part that is genuinely restrictive. `img-src`/`media-src`/`connect-src` cannot be: the Jellyfin origin is typed in by the user at run time and is commonly plain `http` on a LAN, so they allow `http:`/`https:` — a wide grant for *data*, but one that still bars `file:`, `filesystem:` and scripting schemes, and leaves `script-src` untouched. `style-src` keeps `'unsafe-inline'` because Svelte compiles `style="…"` attributes (including `app.html`'s `display: contents` wrapper) into markup; this is safe only while no `<style>` element survives into `index.html`, since a nonce there would make Tauri's injection outrank — and therefore void — `'unsafe-inline'`. `worker-src blob:` and `media-src blob:` are hls.js: it demuxes in a worker built from a blob and attaches MSE through `URL.createObjectURL`. `asset:` and `http://asset.localhost` are the same protocol under the two naming schemes `convertFileSrc` emits (custom scheme on Linux/macOS, `http` host on Windows/Android); `ipc:`/`http://ipc.localhost` is the invoke transport, which would otherwise be blocked by `connect-src`. A run-time CSP naming the server origin exactly was rejected: Tauri computes the header from immutable config when it serves the HTML, so it would mean rebuilding config and reloading the webview on every server change, for a policy the user can already point anywhere. The asset-protocol scope narrows from `$APPDATA/**` to `$APPDATA/thumbnails/**` — since DR-137 moved downloaded media to the loopback server, `imageCache` is the only `convertFileSrc` caller left, so the database and the encrypted-token fallback file no longer sit inside the grant | Security | UR-012, UR-071 | Done |
|
||||
|
||||
---
|
||||
|
||||
@@ -1330,10 +1330,8 @@ class JellyTauPlayer(private val appContext: Context) {
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
// Honour the viewer's system captioning preferences (size,
|
||||
// colour, edge style) and whatever styling the track carries —
|
||||
// the same defaults a PlayerView would have applied.
|
||||
setUserDefaultStyle()
|
||||
// Text over the picture, not a black bar across it. (DR-261)
|
||||
setStyle(captionStyle())
|
||||
setUserDefaultTextSize()
|
||||
}
|
||||
android.util.Log.d("JellyTauPlayer", "SubtitleView created")
|
||||
@@ -1341,6 +1339,61 @@ class JellyTauPlayer(private val appContext: Context) {
|
||||
return videoView!!.hashCode()
|
||||
}
|
||||
|
||||
/**
|
||||
* The caption style to draw cues in: the viewer's own, with the background
|
||||
* taken out.
|
||||
*
|
||||
* `setUserDefaultStyle()` reads Android's captioning preferences and falls
|
||||
* back to media3's `DEFAULT` when the viewer has set none — and that
|
||||
* default is white on **opaque black**, which is what put a black box under
|
||||
* every line, wide enough to sit across the picture.
|
||||
*
|
||||
* Dropping the box is not the same as replacing the style. A viewer who has
|
||||
* configured captions in accessibility settings has said something specific
|
||||
* about colour, typeface and edges, and overriding all of that to get a
|
||||
* transparent background would be answering a question they did not ask. So
|
||||
* their style is kept and only the two colours that paint a box —
|
||||
* background and window — are cleared.
|
||||
*
|
||||
* With no box the text supplies its own contrast or it is unreadable over a
|
||||
* bright scene, so a style that asked for no edge gets a black outline. One
|
||||
* that already specifies an edge keeps it: that viewer has already said how
|
||||
* they want their captions separated from the picture.
|
||||
*
|
||||
* TRACES: UR-020 | DR-261
|
||||
*/
|
||||
private fun captionStyle(): androidx.media3.ui.CaptionStyleCompat {
|
||||
val base = try {
|
||||
val captioning = appContext.getSystemService(Context.CAPTIONING_SERVICE)
|
||||
as? android.view.accessibility.CaptioningManager
|
||||
if (captioning != null && captioning.isEnabled) {
|
||||
androidx.media3.ui.CaptionStyleCompat.createFromCaptionStyle(captioning.userStyle)
|
||||
} else {
|
||||
androidx.media3.ui.CaptionStyleCompat.DEFAULT
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// A captioning service that refuses to answer is not a reason to
|
||||
// draw nothing; fall back to the same default media3 would use.
|
||||
android.util.Log.w("JellyTauPlayer", "Captioning preferences unavailable", e)
|
||||
androidx.media3.ui.CaptionStyleCompat.DEFAULT
|
||||
}
|
||||
|
||||
val needsOwnEdge = base.edgeType == androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_NONE
|
||||
|
||||
return androidx.media3.ui.CaptionStyleCompat(
|
||||
base.foregroundColor,
|
||||
android.graphics.Color.TRANSPARENT,
|
||||
android.graphics.Color.TRANSPARENT,
|
||||
if (needsOwnEdge) {
|
||||
androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_OUTLINE
|
||||
} else {
|
||||
base.edgeType
|
||||
},
|
||||
if (needsOwnEdge) android.graphics.Color.BLACK else base.edgeColor,
|
||||
base.typeface
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The view that draws subtitle cues, for VideoOverlayManager to attach
|
||||
* directly above the video and below the WebView. Null before the first
|
||||
|
||||
Reference in New Issue
Block a user