mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 02:17:25 +02:00
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:
parent
79fc6fed60
commit
e918b1278b
3 changed files with 10 additions and 9 deletions
|
|
@ -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)))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue