mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 02:17:25 +02:00
fix(stream): honor explicit lossless transcoding of lossy sources - #6247
Signed-off-by: Michał Furgała <83299832+00200200@users.noreply.github.com>
This commit is contained in:
parent
3a31f702b5
commit
eccbb39f29
3 changed files with 63 additions and 4 deletions
|
|
@ -259,8 +259,12 @@ func (s *deciderService) computeTranscodedStream(ctx context.Context, src *Detai
|
|||
|
||||
targetIsLossless := isLosslessFormat(targetFormat)
|
||||
|
||||
// Reject lossy to lossless conversion
|
||||
if !src.IsLossless && targetIsLossless {
|
||||
// Skip lossy→lossless when the client can transcode to a lossy format
|
||||
// instead. An explicit lossless-only request (Subsonic format=flac, a
|
||||
// player-forced FLAC transcoder, or a client that only accepts lossless)
|
||||
// has no such alternative and must be honored — AV receivers often need
|
||||
// that path. See #6247.
|
||||
if !src.IsLossless && targetIsLossless && hasLossyTranscodingAlternative(clientInfo) {
|
||||
log.Trace(ctx, "Skipping transcoding profile: lossy to lossless not allowed", "targetFormat", targetFormat)
|
||||
return nil, ""
|
||||
}
|
||||
|
|
@ -308,6 +312,21 @@ func (s *deciderService) computeTranscodedStream(ctx context.Context, src *Detai
|
|||
return ts, targetFormat
|
||||
}
|
||||
|
||||
// hasLossyTranscodingAlternative reports whether clientInfo lists any
|
||||
// transcoding profile whose target is lossy.
|
||||
func hasLossyTranscodingAlternative(clientInfo *ClientInfo) bool {
|
||||
if clientInfo == nil {
|
||||
return false
|
||||
}
|
||||
for i := range clientInfo.TranscodingProfiles {
|
||||
_, format := resolveTargetFormat(&clientInfo.TranscodingProfiles[i])
|
||||
if format != "" && !isLosslessFormat(format) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// lookupDefaultBitrate returns the default bitrate for the given format.
|
||||
// It checks the DB first (for user-customized values), then falls back to
|
||||
// the built-in defaults, and finally to fallbackBitrate.
|
||||
|
|
|
|||
|
|
@ -266,8 +266,25 @@ var _ = Describe("Decider", func() {
|
|||
)))
|
||||
})
|
||||
|
||||
It("rejects lossy to lossless transcoding", func() {
|
||||
It("skips lossy to lossless when a lossy transcoding profile is also available", func() {
|
||||
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2})
|
||||
ci := &ClientInfo{
|
||||
TranscodingProfiles: []Profile{
|
||||
{Container: "flac", Protocol: ProtocolHTTP},
|
||||
{Container: "opus", AudioCodec: "opus", Protocol: ProtocolHTTP},
|
||||
},
|
||||
}
|
||||
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(decision.CanTranscode).To(BeTrue())
|
||||
Expect(decision.TargetFormat).To(Equal("opus"))
|
||||
})
|
||||
|
||||
It("allows lossy to lossless when that is the only requested format", func() {
|
||||
// Subsonic format=flac and a player-forced FLAC transcoder both
|
||||
// produce a lossless-only profile list. AV receivers that cannot
|
||||
// play the source (e.g. ogg) need this path. See #6247.
|
||||
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "ogg", Codec: "Vorbis", BitRate: 192, Channels: 2, SampleRate: 44100})
|
||||
ci := &ClientInfo{
|
||||
TranscodingProfiles: []Profile{
|
||||
{Container: "flac", Protocol: ProtocolHTTP},
|
||||
|
|
@ -275,7 +292,8 @@ var _ = Describe("Decider", func() {
|
|||
}
|
||||
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(decision.CanTranscode).To(BeFalse())
|
||||
Expect(decision.CanTranscode).To(BeTrue())
|
||||
Expect(decision.TargetFormat).To(Equal("flac"))
|
||||
})
|
||||
|
||||
It("uses default bitrate when client doesn't specify", func() {
|
||||
|
|
|
|||
|
|
@ -191,6 +191,18 @@ var _ = Describe("ResolveRequest", func() {
|
|||
Expect(req.Format).To(Equal("opus"))
|
||||
})
|
||||
|
||||
It("transcodes a lossy source to an explicitly requested lossless format", func() {
|
||||
// stream?id=…&format=flac on an ogg file. Before the fix this fell
|
||||
// through to raw / DefaultDownsamplingFormat because lossy→lossless
|
||||
// was unconditionally rejected. See #6247.
|
||||
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "ogg", Codec: "Vorbis", BitRate: 192, Channels: 2, SampleRate: 44100})
|
||||
|
||||
decider := svc.(*deciderService)
|
||||
req := decider.ResolveRequest(ctx, mf, "flac", 0, 0)
|
||||
|
||||
Expect(req.Format).To(Equal("flac"))
|
||||
})
|
||||
|
||||
It("transcodes to requested format with bitrate limit", func() {
|
||||
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)})
|
||||
|
||||
|
|
@ -234,6 +246,16 @@ var _ = Describe("ResolveRequest", func() {
|
|||
})
|
||||
|
||||
Context("Server-side player transcoding override", func() {
|
||||
It("forces a lossless transcoder even when the source is lossy", func() {
|
||||
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "ogg", Codec: "Vorbis", BitRate: 192, Channels: 2, SampleRate: 44100})
|
||||
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "flac", DefaultBitRate: 0})
|
||||
|
||||
decider := svc.(*deciderService)
|
||||
req := decider.ResolveRequest(overrideCtx, mf, "", 0, 0)
|
||||
|
||||
Expect(req.Format).To(Equal("flac"))
|
||||
})
|
||||
|
||||
It("forces transcoding when override targets a different format", func() {
|
||||
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100})
|
||||
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 192})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue