fix: use ADTS for AAC transcoding, temporarily exclude AAC from transcode decisions (#5167)

* fix: use ADTS format for AAC transcoding to avoid silent output on ffmpeg 8.0+

The fragmented MP4 muxer (`-f ipod -movflags frag_keyframe+empty_moov`)
produces corrupt/silent audio when ffmpeg pipes to stdout, confirmed on
ffmpeg 8.0+. The moof atom offset values are zeroed out in pipe mode,
causing AAC decoder errors. Switch to `-f adts` (raw AAC framing) which
works reliably via pipe and is widely supported by clients including
UPnP/Sonos devices.

* fix: exclude AAC from transcode decision, as it is not working for Sonos.

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão
2026-03-11 09:26:32 -04:00
committed by GitHub
parent 51c48bcacd
commit d8bc41fbb1
8 changed files with 111 additions and 17 deletions
+10
View File
@@ -268,6 +268,16 @@ func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request)
}
clientInfo := clientInfoReq.toCoreClientInfo()
// TODO: Remove this filter once AAC transcoding works reliably
// with streaming clients (Sonos, etc).
// See https://github.com/navidrome/navidrome/discussions/4832#discussioncomment-16068231
clientInfo.TranscodingProfiles = slices.DeleteFunc(clientInfo.TranscodingProfiles, func(p stream.Profile) bool {
if p.AudioCodec != "" {
return stream.IsAACCodec(p.AudioCodec)
}
return stream.IsAACCodec(p.Container)
})
// Get media file
mf, err := api.ds.MediaFile(ctx).Get(mediaID)
if err != nil {
+31 -6
View File
@@ -180,6 +180,29 @@ var _ = Describe("Transcode endpoints", func() {
Expect(resp.TranscodeDecision.SourceStream.AudioBitrate).To(Equal(int32(320_000)))
})
It("filters AAC from transcoding profiles", func() {
mockMFRepo.SetData(model.MediaFiles{
{ID: "song-1", Suffix: "opus", Codec: "opus", BitRate: 128, Channels: 2, SampleRate: 48000},
})
mockTD.decision = &stream.TranscodeDecision{MediaID: "song-1", CanDirectPlay: true}
mockTD.token = "token"
body := `{
"transcodingProfiles": [
{"container": "aac", "audioCodec": "aac", "protocol": "http"},
{"container": "mp3", "audioCodec": "mp3", "protocol": "http"},
{"container": "m4a", "audioCodec": "aac", "protocol": "http"}
]
}`
r := newJSONPostRequest("mediaId=song-1&mediaType=song", body)
_, err := router.GetTranscodeDecision(w, r)
Expect(err).ToNot(HaveOccurred())
Expect(mockTD.capturedClient).ToNot(BeNil())
Expect(mockTD.capturedClient.TranscodingProfiles).To(HaveLen(1))
Expect(mockTD.capturedClient.TranscodingProfiles[0].AudioCodec).To(Equal("mp3"))
})
It("includes transcode stream when transcoding", func() {
mockMFRepo.SetData(model.MediaFiles{
{ID: "song-2", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24},
@@ -354,14 +377,16 @@ func newJSONPostRequest(queryParams string, jsonBody string) *http.Request {
// mockTranscodeDecision is a test double for stream.TranscodeDecider
type mockTranscodeDecision struct {
decision *stream.TranscodeDecision
token string
tokenErr error
resolvedReq stream.Request
resolveErr error
decision *stream.TranscodeDecision
token string
tokenErr error
resolvedReq stream.Request
resolveErr error
capturedClient *stream.ClientInfo
}
func (m *mockTranscodeDecision) MakeDecision(_ context.Context, _ *model.MediaFile, _ *stream.ClientInfo, _ stream.TranscodeOptions) (*stream.TranscodeDecision, error) {
func (m *mockTranscodeDecision) MakeDecision(_ context.Context, _ *model.MediaFile, ci *stream.ClientInfo, _ stream.TranscodeOptions) (*stream.TranscodeDecision, error) {
m.capturedClient = ci
if m.decision != nil {
return m.decision, nil
}