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🅰️<n>` per stream, writing audio.<n>.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.
This commit is contained in:
Esa Kataja
2026-05-16 20:07:59 +03:00
parent 71f7ef7bca
commit 022d131cd6
+50 -19
View File
@@ -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:<n>`.
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 {