fix(stream): clamp out-of-range audio-track index to 0🅰️0

The web persists the chosen audioIndex globally, so a value from a
multi-track file can arrive for a file with fewer tracks. buildHLSFFmpegArgsAt
mapped `-map 0🅰️N?` verbatim; the optional `?` then matched nothing and the
HLS output had NO audio stream (video-only — 2026-06-03, Wistoria S02E08 had
one audio track but the session carried audioIndex=2). Clamp an out-of-range
index to the first track so audio is never silently dropped.

Regression test: TestBuildHLSFFmpegArgsAudioClamp.
This commit is contained in:
Deivid Soto 2026-06-03 18:55:42 +02:00
parent 2148b0e2cc
commit 1814d59e09
2 changed files with 51 additions and 0 deletions

View file

@ -1217,6 +1217,17 @@ func buildHLSFFmpegArgsAt(cfg HLSSessionConfig, probe *StreamProbe, tmpDir strin
}
}
}
// Clamp to an audio track that actually exists. The web persists the chosen
// audioIndex globally, so a value from a multi-track file can arrive for a
// file with fewer tracks; `-map 0:a:N?` would then match nothing and the
// optional `?` silently yields a VIDEO-ONLY stream (no sound — 2026-06-03,
// Wistoria S02E08 had one audio track but the session carried audioIndex=2).
// Fall back to the first track so audio is never silently dropped.
if n := len(probe.AudioTracks); n > 0 && audioIdx >= n {
log.Printf("[hls %s] audioIndex %d out of range (%d audio track(s)) — using 0:a:0",
shortHLSID(cfg.SessionID), audioIdx, n)
audioIdx = 0
}
args = append(args, "-map", fmt.Sprintf("0:a:%d?", audioIdx))
// Video encode. Codec + preset come from the EncoderProfile resolved at

View file

@ -188,3 +188,43 @@ func TestBuildHLSFFmpegArgsBurnSubtitle(t *testing.T) {
}
})
}
// Audio clamp (2026-06-03 no-sound regression): the web persists audioIndex
// globally, so a stale value from a multi-track file can arrive for a file with
// fewer tracks. buildHLSFFmpegArgsAt must clamp an out-of-range index to 0:a:0
// rather than emit `-map 0:a:N?` for a track that doesn't exist — the optional
// `?` would otherwise silently drop audio and yield a video-only stream.
func TestBuildHLSFFmpegArgsAudioClamp(t *testing.T) {
cfg := func(audioIdx int) HLSSessionConfig {
return HLSSessionConfig{
SessionID: "audio",
SourcePath: "/tmp/movie.mkv",
Quality: "1080p",
AudioIndex: audioIdx,
Transcode: TranscodeRuntime{
FFmpegPath: "/usr/bin/ffmpeg",
FFprobePath: "/usr/bin/ffprobe",
HWAccel: HWAccelNone,
},
}
}
oneTrack := &StreamProbe{Width: 1920, Height: 1080, DurationSec: 100, AudioTracks: []ProbeAudioTrack{{}}}
t.Run("out-of-range index clamps to 0:a:0", func(t *testing.T) {
got := strings.Join(buildHLSFFmpegArgsAt(cfg(2), oneTrack, "/tmp/d", 0, 0), " ")
if !strings.Contains(got, "-map 0:a:0?") {
t.Errorf("out-of-range audioIndex must clamp to 0:a:0?: %s", got)
}
if strings.Contains(got, "0:a:2?") {
t.Errorf("must not map the non-existent 0:a:2: %s", got)
}
})
t.Run("in-range index is preserved", func(t *testing.T) {
twoTracks := &StreamProbe{Width: 1920, Height: 1080, DurationSec: 100, AudioTracks: []ProbeAudioTrack{{}, {}}}
got := strings.Join(buildHLSFFmpegArgsAt(cfg(1), twoTracks, "/tmp/d", 0, 0), " ")
if !strings.Contains(got, "-map 0:a:1?") {
t.Errorf("in-range audioIndex 1 must be preserved: %s", got)
}
})
}