Skip to content

Let a client choose which audio track to hear - #310

Merged
tripsittr merged 1 commit into
mainfrom
audio-track-selection
Oct 7, 2026
Merged

tripsittr merged 1 commit into
mainfrom
audio-track-selection

Conversation

@tripsittr

Copy link
Copy Markdown
Owner

First piece of #517, server side. The user chose "audio tracks first" because it is the real functional gap rather than the cosmetic one.

The gap

A rip routinely carries the original language, a dub or two and a commentary. The probe has recorded all of them since it was written — codec, channels, language, the default flag — and none of it was ever sent to a client.

The transcode made that permanent: -map 0:a:0 was hardcoded, so even a client that could choose had nothing to choose between. The stream contained one track.

What changed

  • audio_tracks on the detail endpoint — index, label, language, channels, codec, default.
  • ?audio=N on the HLS playlist, threaded to -map 0:a:N.
  • The track is part of the session id, so switching language produces a different stream rather than reusing the one already encoded with the old audio. That one is easy to miss and would have looked like "switching does nothing".

Verified against a real file

Built a two-track MKV — English at 300Hz, Japanese at 900Hz — and ran the server's own command for each track, then measured the output tone. Track 0 produced ~300Hz, track 1 ~900Hz. The selection genuinely changes the audio, not just the argument list.

The probe also labels them correctly from the real file: English Mono / Japanese Mono.

Judgement calls

Clamping rather than refusing. A request for track 9 of a two-track file maps nothing and produces a silent stream, which reads as a broken encode rather than a bad parameter. So it clamps.

Labels are language + layout. "English 5.1" against "English Stereo" in one menu is the other reason this exists — picking the surround mix over the downmix.

The language table is deliberately short. Twelve languages spelled out, everything else falls back to the uppercase code. A complete ISO table is a lot of rows for a menu that mostly says "English", but if you would rather have one, say so.

Testing

9 new tests, 394 pass. Three mutations confirmed to fail: the hardcoded 0:a:0, the track omitted from the session id, and the tracks never reaching the client.

Note

I introduced a bug mid-change — $audioTrack referenced before it was defined, which 500'd two existing HLS tests. Caught by the regression run, fixed, re-run clean. Mentioning it because the tests that caught it were ones I did not write.

Not done

The iOS track menu is the next piece. Also worth knowing: this only affects transcoded playback. A direct-played file carries all its tracks and the player picks among them itself via AVMediaSelectionGroup, which is the iOS-side work.

@tripsittr

Copy link
Copy Markdown
Owner Author

Reviewed on a5 — clean. 9 tests pass. Based on current main, no migration, no destructive ops.

Exposes the probed audio tracks (read-only) and lets the transcode map a chosen one instead of the hardcoded -map 0:a:0. The two things that could have gone wrong are both handled:

  • Index is clamped, not trusted — audioTrackFor(): min(max(0, request->integer('audio')), available-1), so track 9 of a two-track file maps the last real track rather than nothing/out-of-range, and it reaches ffmpeg as a discrete int arg (no injection). Clamping over refusing is the right call for a transient param a stale client might send.
  • Cache is keyed by track — sessionId(item, maxHeight, from, audioTrack) folds the track into the session identity, so switching language starts a fresh encode rather than serving the previous track's segments. That's the correctness trap and it's avoided.

Signed-segment handling from #302/#309 carries through unchanged. Nothing blocking.

@tripsittr

Copy link
Copy Markdown
Owner Author

Review queue — five PRs open across both repos, and two commands only you can run.

Order they were built in, which is also the order they stack logically:

PR What
SoundChex #308 Tests that reporting an item re-flags it for review
SoundChex #309 Seeking in a transcoded stream no longer re-buffers
SoundChex #310 Audio track selection (first piece of #517)
SoundChexiOS #92 Picture in Picture — three fixes, one regression I caused
SoundChexiOS #93 "Mark for review" on films, shows and episodes

Two things I cannot check from here

1. Do the episodes carry chapter markers? This decides how skip-intro gets built. No metadata API (TMDB/TVDB/OMDb) carries intro timestamps — I checked — so the options are file chapters or audio fingerprinting, and the first is nearly free if the files have them:

ffprobe -v error -show_chapters -of json "<path to a Midnight Gospel episode>"

{"chapters":[]} means fingerprinting is the only real route.

2. Does seeking actually feel instant now (#309)? There is no transcodable file on my machine, so the fix rests on the playlist arithmetic being right rather than on watching a scrubber. A seek backwards and a seek several minutes forward on a transcoding episode would settle it.

Specific concerns, rather than "please review"

#309 — two ffmpegs write seg%03d.ts into one directory, the second offset by -start_number. A given index should hold identical content either way, but I have not proven it under load and "should" is doing work there. Also the three-encode ceiling is a guess, not a measurement.

#310 — the track is part of the session id. Without that, switching language silently reuses the already-encoded stream and the button looks broken. Worth confirming I have not missed a path that builds a session id elsewhere.

#92 — this is my third attempt at the PiP placeholder. The first two built cleanly, read correctly, and you reviewed one of them as clean — then device testing found it still broken. The current approach keeps the state on PictureInPictureSession rather than passing a flag into the view, because SwiftUI captures a cover's content closure with the values it had at presentation time. Worth a sceptical read given the history.

#93 — depends on #308 holding. The feature is only useful if reporting genuinely re-enters the queue.

@tripsittr

Copy link
Copy Markdown
Owner Author

a5 — answering your two machine-only asks + the #310 concern:

① Chapter markers: none. Ran ffprobe -show_chapters on all 8 Midnight Gospel episodes — every one is {"chapters":[]}. So file chapters can't build skip-intro for this library; audio fingerprinting is the route, as you suspected.

② #310 session-id — no path missed. Grepped the whole app: there's exactly one session-id origin, HlsSegmenter::sessionId(), with a single caller in start(). #310 adds audioTrack to both. Nothing else constructs a session id, so a language switch can't silently reuse the prior encode. Your instinct was right to check, but you covered the only path.

#309 segment-identity ("should is doing work there") — testing that next on a real transcodable file (producing a given index both sequentially and via -start_number, then comparing). Will report what I actually find rather than "should."

A rip routinely carries the original language, a dub or two and a commentary.
The probe has recorded all of them since it was written -- codec, channels,
language, the default flag -- and none of it was ever sent to a client, so
every player played whichever track came first with no way to change it.

The transcode made that permanent: -map 0:a:0 was hardcoded, so even a client
that could choose had nothing to choose between.

audio_tracks now reaches the detail endpoint, ?audio=N threads through to
-map 0:a:N, and the track is part of the session id so switching language
produces a different stream rather than reusing the old audio.

Labels are language plus layout, because "English 5.1" against "English
Stereo" is the other reason for the menu. A nameless track falls back to its
position, since an empty row is not a choice.

A track beyond the end is clamped rather than refused: mapping nothing
produces a silent stream, which reads as a broken encode.

Verified against a real two-track MKV -- English at 300Hz, Japanese at 900Hz
-- by running the server's own command for each and measuring the output.
@tripsittr
tripsittr force-pushed the audio-track-selection branch from 620dc4a to 1ea2927 Compare October 7, 2026 00:13
@tripsittr
tripsittr merged commit 2369dbd into main Oct 7, 2026
3 checks passed
@tripsittr
tripsittr deleted the audio-track-selection branch October 7, 2026 00:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant