fix(player): change the audio track, and load subtitles at all

Two faults, both present since v0.0.1, both found and confirmed on a device.

Audio track (DR-258). Jellyfin builds a transcode around one AudioStreamIndex,
so the alternate tracks are not in the stream that arrives — 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: logcat showed ExoPlayer
holding `Audio tracks: 1` while the menu listed every track in the file, so
each 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 rather than 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 (caught on device before it shipped).

Subtitles (DR-259). The URL was missing its `Stream.` route segment, so every
sideloaded subtitle 404ed; 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 and selection warned `available: 0`. Verified
against a live server: the built URL answers 404, the corrected one 200. The
tests that should have caught this asserted the shape of a mock helper that
restated the format string instead of the URL the app requests — so the new
test drives the repository itself, and failed red on the old URL.
This commit is contained in:
2026-08-23 19:15:05 +02:00
parent 231ffae626
commit 64de22bd51
8 changed files with 397 additions and 61 deletions
+28 -9
View File
@@ -145,18 +145,37 @@ async playerSetAudioTrack(streamIndex: number) : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_set_audio_track", { streamIndex });
},
/**
* Switch audio track - handles both HTML5 (stream reload) and native (direct switch)
* Switch audio track.
* Note: Frontend should handle saving series preferences after this command succeeds
*
* The split is the requirement: an HTML5 `<video>` element cannot be told to
* change audio track, so the stream is re-opened at the chosen
* `AudioStreamIndex` and the frontend seeks the reloaded element back to
* `position`; a native backend (ExoPlayer) switches in place by track-group
* index. libmpv implements neither — it is the audio-only backend here and
* leaves `PlayerBackend::set_audio_track` at its `not_implemented()` default,
* which is why IR-019 is met by these two paths rather than by MPV.
* What decides the route is **whether the stream in front of the engine
* carries the requested track at all** — see
* [`determine_audio_track_switch_strategy`]:
*
* TRACES: UR-021 | IR-019, DR-024
* - An HTML5 `<video>` element has no track-selection API, so the stream is
* always re-opened at the chosen `AudioStreamIndex` and the frontend seeks
* the reloaded element back to `position`.
* - A native backend playing a **direct play** holds the source file with
* every track in it, so ExoPlayer selects in place by track-group index.
* - A native backend playing a **transcode** does not. Jellyfin builds a
* transcode around one `AudioStreamIndex`, so the alternate tracks are not
* in the stream; the switch has to re-open it, which this command does
* itself and resumes at `current_position`.
*
* That last case is a bug fix, and it was the common case on Android: any
* source whose default audio codec the device cannot decode is transcoded, so
* ExoPlayer saw `Audio tracks: 1` while the menu listed every track in the
* file. The old code called `setAudioTrack(n)` regardless, which indexes
* ExoPlayer's audio track *groups*, found nothing at `n`, warned `Invalid
* audio track index` and dropped the request — the default track just kept
* playing, with nothing in the UI saying so.
*
* libmpv implements neither selection nor reload here — it is the audio-only
* backend and leaves `PlayerBackend::set_audio_track` at its
* `not_implemented()` default, which is why IR-019 is met by these paths
* rather than by MPV.
*
* TRACES: UR-021, UR-005 | IR-019, DR-024, DR-258
*/
async playerSwitchAudioTrack(repositoryHandle: string, streamIndex: number, arrayIndex: number, useHtml5: boolean, currentPosition: number | null, mediaSourceId: string | null) : Promise<AudioTrackSwitchResponse> {
return await TAURI_INVOKE("player_switch_audio_track", { repositoryHandle, streamIndex, arrayIndex, useHtml5, currentPosition, mediaSourceId });