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 {