mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-14 21:37:09 +02:00
* fix(jellyfin): send SessionInfo on login so JellyBox gets past sign-in
JellyBox parses AuthenticateByName's SessionInfo as a required object and
fails silently when it is missing, leaving the user on the login screen.
Real Jellyfin always sends it (SessionManager.AuthenticateNewSessionInternal,
10.10.7 and master), so the login response now carries a full SessionInfo
built from the user and the MediaBrowser auth header. It includes every field
JellyBox (Id, PlayState) and Finamp (UserId, LastActivityDate, the activity
and control bools, PlayState's CanSeek/IsPaused/IsMuted) require once the
object is present. The session Id is derived from client and device id, so
repeated logins from one install share it.
* fix(jellyfin): ignore IncludeItemTypes names that aren't Jellyfin kinds
JellyBox opens an album with ParentId=<album>&IncludeItemTypes=music. Music
is not a BaseItemKind, and Jellyfin's comma-delimited binder drops values it
cannot parse, so real Jellyfin treats the request as having no type filter and
lists the album's tracks. Navidrome returned an empty list, so every album
opened empty. Entries that aren't BaseItemKind names are now dropped before
type resolution, so an all-unknown list behaves like an absent one. Real kinds
Navidrome doesn't serve, such as Boxset, still return nothing.
* fix(jellyfin): treat universal Container as the direct-play list
On /Audio/{id}/universal, Container lists the "container|codec" entries the
client can direct play, and TranscodingContainer/AudioCodec name the target
when it can't (UniversalAudioController builds DirectPlayProfiles from it).
Navidrome passed the whole list to the decider as one target format, which
matched nothing and fell back to DefaultDownsamplingFormat, so JellyBox got
every MP3 transcoded to Opus. /universal now has its own handler: a source
matching an entry keeps its format (still downsampled under a bitrate cap),
anything else is transcoded to TranscodingContainer, then AudioCodec. The
/stream routes keep treating Container as the target format.
* refactor(jellyfin): let the stream decider resolve universal requests
streamUniversal matched the Container list itself with plain string equality
and then asked the legacy resolver for the source format. That skipped the
decider's container and codec aliases (mp4 vs m4a, ogg vs opus), and a
direct-playable source over the bitrate cap was transcoded to its own format
instead of the client's TranscodingContainer.
The shared part of ResolveRequest (server-side player override, player
MaxBitRate cap, decision to Request mapping) moves to a resolve helper, and a
new ResolveClientRequest exposes it for callers that build their own
ClientInfo. streamUniversal now turns Container into DirectPlayProfiles and
TranscodingContainer/AudioCodec into a transcoding profile, so the decision
uses the same rules as the Subsonic getTranscodeDecision path. streamFile and
the /stream routes share a serveStream helper, NewSessionInfo reads the clock
itself, and duplicate comments and tests are trimmed.
485 lines
18 KiB
Go
485 lines
18 KiB
Go
package stream
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/conf/configtest"
|
|
"github.com/navidrome/navidrome/core/auth"
|
|
"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("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, 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(ProtocolHTTP))
|
|
Expect(ci.MaxAudioBitrate).To(BeZero())
|
|
Expect(ci.MaxTranscodingAudioBitrate).To(BeZero())
|
|
Expect(ci.DirectPlayProfiles).To(BeEmpty())
|
|
})
|
|
|
|
It("does not add direct play profile when explicit format differs from source (no bitrate)", func() {
|
|
ci := buildLegacyClientInfo(mf, "opus", 0, 0)
|
|
|
|
Expect(ci.TranscodingProfiles).To(HaveLen(1))
|
|
Expect(ci.TranscodingProfiles[0].Container).To(Equal("opus"))
|
|
Expect(ci.DirectPlayProfiles).To(BeEmpty())
|
|
})
|
|
|
|
It("adds direct play profile when explicit format matches source format", func() {
|
|
ci := buildLegacyClientInfo(mf, "flac", 0, 0)
|
|
|
|
Expect(ci.TranscodingProfiles).To(HaveLen(1))
|
|
Expect(ci.TranscodingProfiles[0].Container).To(Equal("flac"))
|
|
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("sets transcoding profile and bitrate for explicit format with bitrate", func() {
|
|
ci := buildLegacyClientInfo(mf, "mp3", 192, 0)
|
|
|
|
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(BeEmpty())
|
|
})
|
|
|
|
It("returns direct play profile when no format and no bitrate", func() {
|
|
ci := buildLegacyClientInfo(mf, "", 0, 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{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, 0)
|
|
|
|
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(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, 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{ProtocolHTTP}))
|
|
Expect(ci.TranscodingProfiles).To(BeEmpty())
|
|
Expect(ci.MaxAudioBitrate).To(BeZero())
|
|
})
|
|
|
|
It("uses default downsampling format when player MaxBitRate is below source and no format/bitrate", func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.DefaultDownsamplingFormat = "opus"
|
|
|
|
ci := buildLegacyClientInfo(mf, "", 0, 256)
|
|
|
|
Expect(ci.TranscodingProfiles).To(HaveLen(1))
|
|
Expect(ci.TranscodingProfiles[0].Container).To(Equal("opus"))
|
|
Expect(ci.TranscodingProfiles[0].AudioCodec).To(Equal("opus"))
|
|
Expect(ci.DirectPlayProfiles).To(HaveLen(1))
|
|
Expect(ci.DirectPlayProfiles[0].Containers).To(Equal([]string{"flac"}))
|
|
})
|
|
|
|
It("does not downsample when player MaxBitRate is >= source bitrate", func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.DefaultDownsamplingFormat = "opus"
|
|
|
|
ci := buildLegacyClientInfo(mf, "", 0, 960)
|
|
|
|
Expect(ci.TranscodingProfiles).To(BeEmpty())
|
|
Expect(ci.DirectPlayProfiles).To(HaveLen(1))
|
|
Expect(ci.DirectPlayProfiles[0].Containers).To(BeEmpty())
|
|
})
|
|
|
|
It("does not downsample when DefaultDownsamplingFormat is empty even with player cap", func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.DefaultDownsamplingFormat = ""
|
|
|
|
ci := buildLegacyClientInfo(mf, "", 0, 256)
|
|
|
|
Expect(ci.TranscodingProfiles).To(BeEmpty())
|
|
Expect(ci.DirectPlayProfiles).To(HaveLen(1))
|
|
Expect(ci.DirectPlayProfiles[0].Containers).To(BeEmpty())
|
|
})
|
|
|
|
It("prefers explicit request format over player cap", func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.DefaultDownsamplingFormat = "opus"
|
|
|
|
ci := buildLegacyClientInfo(mf, "mp3", 0, 256)
|
|
|
|
Expect(ci.TranscodingProfiles).To(HaveLen(1))
|
|
Expect(ci.TranscodingProfiles[0].Container).To(Equal("mp3"))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("ResolveRequest", func() {
|
|
var (
|
|
svc TranscodeDecider
|
|
ctx context.Context
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
ctx = GinkgoT().Context()
|
|
ds := &tests.MockDataStore{
|
|
MockedProperty: &tests.MockedPropertyRepo{},
|
|
MockedTranscoding: &tests.MockTranscodingRepo{},
|
|
}
|
|
ff := tests.NewMockFFmpeg("")
|
|
auth.Init(ds)
|
|
svc = NewTranscodeDecider(ds, ff)
|
|
})
|
|
|
|
It("returns raw when format is 'raw'", func() {
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(ctx, mf, "raw", 0, 0)
|
|
|
|
Expect(req.Format).To(Equal("raw"))
|
|
})
|
|
|
|
It("returns raw (direct play) when no format or bitrate specified", func() {
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(ctx, mf, "", 0, 0)
|
|
|
|
Expect(req.Format).To(Equal("raw"))
|
|
})
|
|
|
|
It("transcodes to requested format", func() {
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(ctx, mf, "opus", 0, 0)
|
|
|
|
Expect(req.Format).To(Equal("opus"))
|
|
})
|
|
|
|
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)})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(ctx, mf, "mp3", 128, 0)
|
|
|
|
Expect(req.Format).To(Equal("mp3"))
|
|
Expect(req.BitRate).To(Equal(128))
|
|
})
|
|
|
|
It("returns raw when requested format matches source and no bitrate reduction", func() {
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(ctx, mf, "mp3", 320, 0)
|
|
|
|
Expect(req.Format).To(Equal("raw"))
|
|
})
|
|
|
|
It("downsamples when only bitrate is specified below source", func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.DefaultDownsamplingFormat = "opus"
|
|
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(ctx, mf, "", 128, 0)
|
|
|
|
Expect(req.Format).To(Equal("opus"))
|
|
Expect(req.BitRate).To(Equal(128))
|
|
})
|
|
|
|
It("passes offset through", func() {
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(ctx, mf, "opus", 128, 30)
|
|
|
|
Expect(req.Format).To(Equal("opus"))
|
|
Expect(req.Offset).To(Equal(30))
|
|
})
|
|
|
|
Context("Server-side player transcoding override", func() {
|
|
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})
|
|
overrideCtx = request.WithPlayer(overrideCtx, model.Player{MaxBitRate: 0})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(overrideCtx, mf, "", 0, 0)
|
|
|
|
Expect(req.Format).To(Equal("mp3"))
|
|
Expect(req.BitRate).To(Equal(192))
|
|
})
|
|
|
|
It("allows direct play when source matches forced format and bitrate is within cap", func() {
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 128, Channels: 2, SampleRate: 44100})
|
|
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 256})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(overrideCtx, mf, "", 0, 0)
|
|
|
|
Expect(req.Format).To(Equal("raw"))
|
|
})
|
|
|
|
It("transcodes when source bitrate exceeds the forced cap", func() {
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100})
|
|
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 192})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(overrideCtx, mf, "", 0, 0)
|
|
|
|
Expect(req.Format).To(Equal("mp3"))
|
|
Expect(req.BitRate).To(Equal(192))
|
|
})
|
|
|
|
It("uses player MaxBitRate over transcoding DefaultBitRate", 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})
|
|
overrideCtx = request.WithPlayer(overrideCtx, model.Player{MaxBitRate: 320})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(overrideCtx, mf, "", 0, 0)
|
|
|
|
Expect(req.Format).To(Equal("mp3"))
|
|
Expect(req.BitRate).To(Equal(320))
|
|
})
|
|
|
|
It("applies no bitrate cap when both MaxBitRate and DefaultBitRate are 0", 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: 0})
|
|
overrideCtx = request.WithPlayer(overrideCtx, model.Player{MaxBitRate: 0})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(overrideCtx, mf, "", 0, 0)
|
|
|
|
Expect(req.Format).To(Equal("mp3"))
|
|
// With no cap, lossless→lossy uses format default bitrate (160 for mp3 from mock)
|
|
Expect(req.BitRate).To(Equal(160))
|
|
})
|
|
|
|
It("does not apply override when no transcoding is in context", func() {
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(ctx, mf, "", 0, 0)
|
|
|
|
Expect(req.Format).To(Equal("raw"))
|
|
})
|
|
})
|
|
|
|
Context("Player MaxBitRate cap", func() {
|
|
It("applies player MaxBitRate cap when client has no limit", func() {
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)})
|
|
playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 320})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(playerCtx, mf, "mp3", 0, 0)
|
|
|
|
Expect(req.Format).To(Equal("mp3"))
|
|
Expect(req.BitRate).To(Equal(320))
|
|
})
|
|
|
|
It("uses client limit when it is more restrictive than player MaxBitRate", func() {
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)})
|
|
playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 500})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(playerCtx, mf, "mp3", 256, 0)
|
|
|
|
Expect(req.Format).To(Equal("mp3"))
|
|
Expect(req.BitRate).To(Equal(256))
|
|
})
|
|
|
|
It("does not cap when player MaxBitRate is 0", func() {
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100})
|
|
playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 0})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(playerCtx, mf, "", 0, 0)
|
|
|
|
Expect(req.Format).To(Equal("raw"))
|
|
})
|
|
|
|
It("downsamples using DefaultDownsamplingFormat when only player MaxBitRate is set (no format/bitrate)", func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.DefaultDownsamplingFormat = "opus"
|
|
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)})
|
|
playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 256})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(playerCtx, mf, "", 0, 0)
|
|
|
|
Expect(req.Format).To(Equal("opus"))
|
|
Expect(req.BitRate).To(Equal(256))
|
|
})
|
|
|
|
It("serves raw when only player MaxBitRate is set but no DefaultDownsamplingFormat", func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.DefaultDownsamplingFormat = ""
|
|
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)})
|
|
playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 256})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(playerCtx, mf, "", 0, 0)
|
|
|
|
Expect(req.Format).To(Equal("raw"))
|
|
})
|
|
})
|
|
|
|
Context("fallback for unknown format", func() {
|
|
It("falls back to DefaultDownsamplingFormat", func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.DefaultDownsamplingFormat = "opus"
|
|
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(ctx, mf, "xyz", 0, 0)
|
|
|
|
Expect(req.Format).To(Equal("opus"))
|
|
})
|
|
|
|
It("falls back to raw when DefaultDownsamplingFormat is empty", func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.DefaultDownsamplingFormat = ""
|
|
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(ctx, mf, "xyz", 0, 0)
|
|
|
|
Expect(req.Format).To(Equal("raw"))
|
|
})
|
|
|
|
It("falls back to raw when DefaultDownsamplingFormat is also invalid", func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.DefaultDownsamplingFormat = "xyz"
|
|
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(ctx, mf, "xyz", 0, 0)
|
|
|
|
Expect(req.Format).To(Equal("raw"))
|
|
})
|
|
|
|
It("preserves bitrate when falling back to DefaultDownsamplingFormat", func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.DefaultDownsamplingFormat = "opus"
|
|
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)})
|
|
|
|
decider := svc.(*deciderService)
|
|
req := decider.ResolveRequest(ctx, mf, "xyz", 128, 0)
|
|
|
|
Expect(req.Format).To(Equal("opus"))
|
|
Expect(req.BitRate).To(Equal(128))
|
|
})
|
|
})
|
|
})
|
|
|
|
var _ = Describe("ResolveClientRequest", func() {
|
|
var (
|
|
svc TranscodeDecider
|
|
ctx context.Context
|
|
)
|
|
mp3Target := []Profile{{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP}}
|
|
|
|
BeforeEach(func() {
|
|
ctx = GinkgoT().Context()
|
|
ds := &tests.MockDataStore{
|
|
MockedProperty: &tests.MockedPropertyRepo{},
|
|
MockedTranscoding: &tests.MockTranscodingRepo{},
|
|
}
|
|
auth.Init(ds)
|
|
svc = NewTranscodeDecider(ds, tests.NewMockFFmpeg(""))
|
|
})
|
|
|
|
It("direct plays a source matching a profile through container aliases", func() {
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "m4a", Codec: "AAC", BitRate: 256, Channels: 2, SampleRate: 44100})
|
|
ci := &ClientInfo{
|
|
DirectPlayProfiles: []DirectPlayProfile{{Containers: []string{"mp4"}, AudioCodecs: []string{"aac"}}},
|
|
TranscodingProfiles: mp3Target,
|
|
}
|
|
|
|
req := svc.ResolveClientRequest(ctx, mf, ci, 5)
|
|
|
|
Expect(req.Format).To(Equal("raw"))
|
|
Expect(req.Offset).To(Equal(5))
|
|
})
|
|
|
|
It("transcodes to the client's transcoding profile when no direct play profile matches", func() {
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)})
|
|
ci := &ClientInfo{
|
|
DirectPlayProfiles: []DirectPlayProfile{{Containers: []string{"mp3"}}},
|
|
TranscodingProfiles: mp3Target,
|
|
}
|
|
|
|
Expect(svc.ResolveClientRequest(ctx, mf, ci, 0).Format).To(Equal("mp3"))
|
|
})
|
|
|
|
It("transcodes a direct-playable source over the bitrate cap to the client's target", func() {
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)})
|
|
ci := &ClientInfo{
|
|
MaxAudioBitrate: 128,
|
|
DirectPlayProfiles: []DirectPlayProfile{{Containers: []string{"flac"}}},
|
|
TranscodingProfiles: mp3Target,
|
|
}
|
|
|
|
req := svc.ResolveClientRequest(ctx, mf, ci, 0)
|
|
|
|
Expect(req.Format).To(Equal("mp3"))
|
|
Expect(req.BitRate).To(Equal(128))
|
|
})
|
|
|
|
It("applies the player's MaxBitRate cap", func() {
|
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)})
|
|
ctx = request.WithPlayer(ctx, model.Player{MaxBitRate: 192})
|
|
ci := &ClientInfo{
|
|
DirectPlayProfiles: []DirectPlayProfile{{Containers: []string{"flac"}}},
|
|
TranscodingProfiles: mp3Target,
|
|
}
|
|
|
|
req := svc.ResolveClientRequest(ctx, mf, ci, 0)
|
|
|
|
Expect(req.Format).To(Equal("mp3"))
|
|
Expect(req.BitRate).To(Equal(192))
|
|
})
|
|
})
|