refactor(transcode): unify streaming decision engine via MakeDecision

Move transcoding decision-making out of mediaStreamer and into the
subsonic Stream/Download handlers, using transcode.Decider.MakeDecision
as the single decision engine. This eliminates selectTranscodingOptions
and the mismatch between decision and streaming code paths (decision
used LookupTranscodeCommand with built-in fallbacks, while streaming
used FindByFormat which only checked the DB).

- Add DecisionOptions with SkipProbe to MakeDecision so the legacy
  streaming path never calls ffprobe
- Add buildLegacyClientInfo to translate legacy stream params (format,
  maxBitRate, DefaultDownsamplingFormat) into a synthetic ClientInfo
- Add resolveStreamRequest on the subsonic Router to resolve legacy
  params into a fully specified StreamRequest via MakeDecision
- Simplify DoStream to a dumb executor that receives pre-resolved params
- Remove selectTranscodingOptions entirely

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2026-03-08 19:09:39 -04:00
parent c69ed2a913
commit 1ce16d8315
11 changed files with 250 additions and 345 deletions

View File

@ -83,7 +83,12 @@ func (ms *mediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, req
"originalFormat", mf.Suffix, "originalBitRate", mf.BitRate)
}()
format, bitRate = selectTranscodingOptions(ctx, ms.ds, mf, req.Format, req.BitRate, req.SampleRate, req.BitDepth, req.Channels)
format = req.Format
bitRate = req.BitRate
if format == "" || format == "raw" {
format = "raw"
bitRate = 0
}
s := &Stream{ctx: ctx, mf: mf, format: format, bitRate: bitRate}
filePath := mf.AbsolutePath()
@ -149,61 +154,6 @@ func (s *Stream) EstimatedContentLength() int {
return int(s.mf.Duration * float32(s.bitRate) / 8 * 1024)
}
// TODO This function deserves some love (refactoring)
func selectTranscodingOptions(ctx context.Context, ds model.DataStore, mf *model.MediaFile, reqFormat string, reqBitRate int, reqSampleRate int, reqBitDepth int, reqChannels int) (format string, bitRate int) {
format = "raw"
if reqFormat == "raw" {
return format, 0
}
needsTranscode := (reqSampleRate > 0 && reqSampleRate < mf.SampleRate) ||
(reqBitDepth > 0 && reqBitDepth < mf.BitDepth) ||
(reqChannels > 0 && reqChannels < mf.Channels)
if reqFormat == mf.Suffix && reqBitRate == 0 && !needsTranscode {
bitRate = mf.BitRate
return format, bitRate
}
trc, hasDefault := request.TranscodingFrom(ctx)
var cFormat string
var cBitRate int
if reqFormat != "" {
cFormat = reqFormat
} else {
if hasDefault {
cFormat = trc.TargetFormat
cBitRate = trc.DefaultBitRate
if p, ok := request.PlayerFrom(ctx); ok {
cBitRate = p.MaxBitRate
}
} else if reqBitRate > 0 && reqBitRate < mf.BitRate && conf.Server.DefaultDownsamplingFormat != "" {
// If no format is specified and no transcoding associated to the player, but a bitrate is specified,
// and there is no transcoding set for the player, we use the default downsampling format.
// But only if the requested bitRate is lower than the original bitRate.
log.Debug("Default Downsampling", "Using default downsampling format", conf.Server.DefaultDownsamplingFormat)
cFormat = conf.Server.DefaultDownsamplingFormat
}
}
if reqBitRate > 0 {
cBitRate = reqBitRate
}
if cBitRate == 0 && cFormat == "" {
return format, bitRate
}
t, err := ds.Transcoding(ctx).FindByFormat(cFormat)
if err == nil {
format = t.TargetFormat
if cBitRate != 0 {
bitRate = cBitRate
} else {
bitRate = t.DefaultBitRate
}
}
if format == mf.Suffix && bitRate >= mf.BitRate && !needsTranscode {
format = "raw"
bitRate = 0
}
return format, bitRate
}
var (
onceTranscodingCache sync.Once
instanceTranscodingCache TranscodingCache

View File

@ -1,214 +0,0 @@
package core
import (
"context"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("MediaStreamer", func() {
var ds model.DataStore
ctx := log.NewContext(context.Background())
BeforeEach(func() {
ds = &tests.MockDataStore{MockedTranscoding: &tests.MockTranscodingRepo{}}
})
Context("selectTranscodingOptions", func() {
mf := &model.MediaFile{}
Context("player is not configured", func() {
It("returns raw if raw is requested", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, _ := selectTranscodingOptions(ctx, ds, mf, "raw", 0, 0, 0, 0)
Expect(format).To(Equal("raw"))
})
It("returns raw if a transcoder does not exists", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, _ := selectTranscodingOptions(ctx, ds, mf, "m4a", 0, 0, 0, 0)
Expect(format).To(Equal("raw"))
})
It("returns the requested format if a transcoder exists", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "mp3", 0, 0, 0, 0)
Expect(format).To(Equal("mp3"))
Expect(bitRate).To(Equal(160)) // Default Bit Rate
})
It("returns raw if requested format is the same as the original and it is not necessary to downsample", func() {
mf.Suffix = "mp3"
mf.BitRate = 112
format, _ := selectTranscodingOptions(ctx, ds, mf, "mp3", 128, 0, 0, 0)
Expect(format).To(Equal("raw"))
})
It("returns the requested format if requested BitRate is lower than original", func() {
mf.Suffix = "mp3"
mf.BitRate = 320
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "mp3", 192, 0, 0, 0)
Expect(format).To(Equal("mp3"))
Expect(bitRate).To(Equal(192))
})
It("returns raw if requested format is the same as the original, but requested BitRate is 0", func() {
mf.Suffix = "mp3"
mf.BitRate = 320
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "mp3", 0, 0, 0, 0)
Expect(format).To(Equal("raw"))
Expect(bitRate).To(Equal(320))
})
It("returns the format when same format is requested but with a lower sample rate", func() {
mf.Suffix = "flac"
mf.BitRate = 2118
mf.SampleRate = 96000
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "flac", 0, 48000, 0, 0)
Expect(format).To(Equal("flac"))
Expect(bitRate).To(Equal(0))
})
It("returns raw when same format is requested with same sample rate", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
mf.SampleRate = 48000
format, _ := selectTranscodingOptions(ctx, ds, mf, "flac", 0, 48000, 0, 0)
Expect(format).To(Equal("raw"))
})
It("returns raw when same format is requested with no sample rate constraint", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
mf.SampleRate = 96000
format, _ := selectTranscodingOptions(ctx, ds, mf, "flac", 0, 0, 0, 0)
Expect(format).To(Equal("raw"))
})
It("returns the format when same format is requested but with lower bit depth", func() {
mf.Suffix = "flac"
mf.BitRate = 2118
mf.BitDepth = 24
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "flac", 0, 0, 16, 0)
Expect(format).To(Equal("flac"))
Expect(bitRate).To(Equal(0))
})
It("returns raw when same format is requested with same bit depth", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
mf.BitDepth = 16
format, _ := selectTranscodingOptions(ctx, ds, mf, "flac", 0, 0, 16, 0)
Expect(format).To(Equal("raw"))
})
It("returns the format when same format is requested but with fewer channels", func() {
mf.Suffix = "flac"
mf.BitRate = 2118
mf.Channels = 6
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "flac", 0, 0, 0, 2)
Expect(format).To(Equal("flac"))
Expect(bitRate).To(Equal(0))
})
It("returns raw when same format is requested with same channels", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
mf.Channels = 2
format, _ := selectTranscodingOptions(ctx, ds, mf, "flac", 0, 0, 0, 2)
Expect(format).To(Equal("raw"))
})
Context("Downsampling", func() {
BeforeEach(func() {
conf.Server.DefaultDownsamplingFormat = "opus"
mf.Suffix = "FLAC"
mf.BitRate = 960
})
It("returns the DefaultDownsamplingFormat if a maxBitrate is requested but not the format", func() {
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 128, 0, 0, 0)
Expect(format).To(Equal("opus"))
Expect(bitRate).To(Equal(128))
})
It("returns raw if maxBitrate is equal or greater than original", func() {
// This happens with DSub (and maybe other clients?). See https://github.com/navidrome/navidrome/issues/2066
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 960, 0, 0, 0)
Expect(format).To(Equal("raw"))
Expect(bitRate).To(Equal(0))
})
})
})
Context("player has format configured", func() {
BeforeEach(func() {
t := model.Transcoding{ID: "oga1", TargetFormat: "oga", DefaultBitRate: 96}
ctx = request.WithTranscoding(ctx, t)
})
It("returns raw if raw is requested", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, _ := selectTranscodingOptions(ctx, ds, mf, "raw", 0, 0, 0, 0)
Expect(format).To(Equal("raw"))
})
It("returns configured format/bitrate as default", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 0, 0, 0, 0)
Expect(format).To(Equal("oga"))
Expect(bitRate).To(Equal(96))
})
It("returns requested format", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "mp3", 0, 0, 0, 0)
Expect(format).To(Equal("mp3"))
Expect(bitRate).To(Equal(160)) // Default Bit Rate
})
It("returns requested bitrate", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 80, 0, 0, 0)
Expect(format).To(Equal("oga"))
Expect(bitRate).To(Equal(80))
})
It("returns raw if selected bitrate and format is the same as original", func() {
mf.Suffix = "mp3"
mf.BitRate = 192
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "mp3", 192, 0, 0, 0)
Expect(format).To(Equal("raw"))
Expect(bitRate).To(Equal(0))
})
})
Context("player has maxBitRate configured", func() {
BeforeEach(func() {
t := model.Transcoding{ID: "oga1", TargetFormat: "oga", DefaultBitRate: 96}
p := model.Player{ID: "player1", TranscodingId: t.ID, MaxBitRate: 192}
ctx = request.WithTranscoding(ctx, t)
ctx = request.WithPlayer(ctx, p)
})
It("returns raw if raw is requested", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, _ := selectTranscodingOptions(ctx, ds, mf, "raw", 0, 0, 0, 0)
Expect(format).To(Equal("raw"))
})
It("returns configured format/bitrate as default", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 0, 0, 0, 0)
Expect(format).To(Equal("oga"))
Expect(bitRate).To(Equal(192))
})
It("returns requested format", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "mp3", 0, 0, 0, 0)
Expect(format).To(Equal("mp3"))
Expect(bitRate).To(Equal(160)) // Default Bit Rate
})
It("returns requested bitrate", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 160, 0, 0, 0)
Expect(format).To(Equal("oga"))
Expect(bitRate).To(Equal(160))
})
})
})
})

