fix(transcoding): declare the duration of a piped mp3 transcode

The mp3 muxer writes the Xing frame that carries the frame count only after
rewinding to the start of the stream, which it cannot do on a pipe (-f mp3 -).
An uncached transcode therefore declares its length nowhere: no Xing frame, and
no Content-Length unless the client asks for the estimate. A decoder has to
guess the duration from the bytes it has so far, and an offline copy stays
unseekable, the same defect #6105 fixed for FLAC.

Transcode now inserts a Xing frame ahead of the first audio frame, carrying the
frame count derived from the track duration. Only the frame count is declared:
a piped encode cannot know its own byte size, and a wrong size is worse than an
absent one. Decoded audio is unchanged, since decoders skip the tag frame.

Measured on a 20s 128kbps transcode: ffprobe reports 20.010s instead of
20.036s, and Firefox reports 19.998s at loadedmetadata instead of 2.04s
climbing over nine durationchange events. This does not make an uncached
transcode resumable; that needs range support, which only the cache path has.

Reported in #6170.
This commit is contained in:
Deluan 2026-09-24 16:42:48 -04:00 • committed by Deluan Quintão
commit c05fbd0edb
4 changed files with 366 additions and 2 deletions

View file

@ -1,7 +1,9 @@
package ffmpeg
import (
"bytes"
"context"
"encoding/binary"
"errors"
"io"
"os"
@ -719,6 +721,23 @@ var _ = Describe("ffmpeg", func() {
Expect(err).ToNot(HaveOccurred())
Expect(readTotalSamples(out)).To(Equal(uint64(2 * 44100)))
})
It("inserts a Xing frame on a piped mp3 transcode", func() {
stream, err := ff.Transcode(GinkgoT().Context(), TranscodeOptions{
Command: "ffmpeg -i %s -map 0:a:0 -v 0 -b:a 128k -f mp3 -",
Format: "mp3",
FilePath: "tests/fixtures/test.flac",
Duration: 1, // the fixture is exactly 1s at 44100Hz
})
Expect(err).ToNot(HaveOccurred())
defer stream.Close()
out, err := io.ReadAll(stream)
Expect(err).ToNot(HaveOccurred())
at := bytes.Index(out, []byte("Xing"))
Expect(at).To(BeNumerically(">", 0))
Expect(binary.BigEndian.Uint32(out[at+8:])).To(Equal(uint32(38)))
})
})
Context("stderr capture", func() {