fix(transcoding): tag the inserted mp3 frame as Info, not Xing

The frame inserted into piped mp3 transcodes carried a Xing tag with only a
frame count. Media3/ExoPlayer, which most Android clients use, builds a
XingSeeker from a Xing tag and reports it seekable only when a seek table is
present, so a cached or downloaded transcode became unseekable there. Without
any frame it would have used constant-bitrate seeking. An Info tag takes Media3
down its constant-bitrate path, now with the declared duration.

Info is also what ffmpeg and LAME write for a CBR file, which is what the
default mp3 profile produces. Checked against the decoders on hand: ffprobe
reports 20.010s either way, Firefox reports 19.998s at loadedmetadata either
way, and decoded audio is unchanged. Apple's AudioToolbox ignores Info frames,
including ffmpeg's own, so it sees the same stream it saw before this series.
This commit is contained in:
Deluan 2026-09-24 17:08:33 -04:00
commit e918b1278b
3 changed files with 10 additions and 9 deletions

View file

@ -722,7 +722,7 @@ var _ = Describe("ffmpeg", func() {
Expect(readTotalSamples(out)).To(Equal(uint64(2 * 44100)))
})
It("inserts a Xing frame on a piped mp3 transcode", func() {
It("inserts an Info 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",
@ -734,7 +734,7 @@ var _ = Describe("ffmpeg", func() {
out, err := io.ReadAll(stream)
Expect(err).ToNot(HaveOccurred())
at := bytes.Index(out, []byte("Xing"))
at := bytes.Index(out, []byte("Info"))
Expect(at).To(BeNumerically(">", 0))
Expect(binary.BigEndian.Uint32(out[at+8:])).To(Equal(uint32(38)))
})

View file

@ -14,7 +14,7 @@ const (
mp3MaxPrefix = 64 << 10 // ffmpeg cannot write an attached picture to a pipe, so the tag stays small
)
// mp3Prefix returns the head of the stream with a Xing frame inserted before the first
// mp3Prefix returns the head of the stream with an Info frame inserted before the first
// audio frame, which ffmpeg omits on a pipe since it only knows the frame count once it
// can rewind. It returns the head unchanged when it cannot make sense of what ffmpeg wrote.
func (h *headerPatcher) mp3Prefix(buf []byte) ([]byte, error) {
@ -116,7 +116,7 @@ func isXingFrame(frame []byte, tagOffset int) bool {
return tag == "Xing" || tag == "Info"
}
// xingFrame builds a silent frame declaring how many frames follow it. It reuses the
// xingFrame builds a silent Info frame declaring how many frames follow it. It reuses the
// first frame's header, minus its CRC, so the two describe the same stream.
func xingFrame(first []byte, f mp3Frame, duration float32) ([]byte, bool) {
frames := math.Round(float64(duration) * float64(f.sampleRate) / float64(f.samples))
@ -127,7 +127,7 @@ func xingFrame(first []byte, f mp3Frame, duration float32) ([]byte, bool) {
copy(frame, first[:mp3HeaderLen])
frame[1] |= 0x01 // no CRC, so the tag follows the side info directly
at := mp3HeaderLen + f.sideInfo
copy(frame[at:], "Xing")
copy(frame[at:], "Info") // a Xing tag without a seek table marks the stream unseekable to some decoders
binary.BigEndian.PutUint32(frame[at+4:], 1) // only the frame count is present
binary.BigEndian.PutUint32(frame[at+8:], uint32(frames))
return frame, true

View file

@ -31,6 +31,7 @@ var _ = Describe("patchMP3Duration", func() {
Expect(err).ToNot(HaveOccurred())
Expect(pipedMP3[fixtureID3Len : fixtureID3Len+2]).To(Equal([]byte{0xFF, 0xFB}))
Expect(bytes.Contains(pipedMP3[:4096], []byte("Xing"))).To(BeFalse())
Expect(bytes.Contains(pipedMP3[:4096], []byte("Info"))).To(BeFalse())
})
readAll := func(in []byte, duration float32) []byte {
@ -45,11 +46,11 @@ var _ = Describe("patchMP3Duration", func() {
return string(b[at : at+4]), binary.BigEndian.Uint32(b[at+8:])
}
It("inserts a Xing frame declaring the duration in frames", func() {
It("inserts an Info frame declaring the duration in frames", func() {
out := readAll(pipedMP3, 1.0)
tag, frames := readXing(out, fixtureID3Len, stereoTagOffset)
Expect(tag).To(Equal("Xing"))
Expect(tag).To(Equal("Info"))
Expect(frames).To(Equal(uint32(38)), "1s at 44100Hz is 38 frames of 1152 samples")
flags := binary.BigEndian.Uint32(out[fixtureID3Len+stereoTagOffset+4:])
Expect(flags).To(Equal(uint32(1)), "only the frame count is present")
@ -87,7 +88,7 @@ var _ = Describe("patchMP3Duration", func() {
out := readAll(in, 1.0)
tag, frames := readXing(out, 0, monoTagOffset)
Expect(tag).To(Equal("Xing"))
Expect(tag).To(Equal("Info"))
Expect(frames).To(Equal(uint32(38)))
})
@ -97,7 +98,7 @@ var _ = Describe("patchMP3Duration", func() {
out := readAll(in, 1.0)
tag, _ := readXing(out, 0, stereoTagOffset)
Expect(tag).To(Equal("Xing"))
Expect(tag).To(Equal("Info"))
Expect(out[fixtureFrameLen:]).To(Equal(in))
})