View File

@ -43,13 +43,8 @@ var _ = Describe("MediaStreamer", func() {
Expect(err).ToNot(HaveOccurred())
Expect(s.Seekable()).To(BeTrue())
})
It("returns a seekable stream if maxBitRate is 0", func() {
s, err := streamer.NewStream(ctx, core.StreamRequest{ID: "123", Format: "mp3"})
Expect(err).ToNot(HaveOccurred())
Expect(s.Seekable()).To(BeTrue())
})
It("returns a seekable stream if maxBitRate is higher than file bitRate", func() {
s, err := streamer.NewStream(ctx, core.StreamRequest{ID: "123", Format: "mp3", BitRate: 320})
It("returns a seekable stream if no format is specified (direct play)", func() {
s, err := streamer.NewStream(ctx, core.StreamRequest{ID: "123"})
Expect(err).ToNot(HaveOccurred())
Expect(s.Seekable()).To(BeTrue())
})

View File

@ -35,15 +35,19 @@ type deciderService struct {
ff ffmpeg.FFmpeg
}
func (s *deciderService) MakeDecision(ctx context.Context, mf *model.MediaFile, clientInfo *ClientInfo) (*Decision, error) {
func (s *deciderService) MakeDecision(ctx context.Context, mf *model.MediaFile, clientInfo *ClientInfo, opts DecisionOptions) (*Decision, error) {
decision := &Decision{
MediaID: mf.ID,
SourceUpdatedAt: mf.UpdatedAt,
}
probe, err := s.ensureProbed(ctx, mf)
if err != nil {
return nil, err
var probe *ffmpeg.AudioProbeResult
if !opts.SkipProbe {
var err error
probe, err = s.ensureProbed(ctx, mf)
if err != nil {
return nil, err
}
}
// Build source stream details (uses probe data if available)

View File

@ -61,7 +61,7 @@ var _ = Describe("Decider", func() {
{Containers: []string{"mp3"}, AudioCodecs: []string{"mp3"}, Protocols: []string{"http"}, MaxAudioChannels: 2},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeTrue())
Expect(decision.CanTranscode).To(BeFalse())
@ -75,7 +75,7 @@ var _ = Describe("Decider", func() {
{Containers: []string{"mp3"}, Protocols: []string{"http"}},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeFalse())
Expect(decision.TranscodeReasons).To(ContainElement("container not supported"))
@ -88,7 +88,7 @@ var _ = Describe("Decider", func() {
{Containers: []string{"m4a"}, AudioCodecs: []string{"aac"}, Protocols: []string{"http"}},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeFalse())
Expect(decision.TranscodeReasons).To(ContainElement("audio codec not supported"))
@ -101,7 +101,7 @@ var _ = Describe("Decider", func() {
{Containers: []string{"flac"}, Protocols: []string{"http"}, MaxAudioChannels: 2},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeFalse())
Expect(decision.TranscodeReasons).To(ContainElement("audio channels not supported"))
@ -114,7 +114,7 @@ var _ = Describe("Decider", func() {
{Containers: []string{"aac"}, AudioCodecs: []string{"aac"}, Protocols: []string{"http"}},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeTrue())
})
@ -126,7 +126,7 @@ var _ = Describe("Decider", func() {
{Containers: []string{"mp4"}, AudioCodecs: []string{"aac"}, Protocols: []string{"http"}},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeTrue())
})
@ -138,7 +138,7 @@ var _ = Describe("Decider", func() {
{Containers: []string{"m4a"}, AudioCodecs: []string{"adts"}, Protocols: []string{"http"}},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeTrue())
})
@ -150,7 +150,7 @@ var _ = Describe("Decider", func() {
{Containers: []string{"flac"}, AudioCodecs: []string{"flac"}},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeTrue())
})
@ -162,7 +162,7 @@ var _ = Describe("Decider", func() {
{Containers: []string{}, AudioCodecs: []string{}},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeTrue())
})
@ -180,7 +180,7 @@ var _ = Describe("Decider", func() {
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeFalse())
Expect(decision.CanTranscode).To(BeTrue())
@ -200,7 +200,7 @@ var _ = Describe("Decider", func() {
{Container: "mp3", AudioCodec: "mp3", Protocol: "http", MaxAudioChannels: 2},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeFalse())
Expect(decision.CanTranscode).To(BeTrue())
@ -216,7 +216,7 @@ var _ = Describe("Decider", func() {
{Container: "flac", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeFalse())
})
@ -228,7 +228,7 @@ var _ = Describe("Decider", func() {
{Container: "mp3", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TargetBitrate).To(Equal(defaultBitrate)) // 256 kbps
@ -242,7 +242,7 @@ var _ = Describe("Decider", func() {
{Container: "mp3", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TargetBitrate).To(Equal(192)) // source bitrate in kbps
@ -255,7 +255,7 @@ var _ = Describe("Decider", func() {
{Container: "wav", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeFalse())
})
@ -268,7 +268,7 @@ var _ = Describe("Decider", func() {
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TargetBitrate).To(Equal(96)) // capped by maxAudioBitrate
@ -286,7 +286,7 @@ var _ = Describe("Decider", func() {
{Container: "mp3", AudioCodec: "mp3", Protocol: "http", MaxAudioChannels: 2},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TargetFormat).To(Equal("opus"))
@ -305,7 +305,7 @@ var _ = Describe("Decider", func() {
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TargetFormat).To(Equal("mp3"))
@ -320,7 +320,7 @@ var _ = Describe("Decider", func() {
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TranscodeStream.IsLossless).To(BeFalse()) // mp3 is lossy
@ -331,7 +331,7 @@ var _ = Describe("Decider", func() {
It("returns error when nothing matches", func() {
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6})
ci := &ClientInfo{}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeFalse())
Expect(decision.CanTranscode).To(BeFalse())
@ -356,7 +356,7 @@ var _ = Describe("Decider", func() {
},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeFalse())
Expect(decision.TranscodeReasons).To(ContainElement("audio bitrate not supported"))
@ -378,7 +378,7 @@ var _ = Describe("Decider", func() {
},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeTrue())
})
@ -399,7 +399,7 @@ var _ = Describe("Decider", func() {
},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeTrue())
})
@ -420,7 +420,7 @@ var _ = Describe("Decider", func() {
},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeFalse())
})
@ -442,7 +442,7 @@ var _ = Describe("Decider", func() {
},
}
// Source profile is empty (not yet populated from scanner), so Equals("LC") fails
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeFalse())
Expect(decision.TranscodeReasons).To(ContainElement("audio profile not supported"))
@ -464,7 +464,7 @@ var _ = Describe("Decider", func() {
},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeTrue())
})
@ -485,7 +485,7 @@ var _ = Describe("Decider", func() {
},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeFalse())
Expect(decision.TranscodeReasons).To(ContainElement("audio samplerate not supported"))
@ -510,7 +510,7 @@ var _ = Describe("Decider", func() {
},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TranscodeStream.Bitrate).To(Equal(96))
@ -533,7 +533,7 @@ var _ = Describe("Decider", func() {
},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TranscodeStream.Channels).To(Equal(2))
@ -556,7 +556,7 @@ var _ = Describe("Decider", func() {
},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TranscodeStream.SampleRate).To(Equal(48000))
@ -578,7 +578,7 @@ var _ = Describe("Decider", func() {
},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TranscodeStream.BitDepth).To(Equal(16))
@ -592,7 +592,7 @@ var _ = Describe("Decider", func() {
{Container: "flac", AudioCodec: "flac", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TranscodeStream.BitDepth).To(Equal(24))
@ -616,7 +616,7 @@ var _ = Describe("Decider", func() {
},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeFalse())
})
@ -631,7 +631,7 @@ var _ = Describe("Decider", func() {
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TargetFormat).To(Equal("mp3"))
@ -650,7 +650,7 @@ var _ = Describe("Decider", func() {
{Container: "flac", AudioCodec: "flac", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TargetFormat).To(Equal("flac"))
@ -678,7 +678,7 @@ var _ = Describe("Decider", func() {
},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
// DSD64 2822400 / 8 = 352800, capped by codec profile limit of 48000
@ -705,7 +705,7 @@ var _ = Describe("Decider", func() {
},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
// DSD 1-bit → 24-bit PCM, then capped by codec profile limit to 16-bit
@ -730,7 +730,7 @@ var _ = Describe("Decider", func() {
},
MaxTranscodingAudioBitrate: 256,
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.SourceStream.IsLossless).To(BeTrue())
Expect(decision.SourceStream.Codec).To(Equal("wavpack"))
@ -752,7 +752,7 @@ var _ = Describe("Decider", func() {
{Containers: []string{"ogg"}, AudioCodecs: []string{"vorbis"}, Protocols: []string{"http"}},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.SourceStream.IsLossless).To(BeFalse())
Expect(decision.CanDirectPlay).To(BeTrue())
@ -768,7 +768,7 @@ var _ = Describe("Decider", func() {
{Container: "opus", AudioCodec: "opus", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TargetFormat).To(Equal("opus"))
@ -785,7 +785,7 @@ var _ = Describe("Decider", func() {
{Container: "opus", AudioCodec: "opus", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TranscodeStream.SampleRate).To(Equal(48000))
@ -801,7 +801,7 @@ var _ = Describe("Decider", func() {
{Container: "mp4", AudioCodec: "aac", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
// TargetFormat is the internal format used for transcoding ("aac")
@ -819,7 +819,7 @@ var _ = Describe("Decider", func() {
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TargetFormat).To(Equal("mp3"))
@ -836,7 +836,7 @@ var _ = Describe("Decider", func() {
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TranscodeStream.SampleRate).To(Equal(48000))
@ -850,7 +850,7 @@ var _ = Describe("Decider", func() {
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TranscodeStream.SampleRate).To(Equal(44100))
@ -866,7 +866,7 @@ var _ = Describe("Decider", func() {
{Container: "aac", AudioCodec: "aac", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
// DSD64 2822400 / 8 = 352800, capped by AAC max of 96000
@ -887,7 +887,7 @@ var _ = Describe("Decider", func() {
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeFalse())
Expect(decision.TranscodeReasons).To(HaveLen(3))
@ -905,7 +905,7 @@ var _ = Describe("Decider", func() {
{Containers: []string{"flac"}, Protocols: []string{"http"}},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.SourceStream.Container).To(Equal("flac"))
Expect(decision.SourceStream.Codec).To(Equal("flac"))
@ -929,7 +929,7 @@ var _ = Describe("Decider", func() {
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 192})
overrideCtx = request.WithPlayer(overrideCtx, model.Player{MaxBitRate: 0})
decision, err := svc.MakeDecision(overrideCtx, mf, ci)
decision, err := svc.MakeDecision(overrideCtx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeFalse())
Expect(decision.CanTranscode).To(BeTrue())
@ -947,7 +947,7 @@ var _ = Describe("Decider", func() {
}
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 256})
decision, err := svc.MakeDecision(overrideCtx, mf, ci)
decision, err := svc.MakeDecision(overrideCtx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeTrue())
Expect(decision.CanTranscode).To(BeFalse())
@ -960,7 +960,7 @@ var _ = Describe("Decider", func() {
}
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 192})
decision, err := svc.MakeDecision(overrideCtx, mf, ci)
decision, err := svc.MakeDecision(overrideCtx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeFalse())
Expect(decision.CanTranscode).To(BeTrue())
@ -976,7 +976,7 @@ var _ = Describe("Decider", func() {
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 192})
overrideCtx = request.WithPlayer(overrideCtx, model.Player{MaxBitRate: 320})
decision, err := svc.MakeDecision(overrideCtx, mf, ci)
decision, err := svc.MakeDecision(overrideCtx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TargetFormat).To(Equal("mp3"))
@ -991,7 +991,7 @@ var _ = Describe("Decider", func() {
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 0})
overrideCtx = request.WithPlayer(overrideCtx, model.Player{MaxBitRate: 0})
decision, err := svc.MakeDecision(overrideCtx, mf, ci)
decision, err := svc.MakeDecision(overrideCtx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TargetFormat).To(Equal("mp3"))
@ -1008,7 +1008,7 @@ var _ = Describe("Decider", func() {
},
}
// No override in context — client profiles used as-is
decision, err := svc.MakeDecision(ctx, mf, ci)
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeTrue())
})

View File

@ -16,9 +16,16 @@ var (
ErrTokenStale = errors.New("transcode token is stale: media file has changed")
)
// DecisionOptions controls optional behavior of MakeDecision.
type DecisionOptions struct {
// SkipProbe prevents MakeDecision from running ffprobe on the media file.
// When true, source stream details are derived from tag metadata only.
SkipProbe bool
}
// Decider is the core service interface for making transcoding decisions
type Decider interface {
MakeDecision(ctx context.Context, mf *model.MediaFile, clientInfo *ClientInfo) (*Decision, error)
MakeDecision(ctx context.Context, mf *model.MediaFile, clientInfo *ClientInfo, opts DecisionOptions) (*Decision, error)
CreateTranscodeParams(decision *Decision) (string, error)
ParseTranscodeParams(token string) (*Params, error)
ValidateTranscodeParams(ctx context.Context, token string, mediaID string) (*Params, *model.MediaFile, error)

View File

@ -239,7 +239,7 @@ func (n noopStreamer) DoStream(context.Context, *model.MediaFile, core.StreamReq
// noopDecider implements transcode.Decider
type noopDecider struct{}
func (n noopDecider) MakeDecision(context.Context, *model.MediaFile, *transcode.ClientInfo) (*transcode.Decision, error) {
func (n noopDecider) MakeDecision(context.Context, *model.MediaFile, *transcode.ClientInfo, transcode.DecisionOptions) (*transcode.Decision, error) {
return nil, nil
}

View File

@ -10,6 +10,7 @@ import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/transcode"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
@ -60,9 +61,13 @@ func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Su
format, _ := p.String("format")
timeOffset := p.IntOr("timeOffset", 0)
stream, err := api.streamer.NewStream(ctx, core.StreamRequest{
ID: id, Format: format, BitRate: maxBitRate, Offset: timeOffset,
})
mf, err := api.ds.MediaFile(ctx).Get(id)
if err != nil {
return nil, err
}
streamReq := api.resolveStreamRequest(ctx, mf, format, maxBitRate, timeOffset)
stream, err := api.streamer.DoStream(ctx, mf, streamReq)
if err != nil {
return nil, err
}
@ -131,9 +136,8 @@ func (api *Router) Download(w http.ResponseWriter, r *http.Request) (*responses.
switch v := entity.(type) {
case *model.MediaFile:
stream, err := api.streamer.NewStream(ctx, core.StreamRequest{
ID: id, Format: format, BitRate: maxBitRate,
})
streamReq := api.resolveStreamRequest(ctx, v, format, maxBitRate, 0)
stream, err := api.streamer.DoStream(ctx, v, streamReq)
if err != nil {
return nil, err
}
@ -165,3 +169,77 @@ func (api *Router) Download(w http.ResponseWriter, r *http.Request) (*responses.
return nil, err
}
// buildLegacyClientInfo translates legacy Subsonic stream/download parameters
// into a transcode.ClientInfo for use with MakeDecision.
// It does NOT read request.TranscodingFrom(ctx) — that is handled by
// MakeDecision's applyServerOverride.
func buildLegacyClientInfo(mf *model.MediaFile, reqFormat string, reqBitRate int) *transcode.ClientInfo {
ci := &transcode.ClientInfo{Name: "legacy"}
// Determine target format for transcoding
var targetFormat string
switch {
case reqFormat != "":
targetFormat = reqFormat
case reqBitRate > 0 && reqBitRate < mf.BitRate && conf.Server.DefaultDownsamplingFormat != "":
targetFormat = conf.Server.DefaultDownsamplingFormat
}
if targetFormat != "" {
ci.DirectPlayProfiles = []transcode.DirectPlayProfile{
{Containers: []string{mf.Suffix}, AudioCodecs: []string{mf.AudioCodec()}, Protocols: []string{transcode.ProtocolHTTP}},
}
ci.TranscodingProfiles = []transcode.Profile{
{Container: targetFormat, AudioCodec: targetFormat, Protocol: transcode.ProtocolHTTP},
}
if reqBitRate > 0 {
ci.MaxAudioBitrate = reqBitRate
ci.MaxTranscodingAudioBitrate = reqBitRate
}
} else {
// No transcoding requested — direct play everything
ci.DirectPlayProfiles = []transcode.DirectPlayProfile{
{Protocols: []string{transcode.ProtocolHTTP}},
}
}
return ci
}
// resolveStreamRequest uses MakeDecision to resolve legacy stream parameters
// into a fully specified StreamRequest.
func (api *Router) resolveStreamRequest(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int, offset int) core.StreamRequest {
req := core.StreamRequest{ID: mf.ID, Offset: offset}
if reqFormat == "raw" {
req.Format = "raw"
return req
}
clientInfo := buildLegacyClientInfo(mf, reqFormat, reqBitRate)
decision, err := api.transcodeDecision.MakeDecision(ctx, mf, clientInfo, transcode.DecisionOptions{SkipProbe: true})
if err != nil {
log.Error(ctx, "Error making transcode decision, falling back to raw", "id", mf.ID, err)
req.Format = "raw"
return req
}
if decision.CanDirectPlay {
req.Format = "raw"
return req
}
if decision.CanTranscode {
req.Format = decision.TargetFormat
req.BitRate = decision.TargetBitrate
req.SampleRate = decision.TargetSampleRate
req.BitDepth = decision.TargetBitDepth
req.Channels = decision.TargetChannels
return req
}
// No compatible profile — fallback to raw
req.Format = "raw"
return req
}

View File

@ -0,0 +1,85 @@
package subsonic
import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/core/transcode"
"github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("buildLegacyClientInfo", func() {
var mf *model.MediaFile
BeforeEach(func() {
mf = &model.MediaFile{Suffix: "flac", BitRate: 960}
})
It("sets transcoding profile for explicit format without bitrate", func() {
ci := buildLegacyClientInfo(mf, "mp3", 0)
Expect(ci.Name).To(Equal("legacy"))
Expect(ci.TranscodingProfiles).To(HaveLen(1))
Expect(ci.TranscodingProfiles[0].Container).To(Equal("mp3"))
Expect(ci.TranscodingProfiles[0].AudioCodec).To(Equal("mp3"))
Expect(ci.TranscodingProfiles[0].Protocol).To(Equal(transcode.ProtocolHTTP))
Expect(ci.MaxAudioBitrate).To(BeZero())
Expect(ci.MaxTranscodingAudioBitrate).To(BeZero())
Expect(ci.DirectPlayProfiles).To(HaveLen(1))
Expect(ci.DirectPlayProfiles[0].Containers).To(Equal([]string{"flac"}))
Expect(ci.DirectPlayProfiles[0].AudioCodecs).To(Equal([]string{mf.AudioCodec()}))
Expect(ci.DirectPlayProfiles[0].Protocols).To(Equal([]string{transcode.ProtocolHTTP}))
})
It("sets transcoding profile and bitrate for explicit format with bitrate", func() {
ci := buildLegacyClientInfo(mf, "mp3", 192)
Expect(ci.TranscodingProfiles).To(HaveLen(1))
Expect(ci.TranscodingProfiles[0].Container).To(Equal("mp3"))
Expect(ci.TranscodingProfiles[0].AudioCodec).To(Equal("mp3"))
Expect(ci.MaxAudioBitrate).To(Equal(192))
Expect(ci.MaxTranscodingAudioBitrate).To(Equal(192))
Expect(ci.DirectPlayProfiles).To(HaveLen(1))
Expect(ci.DirectPlayProfiles[0].Containers).To(Equal([]string{"flac"}))
})
It("returns direct play profile when no format and no bitrate", func() {
ci := buildLegacyClientInfo(mf, "", 0)
Expect(ci.DirectPlayProfiles).To(HaveLen(1))
Expect(ci.DirectPlayProfiles[0].Containers).To(BeEmpty())
Expect(ci.DirectPlayProfiles[0].AudioCodecs).To(BeEmpty())
Expect(ci.DirectPlayProfiles[0].Protocols).To(Equal([]string{transcode.ProtocolHTTP}))
Expect(ci.TranscodingProfiles).To(BeEmpty())
Expect(ci.MaxAudioBitrate).To(BeZero())
})
It("uses default downsampling format for bitrate-only downsampling", func() {
DeferCleanup(configtest.SetupConfig())
conf.Server.DefaultDownsamplingFormat = "opus"
ci := buildLegacyClientInfo(mf, "", 128)
Expect(ci.TranscodingProfiles).To(HaveLen(1))
Expect(ci.TranscodingProfiles[0].Container).To(Equal("opus"))
Expect(ci.TranscodingProfiles[0].AudioCodec).To(Equal("opus"))
Expect(ci.TranscodingProfiles[0].Protocol).To(Equal(transcode.ProtocolHTTP))
Expect(ci.MaxAudioBitrate).To(Equal(128))
Expect(ci.MaxTranscodingAudioBitrate).To(Equal(128))
Expect(ci.DirectPlayProfiles).To(HaveLen(1))
Expect(ci.DirectPlayProfiles[0].Containers).To(Equal([]string{"flac"}))
Expect(ci.DirectPlayProfiles[0].AudioCodecs).To(Equal([]string{mf.AudioCodec()}))
})
It("returns direct play when bitrate >= source bitrate", func() {
ci := buildLegacyClientInfo(mf, "", 960)
Expect(ci.DirectPlayProfiles).To(HaveLen(1))
Expect(ci.DirectPlayProfiles[0].Containers).To(BeEmpty())
Expect(ci.DirectPlayProfiles[0].AudioCodecs).To(BeEmpty())
Expect(ci.DirectPlayProfiles[0].Protocols).To(Equal([]string{transcode.ProtocolHTTP}))
Expect(ci.TranscodingProfiles).To(BeEmpty())
Expect(ci.MaxAudioBitrate).To(BeZero())
})
})

View File

@ -280,7 +280,7 @@ func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request)
}
// Make the decision
decision, err := api.transcodeDecision.MakeDecision(ctx, mf, clientInfo)
decision, err := api.transcodeDecision.MakeDecision(ctx, mf, clientInfo, transcode.DecisionOptions{})
if err != nil {
log.Error(ctx, "Failed to make transcode decision", "mediaID", mediaID, err)
return nil, newError(responses.ErrorGeneric, "failed to make transcode decision")

View File

@ -376,7 +376,7 @@ type mockTranscodeDecision struct {
validateErr error
}
func (m *mockTranscodeDecision) MakeDecision(_ context.Context, _ *model.MediaFile, _ *transcode.ClientInfo) (*transcode.Decision, error) {
func (m *mockTranscodeDecision) MakeDecision(_ context.Context, _ *model.MediaFile, _ *transcode.ClientInfo, _ transcode.DecisionOptions) (*transcode.Decision, error) {
if m.decision != nil {
return m.decision, nil
}