mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 02:17:25 +02:00
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.
156 lines
4.8 KiB
Go
156 lines
4.8 KiB
Go
package ffmpeg
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"testing/iotest"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
// Offsets of the Xing tag inside the first frame, for the two layouts the specs use.
|
|
const (
|
|
stereoTagOffset = 36 // 4 header + 32 side info
|
|
monoTagOffset = 21 // 4 header + 17 side info
|
|
fixtureID3Len = 39581 // where the first frame of tests/fixtures/test.mp3 starts
|
|
fixtureFrameLen = 627 // its frame size, at 192kbps and 44100Hz
|
|
)
|
|
|
|
var _ = Describe("patchMP3Duration", func() {
|
|
var pipedMP3 []byte
|
|
|
|
// The fixture is an ffmpeg pipe's output in every way that matters here: ID3 tag,
|
|
// frames, no Xing.
|
|
BeforeEach(func() {
|
|
var err error
|
|
pipedMP3, err = os.ReadFile("tests/fixtures/test.mp3")
|
|
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 {
|
|
out, err := io.ReadAll(patchPipedHeader(io.NopCloser(bytes.NewReader(in)), duration))
|
|
Expect(err).ToNot(HaveOccurred())
|
|
return out
|
|
}
|
|
|
|
// Reads the tag and frame count of the frame starting at frameStart.
|
|
readXing := func(b []byte, frameStart, tagOffset int) (string, uint32) {
|
|
at := frameStart + tagOffset
|
|
return string(b[at : at+4]), binary.BigEndian.Uint32(b[at+8:])
|
|
}
|
|
|
|
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("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")
|
|
})
|
|
|
|
It("leaves the ID3 tag and the audio frames untouched", func() {
|
|
out := readAll(pipedMP3, 1.0)
|
|
|
|
start := fixtureID3Len
|
|
Expect(out).To(HaveLen(len(pipedMP3) + fixtureFrameLen))
|
|
Expect(out[:start]).To(Equal(pipedMP3[:start]))
|
|
Expect(out[start+fixtureFrameLen:]).To(Equal(pipedMP3[start:]))
|
|
})
|
|
|
|
It("takes the sample rate from the header, not from the source file", func() {
|
|
in := bytes.Clone(pipedMP3)
|
|
start := fixtureID3Len
|
|
in[start+2] = in[start+2]&^0x0C | 0x04 // sample rate index 1: 48000Hz
|
|
|
|
out := readAll(in, 1.0)
|
|
|
|
_, frames := readXing(out, start, stereoTagOffset)
|
|
Expect(frames).To(Equal(uint32(42))) // 48000/1152, rounded
|
|
})
|
|
|
|
It("rounds the frame count to the nearest frame", func() {
|
|
out := readAll(pipedMP3, 0.99) // 37.9 frames
|
|
_, frames := readXing(out, fixtureID3Len, stereoTagOffset)
|
|
Expect(frames).To(Equal(uint32(38)))
|
|
})
|
|
|
|
It("places the tag after the shorter side info of a mono stream", func() {
|
|
in := monoMP3()
|
|
|
|
out := readAll(in, 1.0)
|
|
|
|
tag, frames := readXing(out, 0, monoTagOffset)
|
|
Expect(tag).To(Equal("Info"))
|
|
Expect(frames).To(Equal(uint32(38)))
|
|
})
|
|
|
|
It("inserts the frame at the start of a stream with no ID3 tag", func() {
|
|
in := pipedMP3[fixtureID3Len:]
|
|
|
|
out := readAll(in, 1.0)
|
|
|
|
tag, _ := readXing(out, 0, stereoTagOffset)
|
|
Expect(tag).To(Equal("Info"))
|
|
Expect(out[fixtureFrameLen:]).To(Equal(in))
|
|
})
|
|
|
|
It("leaves a stream that already declares its duration alone", func() {
|
|
patched := readAll(pipedMP3, 1.0)
|
|
Expect(readAll(patched, 99.0)).To(Equal(patched))
|
|
})
|
|
|
|
It("passes through when the duration is zero or negative", func() {
|
|
Expect(readAll(pipedMP3, 0)).To(Equal(pipedMP3))
|
|
Expect(readAll(pipedMP3, -5)).To(Equal(pipedMP3))
|
|
})
|
|
|
|
It("passes through a stream that is not mp3", func() {
|
|
in := []byte("fLaC\x00\x00\x00\x22 and then some bytes that are not frames")
|
|
Expect(readAll(in, 1.0)).To(Equal(in))
|
|
})
|
|
|
|
It("passes through a frame header with a reserved bitrate or sample rate", func() {
|
|
in := monoMP3()
|
|
in[2] |= 0xF0 // bitrate index 15 is invalid
|
|
Expect(readAll(in, 1.0)).To(Equal(in))
|
|
|
|
in = monoMP3()
|
|
in[2] |= 0x0C // sample rate index 3 is reserved
|
|
Expect(readAll(in, 1.0)).To(Equal(in))
|
|
})
|
|
|
|
It("passes through a stream too short to hold a frame header", func() {
|
|
in := monoMP3()[:3]
|
|
Expect(readAll(in, 1.0)).To(Equal(in))
|
|
})
|
|
|
|
It("passes through a stream whose ID3 tag never ends", func() {
|
|
in := bytes.Clone(pipedMP3[:200])
|
|
Expect(readAll(in, 1.0)).To(Equal(in))
|
|
})
|
|
|
|
It("passes through an empty stream", func() {
|
|
Expect(readAll(nil, 1.0)).To(BeEmpty())
|
|
})
|
|
|
|
It("propagates a read error from the underlying stream", func() {
|
|
r := patchPipedHeader(io.NopCloser(iotest.ErrReader(errors.New("ffmpeg died"))), 1.0)
|
|
_, err := io.ReadAll(r)
|
|
Expect(err).To(MatchError(ContainSubstring("ffmpeg died")))
|
|
})
|
|
})
|
|
|
|
// monoMP3 is one silent MPEG1 Layer III frame, 128kbps, 44100Hz, mono.
|
|
func monoMP3() []byte {
|
|
b := make([]byte, 417)
|
|
copy(b, []byte{0xFF, 0xFB, 0x90, 0xC0})
|
|
return b
|
|
}
|