mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
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:
parent
c69ed2a913
commit
1ce16d8315
@ -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
|
||||
|
||||
@ -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))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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())
|
||||
})
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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())
|
||||
})
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
85
server/subsonic/stream_internal_test.go
Normal file
85
server/subsonic/stream_internal_test.go
Normal 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())
|
||||
})
|
||||
})
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user