From 022d131cd6c9390643a22cd6c6ec6dc474ada3f6 Mon Sep 17 00:00:00 2001 From: Esa Kataja Date: Sat, 16 May 2026 20:05:29 +0300 Subject: [PATCH] fix: preserve all audio tracks and align language metadata indices Bug #5: extractAudio invoked ffmpeg without `-map`, so default stream selection kept only one audio track from sources with multiple audio streams (e.g. eng/fra/jpn Blu-rays). It now enumerates audio streams from the streamLangs already fetched by the caller and runs one `ffmpeg -map 0:a:` per stream, writing audio..wav. Bug #11: the audio language metadata loop in encodeVideo computed the output index by counting source-side audio streams with a lower Index, which drifted when some source streams lacked a language tag. It now walks opusFiles in output order and looks up the language at the matching source-audio position via a sorted helper. These ship together because #11 was masked by #5: when only one audio track survived extraction, the broken index calculation never produced a visible misalignment. Fixing #5 alone would have caused multi-track outputs with shuffled language tags; both fixes are required to land correct multi-track output. --- internal/encoder/encoder.go | 69 +++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/internal/encoder/encoder.go b/internal/encoder/encoder.go index 296d257..600deba 100644 --- a/internal/encoder/encoder.go +++ b/internal/encoder/encoder.go @@ -7,6 +7,7 @@ import ( "os/exec" "path/filepath" "regexp" + "sort" "strconv" "strings" @@ -223,7 +224,7 @@ func (e *Encoder) calculateZscaleWidth(path string, originalHeight int) (string, func (e *Encoder) Transcode(input string, job *types.Job, metadata *types.Metadata, interlaced bool, streamLangs []StreamMetadata) error { dir := filepath.Dir(input) - audioWavs, err := e.extractAudio(input, dir) + audioWavs, err := e.extractAudio(input, dir, streamLangs) if err != nil { return fmt.Errorf("extracting audio: %w", err) } @@ -242,16 +243,39 @@ func (e *Encoder) Transcode(input string, job *types.Job, metadata *types.Metada return nil } -func (e *Encoder) extractAudio(input, dir string) ([]string, error) { - cmd := exec.Command(e.ffmpegPath, "-i", input, - "-vn", "-c:a", "pcm_s16le", "-ar", "48000", - "-f", "wav", filepath.Join(dir, "audio.wav")) - if out, err := cmd.CombinedOutput(); err != nil { - return nil, fmt.Errorf("ffmpeg extract: %s %w", out, err) +// audioStreamsInSourceOrder returns the audio entries of streamLangs sorted by +// their source-side Index. The resulting slice position is the 0-based audio +// stream index used by ffmpeg selectors like `0:a:`. +func audioStreamsInSourceOrder(streamLangs []StreamMetadata) []StreamMetadata { + var audio []StreamMetadata + for _, s := range streamLangs { + if s.CodecType == "audio" { + audio = append(audio, s) + } + } + sort.Slice(audio, func(i, j int) bool { return audio[i].Index < audio[j].Index }) + return audio +} + +func (e *Encoder) extractAudio(input, dir string, streamLangs []StreamMetadata) ([]string, error) { + audio := audioStreamsInSourceOrder(streamLangs) + if len(audio) == 0 { + return nil, fmt.Errorf("no audio streams found") } - matches, _ := filepath.Glob(filepath.Join(dir, "audio.wav")) - return matches, nil + wavs := make([]string, 0, len(audio)) + for srcAudioIndex := range audio { + wavPath := filepath.Join(dir, fmt.Sprintf("audio.%d.wav", srcAudioIndex)) + cmd := exec.Command(e.ffmpegPath, "-i", input, + "-map", fmt.Sprintf("0:a:%d", srcAudioIndex), + "-vn", "-c:a", "pcm_s16le", "-ar", "48000", + "-f", "wav", wavPath) + if out, err := cmd.CombinedOutput(); err != nil { + return wavs, fmt.Errorf("ffmpeg extract a:%d: %s %w", srcAudioIndex, out, err) + } + wavs = append(wavs, wavPath) + } + return wavs, nil } func (e *Encoder) encodeOpus(wavs []string, dir string) ([]string, error) { @@ -319,17 +343,24 @@ func (e *Encoder) encodeVideo(input string, opusFiles []string, job *types.Job, args = append(args, "-c:s", "copy") args = append(args, "-map_metadata", "-1") - for _, stream := range streamLangs { - if stream.CodecType == "audio" && stream.Language != "" { - idx := 1 - for _, s := range streamLangs { - if s.CodecType == "audio" && s.Index < stream.Index { - idx++ - } - } - args = append(args, fmt.Sprintf("-metadata:s:a:%d", idx-1), fmt.Sprintf("language=%s", stream.Language)) - fmt.Printf("DEBUG setting audio language: stream %d -> language=%s\n", idx-1, stream.Language) + // Walk opusFiles by output audio index (0..N-1) and look up the source + // audio language at the same source-audio-index position. This keeps the + // `-metadata:s:a:i` index aligned with the output stream order, regardless + // of whether some source streams lacked a language tag. + audioStreams := audioStreamsInSourceOrder(streamLangs) + for i := range opusFiles { + if i >= len(audioStreams) { + break } + lang := audioStreams[i].Language + if lang == "" { + continue + } + args = append(args, fmt.Sprintf("-metadata:s:a:%d", i), fmt.Sprintf("language=%s", lang)) + fmt.Printf("DEBUG setting audio language: stream %d -> language=%s\n", i, lang) + } + + for _, stream := range streamLangs { if stream.CodecType == "subtitle" && stream.Language != "" { idx := 0 for _, s := range streamLangs {