refactor: rename core/transcode to core/stream, simplify MediaStreamer (#5166)
* refactor: rename core/transcode directory to core/stream * refactor: update all imports from core/transcode to core/stream * refactor: rename exported symbols to fit core/stream package name * refactor: simplify MediaStreamer interface to single NewStream method Remove the two-method interface (NewStream + DoStream) in favor of a single NewStream(ctx, mf, req) method. Callers are now responsible for fetching the MediaFile before calling NewStream. This removes the implicit DB lookup from the streamer, making it a pure streaming concern. * refactor: update all callers from DoStream to NewStream * chore: update wire_gen.go and stale comment for core/stream rename * refactor: update wire command to handle GO_BUILD_TAGS correctly Signed-off-by: Deluan <deluan@navidrome.org> * fix: distinguish not-found from internal errors in public stream handler * refactor: remove unused ID field from stream.Request * refactor: simplify ResolveRequestFromToken to receive *model.MediaFile Move MediaFile fetching responsibility to callers, making the method focused on token validation and request resolution. Remove ErrMediaNotFound (no longer produced). Update GetTranscodeStream handler to fetch the media file before calling ResolveRequestFromToken. * refactor: extend tokenTTL from 12 to 48 hours Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
@@ -109,7 +109,7 @@ format: ##@Development Format code
|
|||||||
.PHONY: format
|
.PHONY: format
|
||||||
|
|
||||||
wire: check_go_env ##@Development Update Dependency Injection
|
wire: check_go_env ##@Development Update Dependency Injection
|
||||||
go tool wire gen -tags=$(GO_BUILD_TAGS) ./...
|
go tool wire gen -tags="$$(echo '$(GO_BUILD_TAGS)' | tr ',' ' ')" ./...
|
||||||
.PHONY: wire
|
.PHONY: wire
|
||||||
|
|
||||||
gen: check_go_env ##@Development Run go generate for code generation
|
gen: check_go_env ##@Development Run go generate for code generation
|
||||||
|
|||||||
+7
-7
@@ -21,7 +21,7 @@ import (
|
|||||||
"github.com/navidrome/navidrome/core/playback"
|
"github.com/navidrome/navidrome/core/playback"
|
||||||
"github.com/navidrome/navidrome/core/playlists"
|
"github.com/navidrome/navidrome/core/playlists"
|
||||||
"github.com/navidrome/navidrome/core/scrobbler"
|
"github.com/navidrome/navidrome/core/scrobbler"
|
||||||
"github.com/navidrome/navidrome/core/transcode"
|
"github.com/navidrome/navidrome/core/stream"
|
||||||
"github.com/navidrome/navidrome/db"
|
"github.com/navidrome/navidrome/db"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
"github.com/navidrome/navidrome/persistence"
|
"github.com/navidrome/navidrome/persistence"
|
||||||
@@ -95,8 +95,8 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
|
|||||||
agentsAgents := agents.GetAgents(dataStore, manager)
|
agentsAgents := agents.GetAgents(dataStore, manager)
|
||||||
provider := external.NewProvider(dataStore, agentsAgents)
|
provider := external.NewProvider(dataStore, agentsAgents)
|
||||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||||
transcodingCache := transcode.GetTranscodingCache()
|
transcodingCache := stream.GetTranscodingCache()
|
||||||
mediaStreamer := transcode.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
mediaStreamer := stream.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
||||||
share := core.NewShare(dataStore)
|
share := core.NewShare(dataStore)
|
||||||
archiver := core.NewArchiver(mediaStreamer, dataStore, share)
|
archiver := core.NewArchiver(mediaStreamer, dataStore, share)
|
||||||
players := core.NewPlayers(dataStore)
|
players := core.NewPlayers(dataStore)
|
||||||
@@ -106,8 +106,8 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
|
|||||||
playTracker := scrobbler.GetPlayTracker(dataStore, broker, manager)
|
playTracker := scrobbler.GetPlayTracker(dataStore, broker, manager)
|
||||||
playbackServer := playback.GetInstance(dataStore)
|
playbackServer := playback.GetInstance(dataStore)
|
||||||
lyricsLyrics := lyrics.NewLyrics(manager)
|
lyricsLyrics := lyrics.NewLyrics(manager)
|
||||||
decider := transcode.NewDecider(dataStore, fFmpeg)
|
transcodeDecider := stream.NewTranscodeDecider(dataStore, fFmpeg)
|
||||||
router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, provider, modelScanner, broker, playlistsPlaylists, playTracker, share, playbackServer, metricsMetrics, lyricsLyrics, decider)
|
router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, provider, modelScanner, broker, playlistsPlaylists, playTracker, share, playbackServer, metricsMetrics, lyricsLyrics, transcodeDecider)
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,8 +122,8 @@ func CreatePublicRouter() *public.Router {
|
|||||||
agentsAgents := agents.GetAgents(dataStore, manager)
|
agentsAgents := agents.GetAgents(dataStore, manager)
|
||||||
provider := external.NewProvider(dataStore, agentsAgents)
|
provider := external.NewProvider(dataStore, agentsAgents)
|
||||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||||
transcodingCache := transcode.GetTranscodingCache()
|
transcodingCache := stream.GetTranscodingCache()
|
||||||
mediaStreamer := transcode.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
mediaStreamer := stream.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
||||||
share := core.NewShare(dataStore)
|
share := core.NewShare(dataStore)
|
||||||
archiver := core.NewArchiver(mediaStreamer, dataStore, share)
|
archiver := core.NewArchiver(mediaStreamer, dataStore, share)
|
||||||
router := public.New(dataStore, artworkArtwork, mediaStreamer, share, archiver)
|
router := public.New(dataStore, artworkArtwork, mediaStreamer, share, archiver)
|
||||||
|
|||||||
+4
-4
@@ -10,7 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Masterminds/squirrel"
|
"github.com/Masterminds/squirrel"
|
||||||
"github.com/navidrome/navidrome/core/transcode"
|
"github.com/navidrome/navidrome/core/stream"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
"github.com/navidrome/navidrome/utils/slice"
|
"github.com/navidrome/navidrome/utils/slice"
|
||||||
@@ -23,13 +23,13 @@ type Archiver interface {
|
|||||||
ZipPlaylist(ctx context.Context, id string, format string, bitrate int, w io.Writer) error
|
ZipPlaylist(ctx context.Context, id string, format string, bitrate int, w io.Writer) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewArchiver(ms transcode.MediaStreamer, ds model.DataStore, shares Share) Archiver {
|
func NewArchiver(ms stream.MediaStreamer, ds model.DataStore, shares Share) Archiver {
|
||||||
return &archiver{ds: ds, ms: ms, shares: shares}
|
return &archiver{ds: ds, ms: ms, shares: shares}
|
||||||
}
|
}
|
||||||
|
|
||||||
type archiver struct {
|
type archiver struct {
|
||||||
ds model.DataStore
|
ds model.DataStore
|
||||||
ms transcode.MediaStreamer
|
ms stream.MediaStreamer
|
||||||
shares Share
|
shares Share
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.Med
|
|||||||
|
|
||||||
var r io.ReadCloser
|
var r io.ReadCloser
|
||||||
if format != "raw" && format != "" {
|
if format != "raw" && format != "" {
|
||||||
r, err = a.ms.DoStream(ctx, &mf, transcode.StreamRequest{Format: format, BitRate: bitrate})
|
r, err = a.ms.NewStream(ctx, &mf, stream.Request{Format: format, BitRate: bitrate})
|
||||||
} else {
|
} else {
|
||||||
r, err = os.Open(path)
|
r, err = os.Open(path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Masterminds/squirrel"
|
"github.com/Masterminds/squirrel"
|
||||||
"github.com/navidrome/navidrome/core"
|
"github.com/navidrome/navidrome/core"
|
||||||
"github.com/navidrome/navidrome/core/transcode"
|
"github.com/navidrome/navidrome/core/stream"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
@@ -45,7 +45,7 @@ var _ = Describe("Archiver", func() {
|
|||||||
}}).Return(mfs, nil)
|
}}).Return(mfs, nil)
|
||||||
|
|
||||||
ds.On("MediaFile", mock.Anything).Return(mfRepo)
|
ds.On("MediaFile", mock.Anything).Return(mfRepo)
|
||||||
ms.On("DoStream", mock.Anything, mock.Anything, transcode.StreamRequest{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(3)
|
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(3)
|
||||||
|
|
||||||
out := new(bytes.Buffer)
|
out := new(bytes.Buffer)
|
||||||
err := arch.ZipAlbum(context.Background(), "1", "mp3", 128, out)
|
err := arch.ZipAlbum(context.Background(), "1", "mp3", 128, out)
|
||||||
@@ -74,7 +74,7 @@ var _ = Describe("Archiver", func() {
|
|||||||
}}).Return(mfs, nil)
|
}}).Return(mfs, nil)
|
||||||
|
|
||||||
ds.On("MediaFile", mock.Anything).Return(mfRepo)
|
ds.On("MediaFile", mock.Anything).Return(mfRepo)
|
||||||
ms.On("DoStream", mock.Anything, mock.Anything, transcode.StreamRequest{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
|
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
|
||||||
|
|
||||||
out := new(bytes.Buffer)
|
out := new(bytes.Buffer)
|
||||||
err := arch.ZipArtist(context.Background(), "1", "mp3", 128, out)
|
err := arch.ZipArtist(context.Background(), "1", "mp3", 128, out)
|
||||||
@@ -105,7 +105,7 @@ var _ = Describe("Archiver", func() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sh.On("Load", mock.Anything, "1").Return(share, nil)
|
sh.On("Load", mock.Anything, "1").Return(share, nil)
|
||||||
ms.On("DoStream", mock.Anything, mock.Anything, transcode.StreamRequest{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
|
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
|
||||||
|
|
||||||
out := new(bytes.Buffer)
|
out := new(bytes.Buffer)
|
||||||
err := arch.ZipShare(context.Background(), "1", out)
|
err := arch.ZipShare(context.Background(), "1", out)
|
||||||
@@ -137,7 +137,7 @@ var _ = Describe("Archiver", func() {
|
|||||||
plRepo := &mockPlaylistRepository{}
|
plRepo := &mockPlaylistRepository{}
|
||||||
plRepo.On("GetWithTracks", "1", true, false).Return(pls, nil)
|
plRepo.On("GetWithTracks", "1", true, false).Return(pls, nil)
|
||||||
ds.On("Playlist", mock.Anything).Return(plRepo)
|
ds.On("Playlist", mock.Anything).Return(plRepo)
|
||||||
ms.On("DoStream", mock.Anything, mock.Anything, transcode.StreamRequest{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
|
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
|
||||||
|
|
||||||
out := new(bytes.Buffer)
|
out := new(bytes.Buffer)
|
||||||
err := arch.ZipPlaylist(context.Background(), "1", "mp3", 128, out)
|
err := arch.ZipPlaylist(context.Background(), "1", "mp3", 128, out)
|
||||||
@@ -215,15 +215,15 @@ func (m *mockPlaylistRepository) GetWithTracks(id string, refreshSmartPlaylists,
|
|||||||
|
|
||||||
type mockMediaStreamer struct {
|
type mockMediaStreamer struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
transcode.MediaStreamer
|
stream.MediaStreamer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockMediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, req transcode.StreamRequest) (*transcode.Stream, error) {
|
func (m *mockMediaStreamer) NewStream(ctx context.Context, mf *model.MediaFile, req stream.Request) (*stream.Stream, error) {
|
||||||
args := m.Called(ctx, mf, req)
|
args := m.Called(ctx, mf, req)
|
||||||
if args.Error(1) != nil {
|
if args.Error(1) != nil {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
}
|
}
|
||||||
return &transcode.Stream{ReadCloser: args.Get(0).(io.ReadCloser)}, nil
|
return &stream.Stream{ReadCloser: args.Get(0).(io.ReadCloser)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockShare struct {
|
type mockShare struct {
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ func injectBeforeOutput(args []string, flag, value string) []string {
|
|||||||
// isLosslessOutputFormat returns true if the format is a lossless audio format
|
// isLosslessOutputFormat returns true if the format is a lossless audio format
|
||||||
// where preserving bit depth via -sample_fmt is meaningful.
|
// where preserving bit depth via -sample_fmt is meaningful.
|
||||||
// Note: this covers only formats ffmpeg can produce as output. For the full set of
|
// Note: this covers only formats ffmpeg can produce as output. For the full set of
|
||||||
// lossless formats used in transcoding decisions, see core/transcode/codec.go:isLosslessFormat.
|
// lossless formats used in transcoding decisions, see core/stream/codec.go:isLosslessFormat.
|
||||||
func isLosslessOutputFormat(format string) bool {
|
func isLosslessOutputFormat(format string) bool {
|
||||||
switch strings.ToLower(format) {
|
switch strings.ToLower(format) {
|
||||||
case "flac", "alac", "wav", "aiff":
|
case "flac", "alac", "wav", "aiff":
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package transcode
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
"slices"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package transcode
|
package stream
|
||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package transcode
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package transcode
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -16,15 +16,15 @@ import (
|
|||||||
|
|
||||||
const fallbackBitrate = 256 // kbps
|
const fallbackBitrate = 256 // kbps
|
||||||
|
|
||||||
// Decider is the core service interface for making transcoding decisions
|
// TranscodeDecider is the core service interface for making transcoding decisions
|
||||||
type Decider interface {
|
type TranscodeDecider interface {
|
||||||
MakeDecision(ctx context.Context, mf *model.MediaFile, clientInfo *ClientInfo, opts DecisionOptions) (*Decision, error)
|
MakeDecision(ctx context.Context, mf *model.MediaFile, clientInfo *ClientInfo, opts TranscodeOptions) (*TranscodeDecision, error)
|
||||||
CreateTranscodeParams(decision *Decision) (string, error)
|
CreateTranscodeParams(decision *TranscodeDecision) (string, error)
|
||||||
ResolveRequestFromToken(ctx context.Context, token string, mediaID string, offset int) (StreamRequest, *model.MediaFile, error)
|
ResolveRequestFromToken(ctx context.Context, token string, mf *model.MediaFile, offset int) (Request, error)
|
||||||
ResolveRequest(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int, offset int) StreamRequest
|
ResolveRequest(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int, offset int) Request
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDecider(ds model.DataStore, ff ffmpeg.FFmpeg) Decider {
|
func NewTranscodeDecider(ds model.DataStore, ff ffmpeg.FFmpeg) TranscodeDecider {
|
||||||
return &deciderService{
|
return &deciderService{
|
||||||
ds: ds,
|
ds: ds,
|
||||||
ff: ff,
|
ff: ff,
|
||||||
@@ -36,8 +36,8 @@ type deciderService struct {
|
|||||||
ff ffmpeg.FFmpeg
|
ff ffmpeg.FFmpeg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *deciderService) MakeDecision(ctx context.Context, mf *model.MediaFile, clientInfo *ClientInfo, opts DecisionOptions) (*Decision, error) {
|
func (s *deciderService) MakeDecision(ctx context.Context, mf *model.MediaFile, clientInfo *ClientInfo, opts TranscodeOptions) (*TranscodeDecision, error) {
|
||||||
decision := &Decision{
|
decision := &TranscodeDecision{
|
||||||
MediaID: mf.ID,
|
MediaID: mf.ID,
|
||||||
SourceUpdatedAt: mf.UpdatedAt,
|
SourceUpdatedAt: mf.UpdatedAt,
|
||||||
}
|
}
|
||||||
@@ -126,8 +126,8 @@ func (s *deciderService) MakeDecision(ctx context.Context, mf *model.MediaFile,
|
|||||||
return decision, nil
|
return decision, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSourceStream(mf *model.MediaFile, probe *ffmpeg.AudioProbeResult) StreamDetails {
|
func buildSourceStream(mf *model.MediaFile, probe *ffmpeg.AudioProbeResult) Details {
|
||||||
sd := StreamDetails{
|
sd := Details{
|
||||||
Container: mf.Suffix,
|
Container: mf.Suffix,
|
||||||
Duration: mf.Duration,
|
Duration: mf.Duration,
|
||||||
Size: mf.Size,
|
Size: mf.Size,
|
||||||
@@ -197,7 +197,7 @@ func parseProbeData(data string) (*ffmpeg.AudioProbeResult, error) {
|
|||||||
|
|
||||||
// checkDirectPlayProfile returns "" if the profile matches (direct play OK),
|
// checkDirectPlayProfile returns "" if the profile matches (direct play OK),
|
||||||
// or a typed reason string if it doesn't match.
|
// or a typed reason string if it doesn't match.
|
||||||
func (s *deciderService) checkDirectPlayProfile(src *StreamDetails, profile *DirectPlayProfile, clientInfo *ClientInfo) string {
|
func (s *deciderService) checkDirectPlayProfile(src *Details, profile *DirectPlayProfile, clientInfo *ClientInfo) string {
|
||||||
// Check protocol (only http for now)
|
// Check protocol (only http for now)
|
||||||
if len(profile.Protocols) > 0 && !containsIgnoreCase(profile.Protocols, ProtocolHTTP) {
|
if len(profile.Protocols) > 0 && !containsIgnoreCase(profile.Protocols, ProtocolHTTP) {
|
||||||
return "protocol not supported"
|
return "protocol not supported"
|
||||||
@@ -234,7 +234,7 @@ func (s *deciderService) checkDirectPlayProfile(src *StreamDetails, profile *Dir
|
|||||||
// Returns the stream details and the internal transcoding format (which may differ from the
|
// Returns the stream details and the internal transcoding format (which may differ from the
|
||||||
// response container when a codec fallback occurs, e.g., "mp4"→"aac").
|
// response container when a codec fallback occurs, e.g., "mp4"→"aac").
|
||||||
// Returns nil, "" if the profile cannot produce a valid output.
|
// Returns nil, "" if the profile cannot produce a valid output.
|
||||||
func (s *deciderService) computeTranscodedStream(ctx context.Context, src *StreamDetails, profile *Profile, clientInfo *ClientInfo) (*StreamDetails, string) {
|
func (s *deciderService) computeTranscodedStream(ctx context.Context, src *Details, profile *Profile, clientInfo *ClientInfo) (*Details, string) {
|
||||||
// Check protocol (only http for now)
|
// Check protocol (only http for now)
|
||||||
if profile.Protocol != "" && !strings.EqualFold(profile.Protocol, ProtocolHTTP) {
|
if profile.Protocol != "" && !strings.EqualFold(profile.Protocol, ProtocolHTTP) {
|
||||||
log.Trace(ctx, "Skipping transcoding profile: unsupported protocol", "protocol", profile.Protocol)
|
log.Trace(ctx, "Skipping transcoding profile: unsupported protocol", "protocol", profile.Protocol)
|
||||||
@@ -260,7 +260,7 @@ func (s *deciderService) computeTranscodedStream(ctx context.Context, src *Strea
|
|||||||
return nil, ""
|
return nil, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := &StreamDetails{
|
ts := &Details{
|
||||||
Container: responseContainer,
|
Container: responseContainer,
|
||||||
Codec: strings.ToLower(profile.AudioCodec),
|
Codec: strings.ToLower(profile.AudioCodec),
|
||||||
SampleRate: normalizeSourceSampleRate(src.SampleRate, src.Codec),
|
SampleRate: normalizeSourceSampleRate(src.SampleRate, src.Codec),
|
||||||
@@ -358,7 +358,7 @@ func resolveTargetFormat(profile *Profile) (responseContainer, targetFormat stri
|
|||||||
|
|
||||||
// computeBitrate determines the target bitrate for the transcoded stream.
|
// computeBitrate determines the target bitrate for the transcoded stream.
|
||||||
// Returns false if the profile should be rejected.
|
// Returns false if the profile should be rejected.
|
||||||
func (s *deciderService) computeBitrate(ctx context.Context, src *StreamDetails, targetFormat string, targetIsLossless bool, clientInfo *ClientInfo, ts *StreamDetails) bool {
|
func (s *deciderService) computeBitrate(ctx context.Context, src *Details, targetFormat string, targetIsLossless bool, clientInfo *ClientInfo, ts *Details) bool {
|
||||||
if src.IsLossless {
|
if src.IsLossless {
|
||||||
if !targetIsLossless {
|
if !targetIsLossless {
|
||||||
if clientInfo.MaxTranscodingAudioBitrate > 0 {
|
if clientInfo.MaxTranscodingAudioBitrate > 0 {
|
||||||
@@ -388,7 +388,7 @@ func (s *deciderService) computeBitrate(ctx context.Context, src *StreamDetails,
|
|||||||
|
|
||||||
// applyCodecLimitations applies codec profile limitations to the transcoded stream.
|
// applyCodecLimitations applies codec profile limitations to the transcoded stream.
|
||||||
// Returns false if the profile should be rejected.
|
// Returns false if the profile should be rejected.
|
||||||
func (s *deciderService) applyCodecLimitations(ctx context.Context, sourceBitrate int, targetFormat string, targetIsLossless bool, clientInfo *ClientInfo, ts *StreamDetails) bool {
|
func (s *deciderService) applyCodecLimitations(ctx context.Context, sourceBitrate int, targetFormat string, targetIsLossless bool, clientInfo *ClientInfo, ts *Details) bool {
|
||||||
targetCodec := ts.Codec
|
targetCodec := ts.Codec
|
||||||
for _, codecProfile := range clientInfo.CodecProfiles {
|
for _, codecProfile := range clientInfo.CodecProfiles {
|
||||||
if !strings.EqualFold(codecProfile.Type, CodecProfileTypeAudio) {
|
if !strings.EqualFold(codecProfile.Type, CodecProfileTypeAudio) {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package transcode
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -35,7 +35,7 @@ var _ = Describe("Decider", func() {
|
|||||||
var (
|
var (
|
||||||
ds *tests.MockDataStore
|
ds *tests.MockDataStore
|
||||||
ff *tests.MockFFmpeg
|
ff *tests.MockFFmpeg
|
||||||
svc Decider
|
svc TranscodeDecider
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ var _ = Describe("Decider", func() {
|
|||||||
}
|
}
|
||||||
ff = tests.NewMockFFmpeg("")
|
ff = tests.NewMockFFmpeg("")
|
||||||
auth.Init(ds)
|
auth.Init(ds)
|
||||||
svc = NewDecider(ds, ff)
|
svc = NewTranscodeDecider(ds, ff)
|
||||||
})
|
})
|
||||||
|
|
||||||
Describe("MakeDecision", func() {
|
Describe("MakeDecision", func() {
|
||||||
@@ -59,7 +59,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Containers: []string{"mp3"}, AudioCodecs: []string{"mp3"}, Protocols: []string{ProtocolHTTP}, MaxAudioChannels: 2},
|
{Containers: []string{"mp3"}, AudioCodecs: []string{"mp3"}, Protocols: []string{ProtocolHTTP}, MaxAudioChannels: 2},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeTrue())
|
Expect(decision.CanDirectPlay).To(BeTrue())
|
||||||
Expect(decision.CanTranscode).To(BeFalse())
|
Expect(decision.CanTranscode).To(BeFalse())
|
||||||
@@ -73,7 +73,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Containers: []string{"mp3"}, Protocols: []string{ProtocolHTTP}},
|
{Containers: []string{"mp3"}, Protocols: []string{ProtocolHTTP}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeFalse())
|
Expect(decision.CanDirectPlay).To(BeFalse())
|
||||||
Expect(decision.TranscodeReasons).To(ContainElement("container not supported"))
|
Expect(decision.TranscodeReasons).To(ContainElement("container not supported"))
|
||||||
@@ -86,7 +86,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Containers: []string{"m4a"}, AudioCodecs: []string{"aac"}, Protocols: []string{ProtocolHTTP}},
|
{Containers: []string{"m4a"}, AudioCodecs: []string{"aac"}, Protocols: []string{ProtocolHTTP}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeFalse())
|
Expect(decision.CanDirectPlay).To(BeFalse())
|
||||||
Expect(decision.TranscodeReasons).To(ContainElement("audio codec not supported"))
|
Expect(decision.TranscodeReasons).To(ContainElement("audio codec not supported"))
|
||||||
@@ -99,7 +99,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}, MaxAudioChannels: 2},
|
{Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}, MaxAudioChannels: 2},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeFalse())
|
Expect(decision.CanDirectPlay).To(BeFalse())
|
||||||
Expect(decision.TranscodeReasons).To(ContainElement("audio channels not supported"))
|
Expect(decision.TranscodeReasons).To(ContainElement("audio channels not supported"))
|
||||||
@@ -112,7 +112,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Containers: []string{"aac"}, AudioCodecs: []string{"aac"}, Protocols: []string{ProtocolHTTP}},
|
{Containers: []string{"aac"}, AudioCodecs: []string{"aac"}, Protocols: []string{ProtocolHTTP}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeTrue())
|
Expect(decision.CanDirectPlay).To(BeTrue())
|
||||||
})
|
})
|
||||||
@@ -124,7 +124,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Containers: []string{"mp4"}, AudioCodecs: []string{"aac"}, Protocols: []string{ProtocolHTTP}},
|
{Containers: []string{"mp4"}, AudioCodecs: []string{"aac"}, Protocols: []string{ProtocolHTTP}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeTrue())
|
Expect(decision.CanDirectPlay).To(BeTrue())
|
||||||
})
|
})
|
||||||
@@ -136,7 +136,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Containers: []string{"ogg"}, AudioCodecs: []string{"opus"}, Protocols: []string{ProtocolHTTP}},
|
{Containers: []string{"ogg"}, AudioCodecs: []string{"opus"}, Protocols: []string{ProtocolHTTP}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeTrue())
|
Expect(decision.CanDirectPlay).To(BeTrue())
|
||||||
})
|
})
|
||||||
@@ -148,7 +148,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Containers: []string{"m4a"}, AudioCodecs: []string{"adts"}, Protocols: []string{ProtocolHTTP}},
|
{Containers: []string{"m4a"}, AudioCodecs: []string{"adts"}, Protocols: []string{ProtocolHTTP}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeTrue())
|
Expect(decision.CanDirectPlay).To(BeTrue())
|
||||||
})
|
})
|
||||||
@@ -160,7 +160,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Containers: []string{"flac"}, AudioCodecs: []string{"flac"}},
|
{Containers: []string{"flac"}, AudioCodecs: []string{"flac"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeTrue())
|
Expect(decision.CanDirectPlay).To(BeTrue())
|
||||||
})
|
})
|
||||||
@@ -172,7 +172,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Containers: []string{}, AudioCodecs: []string{}},
|
{Containers: []string{}, AudioCodecs: []string{}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeTrue())
|
Expect(decision.CanDirectPlay).To(BeTrue())
|
||||||
})
|
})
|
||||||
@@ -190,7 +190,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeFalse())
|
Expect(decision.CanDirectPlay).To(BeFalse())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
@@ -210,7 +210,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP, MaxAudioChannels: 2},
|
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP, MaxAudioChannels: 2},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeFalse())
|
Expect(decision.CanDirectPlay).To(BeFalse())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
@@ -226,7 +226,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "flac", Protocol: ProtocolHTTP},
|
{Container: "flac", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeFalse())
|
Expect(decision.CanTranscode).To(BeFalse())
|
||||||
})
|
})
|
||||||
@@ -238,7 +238,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "mp3", Protocol: ProtocolHTTP},
|
{Container: "mp3", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TargetBitrate).To(Equal(160)) // mp3 default from mock transcoding repo
|
Expect(decision.TargetBitrate).To(Equal(160)) // mp3 default from mock transcoding repo
|
||||||
@@ -252,7 +252,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "mp3", Protocol: ProtocolHTTP},
|
{Container: "mp3", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TargetBitrate).To(Equal(192)) // source bitrate in kbps
|
Expect(decision.TargetBitrate).To(Equal(192)) // source bitrate in kbps
|
||||||
@@ -265,7 +265,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "wav", Protocol: ProtocolHTTP},
|
{Container: "wav", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeFalse())
|
Expect(decision.CanTranscode).To(BeFalse())
|
||||||
})
|
})
|
||||||
@@ -278,7 +278,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TargetBitrate).To(Equal(96)) // capped by maxAudioBitrate
|
Expect(decision.TargetBitrate).To(Equal(96)) // capped by maxAudioBitrate
|
||||||
@@ -296,7 +296,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP, MaxAudioChannels: 2},
|
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP, MaxAudioChannels: 2},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TargetFormat).To(Equal("opus"))
|
Expect(decision.TargetFormat).To(Equal("opus"))
|
||||||
@@ -315,7 +315,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TargetFormat).To(Equal("mp3"))
|
Expect(decision.TargetFormat).To(Equal("mp3"))
|
||||||
@@ -330,7 +330,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TranscodeStream.IsLossless).To(BeFalse()) // mp3 is lossy
|
Expect(decision.TranscodeStream.IsLossless).To(BeFalse()) // mp3 is lossy
|
||||||
@@ -341,7 +341,7 @@ var _ = Describe("Decider", func() {
|
|||||||
It("returns error when nothing matches", func() {
|
It("returns error when nothing matches", func() {
|
||||||
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6})
|
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6})
|
||||||
ci := &ClientInfo{}
|
ci := &ClientInfo{}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeFalse())
|
Expect(decision.CanDirectPlay).To(BeFalse())
|
||||||
Expect(decision.CanTranscode).To(BeFalse())
|
Expect(decision.CanTranscode).To(BeFalse())
|
||||||
@@ -366,7 +366,7 @@ var _ = Describe("Decider", func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeFalse())
|
Expect(decision.CanDirectPlay).To(BeFalse())
|
||||||
Expect(decision.TranscodeReasons).To(ContainElement("audio bitrate not supported"))
|
Expect(decision.TranscodeReasons).To(ContainElement("audio bitrate not supported"))
|
||||||
@@ -388,7 +388,7 @@ var _ = Describe("Decider", func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeTrue())
|
Expect(decision.CanDirectPlay).To(BeTrue())
|
||||||
})
|
})
|
||||||
@@ -409,7 +409,7 @@ var _ = Describe("Decider", func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeTrue())
|
Expect(decision.CanDirectPlay).To(BeTrue())
|
||||||
})
|
})
|
||||||
@@ -430,7 +430,7 @@ var _ = Describe("Decider", func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeFalse())
|
Expect(decision.CanDirectPlay).To(BeFalse())
|
||||||
})
|
})
|
||||||
@@ -452,7 +452,7 @@ var _ = Describe("Decider", func() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
// Source profile is empty (not yet populated from scanner), so Equals("LC") fails
|
// Source profile is empty (not yet populated from scanner), so Equals("LC") fails
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeFalse())
|
Expect(decision.CanDirectPlay).To(BeFalse())
|
||||||
Expect(decision.TranscodeReasons).To(ContainElement("audio profile not supported"))
|
Expect(decision.TranscodeReasons).To(ContainElement("audio profile not supported"))
|
||||||
@@ -474,7 +474,7 @@ var _ = Describe("Decider", func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeTrue())
|
Expect(decision.CanDirectPlay).To(BeTrue())
|
||||||
})
|
})
|
||||||
@@ -495,7 +495,7 @@ var _ = Describe("Decider", func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeFalse())
|
Expect(decision.CanDirectPlay).To(BeFalse())
|
||||||
Expect(decision.TranscodeReasons).To(ContainElement("audio samplerate not supported"))
|
Expect(decision.TranscodeReasons).To(ContainElement("audio samplerate not supported"))
|
||||||
@@ -520,7 +520,7 @@ var _ = Describe("Decider", func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TranscodeStream.Bitrate).To(Equal(96))
|
Expect(decision.TranscodeStream.Bitrate).To(Equal(96))
|
||||||
@@ -543,7 +543,7 @@ var _ = Describe("Decider", func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TranscodeStream.Channels).To(Equal(2))
|
Expect(decision.TranscodeStream.Channels).To(Equal(2))
|
||||||
@@ -566,7 +566,7 @@ var _ = Describe("Decider", func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TranscodeStream.SampleRate).To(Equal(48000))
|
Expect(decision.TranscodeStream.SampleRate).To(Equal(48000))
|
||||||
@@ -588,7 +588,7 @@ var _ = Describe("Decider", func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TranscodeStream.BitDepth).To(Equal(16))
|
Expect(decision.TranscodeStream.BitDepth).To(Equal(16))
|
||||||
@@ -602,7 +602,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "flac", AudioCodec: "flac", Protocol: ProtocolHTTP},
|
{Container: "flac", AudioCodec: "flac", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TranscodeStream.BitDepth).To(Equal(24))
|
Expect(decision.TranscodeStream.BitDepth).To(Equal(24))
|
||||||
@@ -626,7 +626,7 @@ var _ = Describe("Decider", func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeFalse())
|
Expect(decision.CanTranscode).To(BeFalse())
|
||||||
})
|
})
|
||||||
@@ -641,7 +641,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TargetFormat).To(Equal("mp3"))
|
Expect(decision.TargetFormat).To(Equal("mp3"))
|
||||||
@@ -660,7 +660,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "flac", AudioCodec: "flac", Protocol: ProtocolHTTP},
|
{Container: "flac", AudioCodec: "flac", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TargetFormat).To(Equal("flac"))
|
Expect(decision.TargetFormat).To(Equal("flac"))
|
||||||
@@ -688,7 +688,7 @@ var _ = Describe("Decider", func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
// DSD64 2822400 / 8 = 352800, capped by codec profile limit of 48000
|
// DSD64 2822400 / 8 = 352800, capped by codec profile limit of 48000
|
||||||
@@ -715,7 +715,7 @@ var _ = Describe("Decider", func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
// DSD 1-bit → 24-bit PCM, then capped by codec profile limit to 16-bit
|
// DSD 1-bit → 24-bit PCM, then capped by codec profile limit to 16-bit
|
||||||
@@ -740,7 +740,7 @@ var _ = Describe("Decider", func() {
|
|||||||
},
|
},
|
||||||
MaxTranscodingAudioBitrate: 256,
|
MaxTranscodingAudioBitrate: 256,
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.SourceStream.IsLossless).To(BeTrue())
|
Expect(decision.SourceStream.IsLossless).To(BeTrue())
|
||||||
Expect(decision.SourceStream.Codec).To(Equal("wavpack"))
|
Expect(decision.SourceStream.Codec).To(Equal("wavpack"))
|
||||||
@@ -762,7 +762,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Containers: []string{"ogg"}, AudioCodecs: []string{"vorbis"}, Protocols: []string{ProtocolHTTP}},
|
{Containers: []string{"ogg"}, AudioCodecs: []string{"vorbis"}, Protocols: []string{ProtocolHTTP}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.SourceStream.IsLossless).To(BeFalse())
|
Expect(decision.SourceStream.IsLossless).To(BeFalse())
|
||||||
Expect(decision.CanDirectPlay).To(BeTrue())
|
Expect(decision.CanDirectPlay).To(BeTrue())
|
||||||
@@ -778,7 +778,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "opus", AudioCodec: "opus", Protocol: ProtocolHTTP},
|
{Container: "opus", AudioCodec: "opus", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TargetFormat).To(Equal("opus"))
|
Expect(decision.TargetFormat).To(Equal("opus"))
|
||||||
@@ -795,7 +795,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "opus", AudioCodec: "opus", Protocol: ProtocolHTTP},
|
{Container: "opus", AudioCodec: "opus", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TranscodeStream.SampleRate).To(Equal(48000))
|
Expect(decision.TranscodeStream.SampleRate).To(Equal(48000))
|
||||||
@@ -811,7 +811,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "mp4", AudioCodec: "aac", Protocol: ProtocolHTTP},
|
{Container: "mp4", AudioCodec: "aac", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
// TargetFormat is the internal format used for transcoding ("aac")
|
// TargetFormat is the internal format used for transcoding ("aac")
|
||||||
@@ -829,7 +829,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TargetFormat).To(Equal("mp3"))
|
Expect(decision.TargetFormat).To(Equal("mp3"))
|
||||||
@@ -846,7 +846,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TranscodeStream.SampleRate).To(Equal(48000))
|
Expect(decision.TranscodeStream.SampleRate).To(Equal(48000))
|
||||||
@@ -860,7 +860,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TranscodeStream.SampleRate).To(Equal(44100))
|
Expect(decision.TranscodeStream.SampleRate).To(Equal(44100))
|
||||||
@@ -876,7 +876,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "aac", AudioCodec: "aac", Protocol: ProtocolHTTP},
|
{Container: "aac", AudioCodec: "aac", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
// DSD64 2822400 / 8 = 352800, capped by AAC max of 96000
|
// DSD64 2822400 / 8 = 352800, capped by AAC max of 96000
|
||||||
@@ -897,7 +897,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeFalse())
|
Expect(decision.CanDirectPlay).To(BeFalse())
|
||||||
Expect(decision.TranscodeReasons).To(HaveLen(3))
|
Expect(decision.TranscodeReasons).To(HaveLen(3))
|
||||||
@@ -915,7 +915,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}},
|
{Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.SourceStream.Container).To(Equal("flac"))
|
Expect(decision.SourceStream.Container).To(Equal("flac"))
|
||||||
Expect(decision.SourceStream.Codec).To(Equal("flac"))
|
Expect(decision.SourceStream.Codec).To(Equal("flac"))
|
||||||
@@ -939,7 +939,7 @@ var _ = Describe("Decider", func() {
|
|||||||
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 192})
|
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 192})
|
||||||
overrideCtx = request.WithPlayer(overrideCtx, model.Player{MaxBitRate: 0})
|
overrideCtx = request.WithPlayer(overrideCtx, model.Player{MaxBitRate: 0})
|
||||||
|
|
||||||
decision, err := svc.MakeDecision(overrideCtx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(overrideCtx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeFalse())
|
Expect(decision.CanDirectPlay).To(BeFalse())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
@@ -957,7 +957,7 @@ var _ = Describe("Decider", func() {
|
|||||||
}
|
}
|
||||||
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 256})
|
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 256})
|
||||||
|
|
||||||
decision, err := svc.MakeDecision(overrideCtx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(overrideCtx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeTrue())
|
Expect(decision.CanDirectPlay).To(BeTrue())
|
||||||
Expect(decision.CanTranscode).To(BeFalse())
|
Expect(decision.CanTranscode).To(BeFalse())
|
||||||
@@ -970,7 +970,7 @@ var _ = Describe("Decider", func() {
|
|||||||
}
|
}
|
||||||
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 192})
|
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 192})
|
||||||
|
|
||||||
decision, err := svc.MakeDecision(overrideCtx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(overrideCtx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeFalse())
|
Expect(decision.CanDirectPlay).To(BeFalse())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
@@ -986,7 +986,7 @@ var _ = Describe("Decider", func() {
|
|||||||
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 192})
|
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 192})
|
||||||
overrideCtx = request.WithPlayer(overrideCtx, model.Player{MaxBitRate: 320})
|
overrideCtx = request.WithPlayer(overrideCtx, model.Player{MaxBitRate: 320})
|
||||||
|
|
||||||
decision, err := svc.MakeDecision(overrideCtx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(overrideCtx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TargetFormat).To(Equal("mp3"))
|
Expect(decision.TargetFormat).To(Equal("mp3"))
|
||||||
@@ -1001,7 +1001,7 @@ var _ = Describe("Decider", func() {
|
|||||||
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 0})
|
overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 0})
|
||||||
overrideCtx = request.WithPlayer(overrideCtx, model.Player{MaxBitRate: 0})
|
overrideCtx = request.WithPlayer(overrideCtx, model.Player{MaxBitRate: 0})
|
||||||
|
|
||||||
decision, err := svc.MakeDecision(overrideCtx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(overrideCtx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TargetFormat).To(Equal("mp3"))
|
Expect(decision.TargetFormat).To(Equal("mp3"))
|
||||||
@@ -1018,7 +1018,7 @@ var _ = Describe("Decider", func() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
// No override in context — client profiles used as-is
|
// No override in context — client profiles used as-is
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeTrue())
|
Expect(decision.CanDirectPlay).To(BeTrue())
|
||||||
})
|
})
|
||||||
@@ -1039,7 +1039,7 @@ var _ = Describe("Decider", func() {
|
|||||||
}
|
}
|
||||||
playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 320})
|
playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 320})
|
||||||
|
|
||||||
decision, err := svc.MakeDecision(playerCtx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(playerCtx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
// Source bitrate 1000 > player cap 320, so direct play is not possible
|
// Source bitrate 1000 > player cap 320, so direct play is not possible
|
||||||
Expect(decision.CanDirectPlay).To(BeFalse())
|
Expect(decision.CanDirectPlay).To(BeFalse())
|
||||||
@@ -1060,7 +1060,7 @@ var _ = Describe("Decider", func() {
|
|||||||
}
|
}
|
||||||
playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 500})
|
playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 500})
|
||||||
|
|
||||||
decision, err := svc.MakeDecision(playerCtx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(playerCtx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
// Client limit 256 < player cap 500, so player cap doesn't apply; client limit wins
|
// Client limit 256 < player cap 500, so player cap doesn't apply; client limit wins
|
||||||
@@ -1077,7 +1077,7 @@ var _ = Describe("Decider", func() {
|
|||||||
}
|
}
|
||||||
playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 0})
|
playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 0})
|
||||||
|
|
||||||
decision, err := svc.MakeDecision(playerCtx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(playerCtx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanDirectPlay).To(BeTrue())
|
Expect(decision.CanDirectPlay).To(BeTrue())
|
||||||
})
|
})
|
||||||
@@ -1091,7 +1091,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "opus", AudioCodec: "opus", Protocol: ProtocolHTTP},
|
{Container: "opus", AudioCodec: "opus", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TargetBitrate).To(Equal(96)) // opus default from mock
|
Expect(decision.TargetBitrate).To(Equal(96)) // opus default from mock
|
||||||
@@ -1104,7 +1104,7 @@ var _ = Describe("Decider", func() {
|
|||||||
{Container: "aac", AudioCodec: "aac", Protocol: ProtocolHTTP},
|
{Container: "aac", AudioCodec: "aac", Protocol: ProtocolHTTP},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decision, err := svc.MakeDecision(ctx, mf, ci, DecisionOptions{})
|
decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(decision.CanTranscode).To(BeTrue())
|
Expect(decision.CanTranscode).To(BeTrue())
|
||||||
Expect(decision.TargetBitrate).To(Equal(256)) // aac default from mock
|
Expect(decision.TargetBitrate).To(Equal(256)) // aac default from mock
|
||||||
@@ -1133,7 +1133,7 @@ var _ = Describe("Decider", func() {
|
|||||||
Codec: "mp3", BitRate: 320, SampleRate: 44100, Channels: 2,
|
Codec: "mp3", BitRate: 320, SampleRate: 44100, Channels: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
svc := NewDecider(ds, ff).(*deciderService)
|
svc := NewTranscodeDecider(ds, ff).(*deciderService)
|
||||||
probe, err := svc.ensureProbed(ctx, mf)
|
probe, err := svc.ensureProbed(ctx, mf)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(mf.ProbeData).ToNot(BeEmpty())
|
Expect(mf.ProbeData).ToNot(BeEmpty())
|
||||||
@@ -1154,7 +1154,7 @@ var _ = Describe("Decider", func() {
|
|||||||
// Set error on mock — if ffprobe were called, this would fail
|
// Set error on mock — if ffprobe were called, this would fail
|
||||||
ff.Error = fmt.Errorf("should not be called")
|
ff.Error = fmt.Errorf("should not be called")
|
||||||
|
|
||||||
svc := NewDecider(ds, ff).(*deciderService)
|
svc := NewTranscodeDecider(ds, ff).(*deciderService)
|
||||||
probe, err := svc.ensureProbed(ctx, mf)
|
probe, err := svc.ensureProbed(ctx, mf)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(probe).To(BeNil())
|
Expect(probe).To(BeNil())
|
||||||
@@ -1164,7 +1164,7 @@ var _ = Describe("Decider", func() {
|
|||||||
mf := &model.MediaFile{ID: "probe-3", Suffix: "mp3"}
|
mf := &model.MediaFile{ID: "probe-3", Suffix: "mp3"}
|
||||||
ff.Error = fmt.Errorf("ffprobe not found")
|
ff.Error = fmt.Errorf("ffprobe not found")
|
||||||
|
|
||||||
svc := NewDecider(ds, ff).(*deciderService)
|
svc := NewTranscodeDecider(ds, ff).(*deciderService)
|
||||||
_, err := svc.ensureProbed(ctx, mf)
|
_, err := svc.ensureProbed(ctx, mf)
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(err).To(HaveOccurred())
|
||||||
Expect(err.Error()).To(ContainSubstring("probing media file"))
|
Expect(err.Error()).To(ContainSubstring("probing media file"))
|
||||||
@@ -1179,7 +1179,7 @@ var _ = Describe("Decider", func() {
|
|||||||
// Set a result — if ffprobe were called, ProbeData would be populated
|
// Set a result — if ffprobe were called, ProbeData would be populated
|
||||||
ff.ProbeAudioResult = &ffmpeg.AudioProbeResult{Codec: "mp3"}
|
ff.ProbeAudioResult = &ffmpeg.AudioProbeResult{Codec: "mp3"}
|
||||||
|
|
||||||
svc := NewDecider(ds, ff).(*deciderService)
|
svc := NewTranscodeDecider(ds, ff).(*deciderService)
|
||||||
probe, err := svc.ensureProbed(ctx, mf)
|
probe, err := svc.ensureProbed(ctx, mf)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(probe).To(BeNil())
|
Expect(probe).To(BeNil())
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package transcode
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -46,10 +46,9 @@ func buildLegacyClientInfo(mf *model.MediaFile, reqFormat string, reqBitRate int
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ResolveRequest uses MakeDecision to resolve legacy Subsonic stream parameters
|
// ResolveRequest uses MakeDecision to resolve legacy Subsonic stream parameters
|
||||||
// into a fully specified StreamRequest.
|
// into a fully specified Request.
|
||||||
func (s *deciderService) ResolveRequest(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int, offset int) StreamRequest {
|
func (s *deciderService) ResolveRequest(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int, offset int) Request {
|
||||||
var req StreamRequest
|
var req Request
|
||||||
req.ID = mf.ID
|
|
||||||
req.Offset = offset
|
req.Offset = offset
|
||||||
|
|
||||||
if reqFormat == "raw" {
|
if reqFormat == "raw" {
|
||||||
@@ -58,7 +57,7 @@ func (s *deciderService) ResolveRequest(ctx context.Context, mf *model.MediaFile
|
|||||||
}
|
}
|
||||||
|
|
||||||
clientInfo := buildLegacyClientInfo(mf, reqFormat, reqBitRate)
|
clientInfo := buildLegacyClientInfo(mf, reqFormat, reqBitRate)
|
||||||
decision, err := s.MakeDecision(ctx, mf, clientInfo, DecisionOptions{SkipProbe: true})
|
decision, err := s.MakeDecision(ctx, mf, clientInfo, TranscodeOptions{SkipProbe: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx, "Error making transcode decision, falling back to raw", "id", mf.ID, err)
|
log.Error(ctx, "Error making transcode decision, falling back to raw", "id", mf.ID, err)
|
||||||
req.Format = "raw"
|
req.Format = "raw"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package transcode
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/navidrome/navidrome/conf"
|
"github.com/navidrome/navidrome/conf"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package transcode
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -16,7 +16,7 @@ const (
|
|||||||
|
|
||||||
// checkLimitations checks codec profile limitations against source stream details.
|
// checkLimitations checks codec profile limitations against source stream details.
|
||||||
// Returns "" if all limitations pass, or a typed reason string for the first failure.
|
// Returns "" if all limitations pass, or a typed reason string for the first failure.
|
||||||
func checkLimitations(src *StreamDetails, limitations []Limitation) string {
|
func checkLimitations(src *Details, limitations []Limitation) string {
|
||||||
for _, lim := range limitations {
|
for _, lim := range limitations {
|
||||||
var ok bool
|
var ok bool
|
||||||
var reason string
|
var reason string
|
||||||
@@ -50,7 +50,7 @@ func checkLimitations(src *StreamDetails, limitations []Limitation) string {
|
|||||||
|
|
||||||
// applyLimitation adjusts a transcoded stream parameter to satisfy the limitation.
|
// applyLimitation adjusts a transcoded stream parameter to satisfy the limitation.
|
||||||
// Returns the adjustment result.
|
// Returns the adjustment result.
|
||||||
func applyLimitation(sourceBitrate int, lim *Limitation, ts *StreamDetails) adjustResult {
|
func applyLimitation(sourceBitrate int, lim *Limitation, ts *Details) adjustResult {
|
||||||
switch lim.Name {
|
switch lim.Name {
|
||||||
case LimitationAudioChannels:
|
case LimitationAudioChannels:
|
||||||
return applyIntLimitation(lim.Comparison, lim.Values, ts.Channels, func(v int) { ts.Channels = v })
|
return applyIntLimitation(lim.Comparison, lim.Values, ts.Channels, func(v int) { ts.Channels = v })
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package transcode
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -20,8 +20,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type MediaStreamer interface {
|
type MediaStreamer interface {
|
||||||
NewStream(ctx context.Context, req StreamRequest) (*Stream, error)
|
NewStream(ctx context.Context, mf *model.MediaFile, req Request) (*Stream, error)
|
||||||
DoStream(ctx context.Context, mf *model.MediaFile, req StreamRequest) (*Stream, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TranscodingCache cache.FileCache
|
type TranscodingCache cache.FileCache
|
||||||
@@ -52,16 +51,7 @@ func (j *streamJob) Key() string {
|
|||||||
return fmt.Sprintf("%s.%s.%d.%d.%d.%d.%s.%d", j.mf.ID, j.mf.UpdatedAt.Format(time.RFC3339Nano), j.bitRate, j.sampleRate, j.bitDepth, j.channels, j.format, j.offset)
|
return fmt.Sprintf("%s.%s.%d.%d.%d.%d.%s.%d", j.mf.ID, j.mf.UpdatedAt.Format(time.RFC3339Nano), j.bitRate, j.sampleRate, j.bitDepth, j.channels, j.format, j.offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ms *mediaStreamer) NewStream(ctx context.Context, req StreamRequest) (*Stream, error) {
|
func (ms *mediaStreamer) NewStream(ctx context.Context, mf *model.MediaFile, req Request) (*Stream, error) {
|
||||||
mf, err := ms.ds.MediaFile(ctx).Get(req.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ms.DoStream(ctx, mf, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ms *mediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, req StreamRequest) (*Stream, error) {
|
|
||||||
var format string
|
var format string
|
||||||
var bitRate int
|
var bitRate int
|
||||||
var cached bool
|
var cached bool
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package transcode_test
|
package stream_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/navidrome/navidrome/conf"
|
"github.com/navidrome/navidrome/conf"
|
||||||
"github.com/navidrome/navidrome/conf/configtest"
|
"github.com/navidrome/navidrome/conf/configtest"
|
||||||
"github.com/navidrome/navidrome/core/transcode"
|
"github.com/navidrome/navidrome/core/stream"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
"github.com/navidrome/navidrome/tests"
|
"github.com/navidrome/navidrome/tests"
|
||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var _ = Describe("MediaStreamer", func() {
|
var _ = Describe("MediaStreamer", func() {
|
||||||
var streamer transcode.MediaStreamer
|
var streamer stream.MediaStreamer
|
||||||
var ds model.DataStore
|
var ds model.DataStore
|
||||||
ffmpeg := tests.NewMockFFmpeg("fake data")
|
ffmpeg := tests.NewMockFFmpeg("fake data")
|
||||||
ctx := log.NewContext(context.TODO())
|
ctx := log.NewContext(context.TODO())
|
||||||
@@ -29,39 +29,45 @@ var _ = Describe("MediaStreamer", func() {
|
|||||||
ds.MediaFile(ctx).(*tests.MockMediaFileRepo).SetData(model.MediaFiles{
|
ds.MediaFile(ctx).(*tests.MockMediaFileRepo).SetData(model.MediaFiles{
|
||||||
{ID: "123", Path: "tests/fixtures/test.mp3", Suffix: "mp3", BitRate: 128, Duration: 257.0},
|
{ID: "123", Path: "tests/fixtures/test.mp3", Suffix: "mp3", BitRate: 128, Duration: 257.0},
|
||||||
})
|
})
|
||||||
testCache := transcode.NewTranscodingCache()
|
testCache := stream.NewTranscodingCache()
|
||||||
Eventually(func() bool { return testCache.Available(context.TODO()) }).Should(BeTrue())
|
Eventually(func() bool { return testCache.Available(context.TODO()) }).Should(BeTrue())
|
||||||
streamer = transcode.NewMediaStreamer(ds, ffmpeg, testCache)
|
streamer = stream.NewMediaStreamer(ds, ffmpeg, testCache)
|
||||||
})
|
})
|
||||||
AfterEach(func() {
|
AfterEach(func() {
|
||||||
_ = os.RemoveAll(conf.Server.CacheFolder)
|
_ = os.RemoveAll(conf.Server.CacheFolder)
|
||||||
})
|
})
|
||||||
|
|
||||||
Context("NewStream", func() {
|
Context("NewStream", func() {
|
||||||
|
var mf *model.MediaFile
|
||||||
|
BeforeEach(func() {
|
||||||
|
var err error
|
||||||
|
mf, err = ds.MediaFile(ctx).Get("123")
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
})
|
||||||
It("returns a seekable stream if format is 'raw'", func() {
|
It("returns a seekable stream if format is 'raw'", func() {
|
||||||
s, err := streamer.NewStream(ctx, transcode.StreamRequest{ID: "123", Format: "raw"})
|
s, err := streamer.NewStream(ctx, mf, stream.Request{Format: "raw"})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(s.Seekable()).To(BeTrue())
|
Expect(s.Seekable()).To(BeTrue())
|
||||||
})
|
})
|
||||||
It("returns a seekable stream if no format is specified (direct play)", func() {
|
It("returns a seekable stream if no format is specified (direct play)", func() {
|
||||||
s, err := streamer.NewStream(ctx, transcode.StreamRequest{ID: "123"})
|
s, err := streamer.NewStream(ctx, mf, stream.Request{})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(s.Seekable()).To(BeTrue())
|
Expect(s.Seekable()).To(BeTrue())
|
||||||
})
|
})
|
||||||
It("returns a NON seekable stream if transcode is required", func() {
|
It("returns a NON seekable stream if transcode is required", func() {
|
||||||
s, err := streamer.NewStream(ctx, transcode.StreamRequest{ID: "123", Format: "mp3", BitRate: 64})
|
s, err := streamer.NewStream(ctx, mf, stream.Request{Format: "mp3", BitRate: 64})
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
Expect(s.Seekable()).To(BeFalse())
|
Expect(s.Seekable()).To(BeFalse())
|
||||||
Expect(s.Duration()).To(Equal(float32(257.0)))
|
Expect(s.Duration()).To(Equal(float32(257.0)))
|
||||||
})
|
})
|
||||||
It("returns a seekable stream if the file is complete in the cache", func() {
|
It("returns a seekable stream if the file is complete in the cache", func() {
|
||||||
s, err := streamer.NewStream(ctx, transcode.StreamRequest{ID: "123", Format: "mp3", BitRate: 32})
|
s, err := streamer.NewStream(ctx, mf, stream.Request{Format: "mp3", BitRate: 32})
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
_, _ = io.ReadAll(s)
|
_, _ = io.ReadAll(s)
|
||||||
_ = s.Close()
|
_ = s.Close()
|
||||||
Eventually(func() bool { return ffmpeg.IsClosed() }, "3s").Should(BeTrue())
|
Eventually(func() bool { return ffmpeg.IsClosed() }, "3s").Should(BeTrue())
|
||||||
|
|
||||||
s, err = streamer.NewStream(ctx, transcode.StreamRequest{ID: "123", Format: "mp3", BitRate: 32})
|
s, err = streamer.NewStream(ctx, mf, stream.Request{Format: "mp3", BitRate: 32})
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
Expect(s.Seekable()).To(BeTrue())
|
Expect(s.Seekable()).To(BeTrue())
|
||||||
})
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package transcode
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTranscode(t *testing.T) {
|
func TestStream(t *testing.T) {
|
||||||
tests.Init(t, false)
|
tests.Init(t, false)
|
||||||
log.SetLevel(log.LevelFatal)
|
log.SetLevel(log.LevelFatal)
|
||||||
RegisterFailHandler(Fail)
|
RegisterFailHandler(Fail)
|
||||||
RunSpecs(t, "Transcode Suite")
|
RunSpecs(t, "Stream Suite")
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package transcode
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
const tokenTTL = 12 * time.Hour
|
const tokenTTL = 48 * time.Hour
|
||||||
|
|
||||||
// params contains the parameters extracted from a transcode token.
|
// params contains the parameters extracted from a transcode token.
|
||||||
// TargetBitrate is in kilobits per second (kbps).
|
// TargetBitrate is in kilobits per second (kbps).
|
||||||
@@ -29,7 +29,7 @@ type params struct {
|
|||||||
|
|
||||||
// toClaimsMap converts a Decision into a JWT claims map for token encoding.
|
// toClaimsMap converts a Decision into a JWT claims map for token encoding.
|
||||||
// Only non-zero transcode fields are included.
|
// Only non-zero transcode fields are included.
|
||||||
func (d *Decision) toClaimsMap() map[string]any {
|
func (d *TranscodeDecision) toClaimsMap() map[string]any {
|
||||||
m := map[string]any{
|
m := map[string]any{
|
||||||
"mid": d.MediaID,
|
"mid": d.MediaID,
|
||||||
"ua": d.SourceUpdatedAt.Truncate(time.Second).Unix(),
|
"ua": d.SourceUpdatedAt.Truncate(time.Second).Unix(),
|
||||||
@@ -110,7 +110,7 @@ func getIntClaim(token jwt.Token, key string) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *deciderService) CreateTranscodeParams(decision *Decision) (string, error) {
|
func (s *deciderService) CreateTranscodeParams(decision *TranscodeDecision) (string, error) {
|
||||||
return auth.EncodeToken(decision.toClaimsMap())
|
return auth.EncodeToken(decision.toClaimsMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,28 +122,21 @@ func (s *deciderService) parseTranscodeParams(tokenStr string) (*params, error)
|
|||||||
return paramsFromToken(token)
|
return paramsFromToken(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *deciderService) ResolveRequestFromToken(ctx context.Context, token string, mediaID string, offset int) (StreamRequest, *model.MediaFile, error) {
|
func (s *deciderService) ResolveRequestFromToken(ctx context.Context, token string, mf *model.MediaFile, offset int) (Request, error) {
|
||||||
p, err := s.parseTranscodeParams(token)
|
p, err := s.parseTranscodeParams(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return StreamRequest{}, nil, errors.Join(ErrTokenInvalid, err)
|
return Request{}, errors.Join(ErrTokenInvalid, err)
|
||||||
}
|
}
|
||||||
if p.MediaID != mediaID {
|
if p.MediaID != mf.ID {
|
||||||
return StreamRequest{}, nil, fmt.Errorf("%w: token mediaID %q does not match %q", ErrTokenInvalid, p.MediaID, mediaID)
|
return Request{}, fmt.Errorf("%w: token mediaID %q does not match %q", ErrTokenInvalid, p.MediaID, mf.ID)
|
||||||
}
|
|
||||||
mf, err := s.ds.MediaFile(ctx).Get(mediaID)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, model.ErrNotFound) {
|
|
||||||
return StreamRequest{}, nil, ErrMediaNotFound
|
|
||||||
}
|
|
||||||
return StreamRequest{}, nil, err
|
|
||||||
}
|
}
|
||||||
if !mf.UpdatedAt.Truncate(time.Second).Equal(p.SourceUpdatedAt) {
|
if !mf.UpdatedAt.Truncate(time.Second).Equal(p.SourceUpdatedAt) {
|
||||||
log.Info(ctx, "Transcode token is stale", "mediaID", mediaID,
|
log.Info(ctx, "Transcode token is stale", "mediaID", mf.ID,
|
||||||
"tokenUpdatedAt", p.SourceUpdatedAt, "fileUpdatedAt", mf.UpdatedAt)
|
"tokenUpdatedAt", p.SourceUpdatedAt, "fileUpdatedAt", mf.UpdatedAt)
|
||||||
return StreamRequest{}, nil, ErrTokenStale
|
return Request{}, ErrTokenStale
|
||||||
}
|
}
|
||||||
|
|
||||||
req := StreamRequest{ID: mediaID, Offset: offset}
|
req := Request{Offset: offset}
|
||||||
if !p.DirectPlay && p.TargetFormat != "" {
|
if !p.DirectPlay && p.TargetFormat != "" {
|
||||||
req.Format = p.TargetFormat
|
req.Format = p.TargetFormat
|
||||||
req.BitRate = p.TargetBitrate
|
req.BitRate = p.TargetBitrate
|
||||||
@@ -151,5 +144,5 @@ func (s *deciderService) ResolveRequestFromToken(ctx context.Context, token stri
|
|||||||
req.BitDepth = p.TargetBitDepth
|
req.BitDepth = p.TargetBitDepth
|
||||||
req.Channels = p.TargetChannels
|
req.Channels = p.TargetChannels
|
||||||
}
|
}
|
||||||
return req, mf, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package transcode
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -16,7 +16,7 @@ var _ = Describe("Token", func() {
|
|||||||
var (
|
var (
|
||||||
ds *tests.MockDataStore
|
ds *tests.MockDataStore
|
||||||
ff *tests.MockFFmpeg
|
ff *tests.MockFFmpeg
|
||||||
svc Decider
|
svc TranscodeDecider
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ var _ = Describe("Token", func() {
|
|||||||
}
|
}
|
||||||
ff = tests.NewMockFFmpeg("")
|
ff = tests.NewMockFFmpeg("")
|
||||||
auth.Init(ds)
|
auth.Init(ds)
|
||||||
svc = NewDecider(ds, ff)
|
svc = NewTranscodeDecider(ds, ff)
|
||||||
})
|
})
|
||||||
|
|
||||||
Describe("Token round-trip", func() {
|
Describe("Token round-trip", func() {
|
||||||
@@ -43,7 +43,7 @@ var _ = Describe("Token", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("creates and parses a direct play token", func() {
|
It("creates and parses a direct play token", func() {
|
||||||
decision := &Decision{
|
decision := &TranscodeDecision{
|
||||||
MediaID: "media-123",
|
MediaID: "media-123",
|
||||||
CanDirectPlay: true,
|
CanDirectPlay: true,
|
||||||
SourceUpdatedAt: sourceTime,
|
SourceUpdatedAt: sourceTime,
|
||||||
@@ -61,7 +61,7 @@ var _ = Describe("Token", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("creates and parses a transcode token with kbps bitrate", func() {
|
It("creates and parses a transcode token with kbps bitrate", func() {
|
||||||
decision := &Decision{
|
decision := &TranscodeDecision{
|
||||||
MediaID: "media-456",
|
MediaID: "media-456",
|
||||||
CanDirectPlay: false,
|
CanDirectPlay: false,
|
||||||
CanTranscode: true,
|
CanTranscode: true,
|
||||||
@@ -84,7 +84,7 @@ var _ = Describe("Token", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("creates and parses a transcode token with sample rate", func() {
|
It("creates and parses a transcode token with sample rate", func() {
|
||||||
decision := &Decision{
|
decision := &TranscodeDecision{
|
||||||
MediaID: "media-789",
|
MediaID: "media-789",
|
||||||
CanDirectPlay: false,
|
CanDirectPlay: false,
|
||||||
CanTranscode: true,
|
CanTranscode: true,
|
||||||
@@ -107,7 +107,7 @@ var _ = Describe("Token", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("creates and parses a transcode token with bit depth", func() {
|
It("creates and parses a transcode token with bit depth", func() {
|
||||||
decision := &Decision{
|
decision := &TranscodeDecision{
|
||||||
MediaID: "media-bd",
|
MediaID: "media-bd",
|
||||||
CanDirectPlay: false,
|
CanDirectPlay: false,
|
||||||
CanTranscode: true,
|
CanTranscode: true,
|
||||||
@@ -127,7 +127,7 @@ var _ = Describe("Token", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("omits bit depth from token when 0", func() {
|
It("omits bit depth from token when 0", func() {
|
||||||
decision := &Decision{
|
decision := &TranscodeDecision{
|
||||||
MediaID: "media-nobd",
|
MediaID: "media-nobd",
|
||||||
CanDirectPlay: false,
|
CanDirectPlay: false,
|
||||||
CanTranscode: true,
|
CanTranscode: true,
|
||||||
@@ -145,7 +145,7 @@ var _ = Describe("Token", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("omits sample rate from token when 0", func() {
|
It("omits sample rate from token when 0", func() {
|
||||||
decision := &Decision{
|
decision := &TranscodeDecision{
|
||||||
MediaID: "media-100",
|
MediaID: "media-100",
|
||||||
CanDirectPlay: false,
|
CanDirectPlay: false,
|
||||||
CanTranscode: true,
|
CanTranscode: true,
|
||||||
@@ -164,7 +164,7 @@ var _ = Describe("Token", func() {
|
|||||||
|
|
||||||
It("truncates SourceUpdatedAt to seconds", func() {
|
It("truncates SourceUpdatedAt to seconds", func() {
|
||||||
timeWithNanos := time.Date(2025, 6, 15, 10, 30, 0, 123456789, time.UTC)
|
timeWithNanos := time.Date(2025, 6, 15, 10, 30, 0, 123456789, time.UTC)
|
||||||
decision := &Decision{
|
decision := &TranscodeDecision{
|
||||||
MediaID: "media-trunc",
|
MediaID: "media-trunc",
|
||||||
CanDirectPlay: true,
|
CanDirectPlay: true,
|
||||||
SourceUpdatedAt: timeWithNanos,
|
SourceUpdatedAt: timeWithNanos,
|
||||||
@@ -184,19 +184,14 @@ var _ = Describe("Token", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Describe("ResolveRequestFromToken", func() {
|
Describe("ResolveRequestFromToken", func() {
|
||||||
var (
|
var sourceTime time.Time
|
||||||
mockMFRepo *tests.MockMediaFileRepo
|
|
||||||
sourceTime time.Time
|
|
||||||
)
|
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
sourceTime = time.Date(2025, 6, 15, 10, 30, 0, 0, time.UTC)
|
sourceTime = time.Date(2025, 6, 15, 10, 30, 0, 0, time.UTC)
|
||||||
mockMFRepo = &tests.MockMediaFileRepo{}
|
|
||||||
ds.MockedMediaFile = mockMFRepo
|
|
||||||
})
|
})
|
||||||
|
|
||||||
createTokenForMedia := func(mediaID string, updatedAt time.Time) string {
|
createTokenForMedia := func(mediaID string, updatedAt time.Time) string {
|
||||||
decision := &Decision{
|
decision := &TranscodeDecision{
|
||||||
MediaID: mediaID,
|
MediaID: mediaID,
|
||||||
CanDirectPlay: true,
|
CanDirectPlay: true,
|
||||||
SourceUpdatedAt: updatedAt,
|
SourceUpdatedAt: updatedAt,
|
||||||
@@ -206,46 +201,35 @@ var _ = Describe("Token", func() {
|
|||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
It("returns stream request and media file for valid token", func() {
|
It("returns stream request for valid token", func() {
|
||||||
mockMFRepo.SetData(model.MediaFiles{
|
mf := &model.MediaFile{ID: "song-1", UpdatedAt: sourceTime}
|
||||||
{ID: "song-1", UpdatedAt: sourceTime},
|
|
||||||
})
|
|
||||||
token := createTokenForMedia("song-1", sourceTime)
|
token := createTokenForMedia("song-1", sourceTime)
|
||||||
|
|
||||||
req, mf, err := svc.ResolveRequestFromToken(ctx, token, "song-1", 0)
|
req, err := svc.ResolveRequestFromToken(ctx, token, mf, 0)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(req.ID).To(Equal("song-1"))
|
|
||||||
Expect(req.Format).To(BeEmpty()) // direct play has no target format
|
Expect(req.Format).To(BeEmpty()) // direct play has no target format
|
||||||
Expect(mf.ID).To(Equal("song-1"))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
It("returns ErrTokenInvalid for invalid token", func() {
|
It("returns ErrTokenInvalid for invalid token", func() {
|
||||||
_, _, err := svc.ResolveRequestFromToken(ctx, "bad-token", "song-1", 0)
|
mf := &model.MediaFile{ID: "song-1", UpdatedAt: sourceTime}
|
||||||
|
_, err := svc.ResolveRequestFromToken(ctx, "bad-token", mf, 0)
|
||||||
Expect(err).To(MatchError(ContainSubstring(ErrTokenInvalid.Error())))
|
Expect(err).To(MatchError(ContainSubstring(ErrTokenInvalid.Error())))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("returns ErrTokenInvalid when mediaID does not match token", func() {
|
It("returns ErrTokenInvalid when mediaID does not match token", func() {
|
||||||
|
mf := &model.MediaFile{ID: "song-2", UpdatedAt: sourceTime}
|
||||||
token := createTokenForMedia("song-1", sourceTime)
|
token := createTokenForMedia("song-1", sourceTime)
|
||||||
|
|
||||||
_, _, err := svc.ResolveRequestFromToken(ctx, token, "song-2", 0)
|
_, err := svc.ResolveRequestFromToken(ctx, token, mf, 0)
|
||||||
Expect(err).To(MatchError(ContainSubstring(ErrTokenInvalid.Error())))
|
Expect(err).To(MatchError(ContainSubstring(ErrTokenInvalid.Error())))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("returns ErrMediaNotFound when media file does not exist", func() {
|
|
||||||
token := createTokenForMedia("gone-id", sourceTime)
|
|
||||||
|
|
||||||
_, _, err := svc.ResolveRequestFromToken(ctx, token, "gone-id", 0)
|
|
||||||
Expect(err).To(MatchError(ErrMediaNotFound))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("returns ErrTokenStale when media file has changed", func() {
|
It("returns ErrTokenStale when media file has changed", func() {
|
||||||
newTime := sourceTime.Add(1 * time.Hour)
|
newTime := sourceTime.Add(1 * time.Hour)
|
||||||
mockMFRepo.SetData(model.MediaFiles{
|
mf := &model.MediaFile{ID: "song-1", UpdatedAt: newTime}
|
||||||
{ID: "song-1", UpdatedAt: newTime},
|
|
||||||
})
|
|
||||||
token := createTokenForMedia("song-1", sourceTime)
|
token := createTokenForMedia("song-1", sourceTime)
|
||||||
|
|
||||||
_, _, err := svc.ResolveRequestFromToken(ctx, token, "song-1", 0)
|
_, err := svc.ResolveRequestFromToken(ctx, token, mf, 0)
|
||||||
Expect(err).To(MatchError(ErrTokenStale))
|
Expect(err).To(MatchError(ErrTokenStale))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package transcode
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -7,20 +7,18 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
ErrTokenInvalid = errors.New("invalid or expired transcode token")
|
ErrTokenInvalid = errors.New("invalid or expired transcode token")
|
||||||
ErrMediaNotFound = errors.New("media file not found")
|
|
||||||
ErrTokenStale = errors.New("transcode token is stale: media file has changed")
|
ErrTokenStale = errors.New("transcode token is stale: media file has changed")
|
||||||
)
|
)
|
||||||
|
|
||||||
// DecisionOptions controls optional behavior of MakeDecision.
|
// TranscodeOptions controls optional behavior of MakeTranscodeDecision.
|
||||||
type DecisionOptions struct {
|
type TranscodeOptions struct {
|
||||||
// SkipProbe prevents MakeDecision from running ffprobe on the media file.
|
// SkipProbe prevents MakeTranscodeDecision from running ffprobe on the media file.
|
||||||
// When true, source stream details are derived from tag metadata only.
|
// When true, source stream details are derived from tag metadata only.
|
||||||
SkipProbe bool
|
SkipProbe bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamRequest contains the resolved parameters for creating a media stream.
|
// Request contains the resolved parameters for creating a media stream.
|
||||||
type StreamRequest struct {
|
type Request struct {
|
||||||
ID string
|
|
||||||
Format string
|
Format string
|
||||||
BitRate int // kbps
|
BitRate int // kbps
|
||||||
SampleRate int
|
SampleRate int
|
||||||
@@ -100,9 +98,9 @@ const (
|
|||||||
CodecProfileTypeAudio = "AudioCodec"
|
CodecProfileTypeAudio = "AudioCodec"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Decision represents the internal decision result.
|
// TranscodeDecision represents the internal decision result.
|
||||||
// All bitrate values are in kilobits per second (kbps).
|
// All bitrate values are in kilobits per second (kbps).
|
||||||
type Decision struct {
|
type TranscodeDecision struct {
|
||||||
MediaID string
|
MediaID string
|
||||||
CanDirectPlay bool
|
CanDirectPlay bool
|
||||||
CanTranscode bool
|
CanTranscode bool
|
||||||
@@ -113,14 +111,14 @@ type Decision struct {
|
|||||||
TargetChannels int
|
TargetChannels int
|
||||||
TargetSampleRate int
|
TargetSampleRate int
|
||||||
TargetBitDepth int
|
TargetBitDepth int
|
||||||
SourceStream StreamDetails
|
SourceStream Details
|
||||||
SourceUpdatedAt time.Time
|
SourceUpdatedAt time.Time
|
||||||
TranscodeStream *StreamDetails
|
TranscodeStream *Details
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamDetails describes audio stream properties.
|
// Details describes audio stream properties.
|
||||||
// Bitrate is in kilobits per second (kbps).
|
// Bitrate is in kilobits per second (kbps).
|
||||||
type StreamDetails struct {
|
type Details struct {
|
||||||
Container string
|
Container string
|
||||||
Codec string
|
Codec string
|
||||||
Profile string // Audio profile (e.g., "LC", "HE-AACv2"). Populated from ffprobe data.
|
Profile string // Audio profile (e.g., "LC", "HE-AACv2"). Populated from ffprobe data.
|
||||||
@@ -10,12 +10,12 @@ import (
|
|||||||
"github.com/navidrome/navidrome/core/playback"
|
"github.com/navidrome/navidrome/core/playback"
|
||||||
"github.com/navidrome/navidrome/core/playlists"
|
"github.com/navidrome/navidrome/core/playlists"
|
||||||
"github.com/navidrome/navidrome/core/scrobbler"
|
"github.com/navidrome/navidrome/core/scrobbler"
|
||||||
"github.com/navidrome/navidrome/core/transcode"
|
"github.com/navidrome/navidrome/core/stream"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Set = wire.NewSet(
|
var Set = wire.NewSet(
|
||||||
transcode.NewMediaStreamer,
|
stream.NewMediaStreamer,
|
||||||
transcode.GetTranscodingCache,
|
stream.GetTranscodingCache,
|
||||||
NewArchiver,
|
NewArchiver,
|
||||||
NewPlayers,
|
NewPlayers,
|
||||||
NewShare,
|
NewShare,
|
||||||
@@ -23,7 +23,7 @@ var Set = wire.NewSet(
|
|||||||
NewLibrary,
|
NewLibrary,
|
||||||
NewUser,
|
NewUser,
|
||||||
NewMaintenance,
|
NewMaintenance,
|
||||||
transcode.NewDecider,
|
stream.NewTranscodeDecider,
|
||||||
agents.GetAgents,
|
agents.GetAgents,
|
||||||
external.NewProvider,
|
external.NewProvider,
|
||||||
wire.Bind(new(external.Agents), new(*agents.Agents)),
|
wire.Bind(new(external.Agents), new(*agents.Agents)),
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import (
|
|||||||
"github.com/navidrome/navidrome/core/playlists"
|
"github.com/navidrome/navidrome/core/playlists"
|
||||||
"github.com/navidrome/navidrome/core/scrobbler"
|
"github.com/navidrome/navidrome/core/scrobbler"
|
||||||
"github.com/navidrome/navidrome/core/storage/storagetest"
|
"github.com/navidrome/navidrome/core/storage/storagetest"
|
||||||
"github.com/navidrome/navidrome/core/transcode"
|
"github.com/navidrome/navidrome/core/stream"
|
||||||
"github.com/navidrome/navidrome/db"
|
"github.com/navidrome/navidrome/db"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
@@ -284,25 +284,21 @@ func (n noopArtwork) GetOrPlaceholder(_ context.Context, _ string, _ int, _ bool
|
|||||||
return io.NopCloser(io.LimitReader(nil, 0)), time.Time{}, nil
|
return io.NopCloser(io.LimitReader(nil, 0)), time.Time{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// spyStreamer captures the StreamRequest passed to DoStream for test assertions,
|
// spyStreamer captures the Request passed to NewStream for test assertions,
|
||||||
// then returns a minimal fake Stream so the handler completes without error.
|
// then returns a minimal fake Stream so the handler completes without error.
|
||||||
type spyStreamer struct {
|
type spyStreamer struct {
|
||||||
LastRequest transcode.StreamRequest
|
LastRequest stream.Request
|
||||||
LastMediaFile *model.MediaFile
|
LastMediaFile *model.MediaFile
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *spyStreamer) NewStream(ctx context.Context, req transcode.StreamRequest) (*transcode.Stream, error) {
|
func (s *spyStreamer) NewStream(_ context.Context, mf *model.MediaFile, req stream.Request) (*stream.Stream, error) {
|
||||||
return nil, model.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *spyStreamer) DoStream(_ context.Context, mf *model.MediaFile, req transcode.StreamRequest) (*transcode.Stream, error) {
|
|
||||||
s.LastRequest = req
|
s.LastRequest = req
|
||||||
s.LastMediaFile = mf
|
s.LastMediaFile = mf
|
||||||
format := req.Format
|
format := req.Format
|
||||||
if format == "" || format == "raw" {
|
if format == "" || format == "raw" {
|
||||||
format = mf.Suffix
|
format = mf.Suffix
|
||||||
}
|
}
|
||||||
return transcode.NewTestStream(mf, format, req.BitRate), nil
|
return stream.NewTestStream(mf, format, req.BitRate), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// noopFFmpeg implements ffmpeg.FFmpeg with no-op methods.
|
// noopFFmpeg implements ffmpeg.FFmpeg with no-op methods.
|
||||||
@@ -392,7 +388,7 @@ func (n noopPlayTracker) Submit(context.Context, []scrobbler.Submission) error {
|
|||||||
// Compile-time interface checks
|
// Compile-time interface checks
|
||||||
var (
|
var (
|
||||||
_ artwork.Artwork = noopArtwork{}
|
_ artwork.Artwork = noopArtwork{}
|
||||||
_ transcode.MediaStreamer = &spyStreamer{}
|
_ stream.MediaStreamer = &spyStreamer{}
|
||||||
_ core.Archiver = noopArchiver{}
|
_ core.Archiver = noopArchiver{}
|
||||||
_ external.Provider = noopProvider{}
|
_ external.Provider = noopProvider{}
|
||||||
_ scrobbler.PlayTracker = noopPlayTracker{}
|
_ scrobbler.PlayTracker = noopPlayTracker{}
|
||||||
@@ -477,7 +473,7 @@ func setupTestDB() {
|
|||||||
|
|
||||||
// Create the Subsonic Router with real DS, spy streamer, and real Decider
|
// Create the Subsonic Router with real DS, spy streamer, and real Decider
|
||||||
spy = &spyStreamer{}
|
spy = &spyStreamer{}
|
||||||
decider := transcode.NewDecider(ds, noopFFmpeg{})
|
decider := stream.NewTranscodeDecider(ds, noopFFmpeg{})
|
||||||
s := scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
s := scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
||||||
playlists.NewPlaylists(ds), metrics.NewNoopInstance())
|
playlists.NewPlaylists(ds), metrics.NewNoopInstance())
|
||||||
router = subsonic.New(
|
router = subsonic.New(
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/core/auth"
|
"github.com/navidrome/navidrome/core/auth"
|
||||||
"github.com/navidrome/navidrome/core/transcode"
|
"github.com/navidrome/navidrome/core/stream"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
"github.com/navidrome/navidrome/utils/req"
|
"github.com/navidrome/navidrome/utils/req"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,8 +24,19 @@ func (pub *Router) handleStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stream, err := pub.streamer.NewStream(ctx, transcode.StreamRequest{
|
mf, err := pub.ds.MediaFile(ctx).Get(info.id)
|
||||||
ID: info.id, Format: info.format, BitRate: info.bitrate,
|
if err != nil {
|
||||||
|
if errors.Is(err, model.ErrNotFound) {
|
||||||
|
http.Error(w, "not found", http.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
log.Error(ctx, "Error retrieving media file for shared stream", "id", info.id, err)
|
||||||
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stream, err := pub.streamer.NewStream(ctx, mf, stream.Request{
|
||||||
|
Format: info.format, BitRate: info.bitrate,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx, "Error starting shared stream", err)
|
log.Error(ctx, "Error starting shared stream", err)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/navidrome/navidrome/core"
|
"github.com/navidrome/navidrome/core"
|
||||||
"github.com/navidrome/navidrome/core/artwork"
|
"github.com/navidrome/navidrome/core/artwork"
|
||||||
"github.com/navidrome/navidrome/core/publicurl"
|
"github.com/navidrome/navidrome/core/publicurl"
|
||||||
"github.com/navidrome/navidrome/core/transcode"
|
"github.com/navidrome/navidrome/core/stream"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
"github.com/navidrome/navidrome/server"
|
"github.com/navidrome/navidrome/server"
|
||||||
@@ -21,14 +21,14 @@ import (
|
|||||||
type Router struct {
|
type Router struct {
|
||||||
http.Handler
|
http.Handler
|
||||||
artwork artwork.Artwork
|
artwork artwork.Artwork
|
||||||
streamer transcode.MediaStreamer
|
streamer stream.MediaStreamer
|
||||||
archiver core.Archiver
|
archiver core.Archiver
|
||||||
share core.Share
|
share core.Share
|
||||||
assetsHandler http.Handler
|
assetsHandler http.Handler
|
||||||
ds model.DataStore
|
ds model.DataStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ds model.DataStore, artwork artwork.Artwork, streamer transcode.MediaStreamer, share core.Share, archiver core.Archiver) *Router {
|
func New(ds model.DataStore, artwork artwork.Artwork, streamer stream.MediaStreamer, share core.Share, archiver core.Archiver) *Router {
|
||||||
p := &Router{ds: ds, artwork: artwork, streamer: streamer, share: share, archiver: archiver}
|
p := &Router{ds: ds, artwork: artwork, streamer: streamer, share: share, archiver: archiver}
|
||||||
shareRoot := path.Join(conf.Server.BasePath, consts.URLPathPublic)
|
shareRoot := path.Join(conf.Server.BasePath, consts.URLPathPublic)
|
||||||
p.assetsHandler = http.StripPrefix(shareRoot, http.FileServer(http.FS(ui.BuildAssets())))
|
p.assetsHandler = http.StripPrefix(shareRoot, http.FileServer(http.FS(ui.BuildAssets())))
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
"github.com/navidrome/navidrome/core/playback"
|
"github.com/navidrome/navidrome/core/playback"
|
||||||
playlistsvc "github.com/navidrome/navidrome/core/playlists"
|
playlistsvc "github.com/navidrome/navidrome/core/playlists"
|
||||||
"github.com/navidrome/navidrome/core/scrobbler"
|
"github.com/navidrome/navidrome/core/scrobbler"
|
||||||
"github.com/navidrome/navidrome/core/transcode"
|
"github.com/navidrome/navidrome/core/stream"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
"github.com/navidrome/navidrome/server"
|
"github.com/navidrome/navidrome/server"
|
||||||
@@ -39,7 +39,7 @@ type Router struct {
|
|||||||
http.Handler
|
http.Handler
|
||||||
ds model.DataStore
|
ds model.DataStore
|
||||||
artwork artwork.Artwork
|
artwork artwork.Artwork
|
||||||
streamer transcode.MediaStreamer
|
streamer stream.MediaStreamer
|
||||||
archiver core.Archiver
|
archiver core.Archiver
|
||||||
players core.Players
|
players core.Players
|
||||||
provider external.Provider
|
provider external.Provider
|
||||||
@@ -51,13 +51,13 @@ type Router struct {
|
|||||||
playback playback.PlaybackServer
|
playback playback.PlaybackServer
|
||||||
metrics metrics.Metrics
|
metrics metrics.Metrics
|
||||||
lyrics lyricssvc.Lyrics
|
lyrics lyricssvc.Lyrics
|
||||||
transcodeDecision transcode.Decider
|
transcodeDecision stream.TranscodeDecider
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ds model.DataStore, artwork artwork.Artwork, streamer transcode.MediaStreamer, archiver core.Archiver,
|
func New(ds model.DataStore, artwork artwork.Artwork, streamer stream.MediaStreamer, archiver core.Archiver,
|
||||||
players core.Players, provider external.Provider, scanner model.Scanner, broker events.Broker,
|
players core.Players, provider external.Provider, scanner model.Scanner, broker events.Broker,
|
||||||
playlists playlistsvc.Playlists, scrobbler scrobbler.PlayTracker, share core.Share, playback playback.PlaybackServer,
|
playlists playlistsvc.Playlists, scrobbler scrobbler.PlayTracker, share core.Share, playback playback.PlaybackServer,
|
||||||
metrics metrics.Metrics, lyrics lyricssvc.Lyrics, transcodeDecision transcode.Decider,
|
metrics metrics.Metrics, lyrics lyricssvc.Lyrics, transcodeDecision stream.TranscodeDecider,
|
||||||
) *Router {
|
) *Router {
|
||||||
r := &Router{
|
r := &Router{
|
||||||
ds: ds,
|
ds: ds,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/conf"
|
"github.com/navidrome/navidrome/conf"
|
||||||
"github.com/navidrome/navidrome/core/transcode"
|
"github.com/navidrome/navidrome/core/stream"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
"github.com/navidrome/navidrome/model/request"
|
"github.com/navidrome/navidrome/model/request"
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"github.com/navidrome/navidrome/utils/req"
|
"github.com/navidrome/navidrome/utils/req"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (api *Router) serveStream(ctx context.Context, w http.ResponseWriter, r *http.Request, stream *transcode.Stream, id string) {
|
func (api *Router) serveStream(ctx context.Context, w http.ResponseWriter, r *http.Request, stream *stream.Stream, id string) {
|
||||||
if stream.Seekable() {
|
if stream.Seekable() {
|
||||||
http.ServeContent(w, r, stream.Name(), stream.ModTime(), stream)
|
http.ServeContent(w, r, stream.Name(), stream.ModTime(), stream)
|
||||||
} else {
|
} else {
|
||||||
@@ -66,7 +66,7 @@ func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Su
|
|||||||
}
|
}
|
||||||
|
|
||||||
streamReq := api.transcodeDecision.ResolveRequest(ctx, mf, format, maxBitRate, timeOffset)
|
streamReq := api.transcodeDecision.ResolveRequest(ctx, mf, format, maxBitRate, timeOffset)
|
||||||
stream, err := api.streamer.DoStream(ctx, mf, streamReq)
|
stream, err := api.streamer.NewStream(ctx, mf, streamReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ func (api *Router) Download(w http.ResponseWriter, r *http.Request) (*responses.
|
|||||||
switch v := entity.(type) {
|
switch v := entity.(type) {
|
||||||
case *model.MediaFile:
|
case *model.MediaFile:
|
||||||
streamReq := api.transcodeDecision.ResolveRequest(ctx, v, format, maxBitRate, 0)
|
streamReq := api.transcodeDecision.ResolveRequest(ctx, v, format, maxBitRate, 0)
|
||||||
stream, err := api.streamer.DoStream(ctx, v, streamReq)
|
stream, err := api.streamer.NewStream(ctx, v, streamReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/core/transcode"
|
"github.com/navidrome/navidrome/core/stream"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||||
@@ -59,10 +59,10 @@ type limitationRequest struct {
|
|||||||
Required bool `json:"required,omitempty"`
|
Required bool `json:"required,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// toCoreClientInfo converts the API request struct to the transcode.ClientInfo struct.
|
// toCoreClientInfo converts the API request struct to the stream.ClientInfo struct.
|
||||||
// The OpenSubsonic spec uses bps for bitrate values; core uses kbps.
|
// The OpenSubsonic spec uses bps for bitrate values; core uses kbps.
|
||||||
func (r *clientInfoRequest) toCoreClientInfo() *transcode.ClientInfo {
|
func (r *clientInfoRequest) toCoreClientInfo() *stream.ClientInfo {
|
||||||
ci := &transcode.ClientInfo{
|
ci := &stream.ClientInfo{
|
||||||
Name: r.Name,
|
Name: r.Name,
|
||||||
Platform: r.Platform,
|
Platform: r.Platform,
|
||||||
MaxAudioBitrate: bpsToKbps(r.MaxAudioBitrate),
|
MaxAudioBitrate: bpsToKbps(r.MaxAudioBitrate),
|
||||||
@@ -70,7 +70,7 @@ func (r *clientInfoRequest) toCoreClientInfo() *transcode.ClientInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, dp := range r.DirectPlayProfiles {
|
for _, dp := range r.DirectPlayProfiles {
|
||||||
ci.DirectPlayProfiles = append(ci.DirectPlayProfiles, transcode.DirectPlayProfile{
|
ci.DirectPlayProfiles = append(ci.DirectPlayProfiles, stream.DirectPlayProfile{
|
||||||
Containers: dp.Containers,
|
Containers: dp.Containers,
|
||||||
AudioCodecs: dp.AudioCodecs,
|
AudioCodecs: dp.AudioCodecs,
|
||||||
Protocols: dp.Protocols,
|
Protocols: dp.Protocols,
|
||||||
@@ -79,7 +79,7 @@ func (r *clientInfoRequest) toCoreClientInfo() *transcode.ClientInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tp := range r.TranscodingProfiles {
|
for _, tp := range r.TranscodingProfiles {
|
||||||
ci.TranscodingProfiles = append(ci.TranscodingProfiles, transcode.Profile{
|
ci.TranscodingProfiles = append(ci.TranscodingProfiles, stream.Profile{
|
||||||
Container: tp.Container,
|
Container: tp.Container,
|
||||||
AudioCodec: tp.AudioCodec,
|
AudioCodec: tp.AudioCodec,
|
||||||
Protocol: tp.Protocol,
|
Protocol: tp.Protocol,
|
||||||
@@ -88,19 +88,19 @@ func (r *clientInfoRequest) toCoreClientInfo() *transcode.ClientInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, cp := range r.CodecProfiles {
|
for _, cp := range r.CodecProfiles {
|
||||||
coreCP := transcode.CodecProfile{
|
coreCP := stream.CodecProfile{
|
||||||
Type: cp.Type,
|
Type: cp.Type,
|
||||||
Name: cp.Name,
|
Name: cp.Name,
|
||||||
}
|
}
|
||||||
for _, lim := range cp.Limitations {
|
for _, lim := range cp.Limitations {
|
||||||
coreLim := transcode.Limitation{
|
coreLim := stream.Limitation{
|
||||||
Name: lim.Name,
|
Name: lim.Name,
|
||||||
Comparison: lim.Comparison,
|
Comparison: lim.Comparison,
|
||||||
Values: lim.Values,
|
Values: lim.Values,
|
||||||
Required: lim.Required,
|
Required: lim.Required,
|
||||||
}
|
}
|
||||||
// Convert audioBitrate limitation values from bps to kbps
|
// Convert audioBitrate limitation values from bps to kbps
|
||||||
if lim.Name == transcode.LimitationAudioBitrate {
|
if lim.Name == stream.LimitationAudioBitrate {
|
||||||
coreLim.Values = convertBitrateValues(lim.Values)
|
coreLim.Values = convertBitrateValues(lim.Values)
|
||||||
}
|
}
|
||||||
coreCP.Limitations = append(coreCP.Limitations, coreLim)
|
coreCP.Limitations = append(coreCP.Limitations, coreLim)
|
||||||
@@ -178,8 +178,8 @@ func isValidMediaType(mediaType string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var validProtocols = []string{
|
var validProtocols = []string{
|
||||||
transcode.ProtocolHTTP,
|
stream.ProtocolHTTP,
|
||||||
transcode.ProtocolHLS,
|
stream.ProtocolHLS,
|
||||||
}
|
}
|
||||||
|
|
||||||
func isValidProtocol(p string) bool {
|
func isValidProtocol(p string) bool {
|
||||||
@@ -187,7 +187,7 @@ func isValidProtocol(p string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var validCodecProfileTypes = []string{
|
var validCodecProfileTypes = []string{
|
||||||
transcode.CodecProfileTypeAudio,
|
stream.CodecProfileTypeAudio,
|
||||||
}
|
}
|
||||||
|
|
||||||
func isValidCodecProfileType(t string) bool {
|
func isValidCodecProfileType(t string) bool {
|
||||||
@@ -195,11 +195,11 @@ func isValidCodecProfileType(t string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var validLimitationNames = []string{
|
var validLimitationNames = []string{
|
||||||
transcode.LimitationAudioChannels,
|
stream.LimitationAudioChannels,
|
||||||
transcode.LimitationAudioBitrate,
|
stream.LimitationAudioBitrate,
|
||||||
transcode.LimitationAudioProfile,
|
stream.LimitationAudioProfile,
|
||||||
transcode.LimitationAudioSamplerate,
|
stream.LimitationAudioSamplerate,
|
||||||
transcode.LimitationAudioBitdepth,
|
stream.LimitationAudioBitdepth,
|
||||||
}
|
}
|
||||||
|
|
||||||
func isValidLimitationName(n string) bool {
|
func isValidLimitationName(n string) bool {
|
||||||
@@ -207,10 +207,10 @@ func isValidLimitationName(n string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var validComparisons = []string{
|
var validComparisons = []string{
|
||||||
transcode.ComparisonEquals,
|
stream.ComparisonEquals,
|
||||||
transcode.ComparisonNotEquals,
|
stream.ComparisonNotEquals,
|
||||||
transcode.ComparisonLessThanEqual,
|
stream.ComparisonLessThanEqual,
|
||||||
transcode.ComparisonGreaterThanEqual,
|
stream.ComparisonGreaterThanEqual,
|
||||||
}
|
}
|
||||||
|
|
||||||
func isValidComparison(c string) bool {
|
func isValidComparison(c string) bool {
|
||||||
@@ -218,9 +218,9 @@ func isValidComparison(c string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// toResponseStreamDetails converts a core StreamDetails to the API response type.
|
// toResponseStreamDetails converts a core StreamDetails to the API response type.
|
||||||
func toResponseStreamDetails(sd *transcode.StreamDetails) *responses.StreamDetails {
|
func toResponseStreamDetails(sd *stream.Details) *responses.StreamDetails {
|
||||||
return &responses.StreamDetails{
|
return &responses.StreamDetails{
|
||||||
Protocol: transcode.ProtocolHTTP, // TODO: derive from decision when HLS support is added
|
Protocol: stream.ProtocolHTTP, // TODO: derive from decision when HLS support is added
|
||||||
Container: sd.Container,
|
Container: sd.Container,
|
||||||
Codec: sd.Codec,
|
Codec: sd.Codec,
|
||||||
AudioBitrate: int32(kbpsToBps(sd.Bitrate)),
|
AudioBitrate: int32(kbpsToBps(sd.Bitrate)),
|
||||||
@@ -232,7 +232,7 @@ func toResponseStreamDetails(sd *transcode.StreamDetails) *responses.StreamDetai
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetTranscodeDecision handles the OpenSubsonic getTranscodeDecision endpoint.
|
// GetTranscodeDecision handles the OpenSubsonic getTranscodeDecision endpoint.
|
||||||
// It receives client capabilities and returns a decision on whether to direct play or transcode.
|
// It receives client capabilities and returns a decision on whether to direct play or stream.
|
||||||
func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
w.Header().Set("Allow", "POST")
|
w.Header().Set("Allow", "POST")
|
||||||
@@ -279,7 +279,7 @@ func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make the decision
|
// Make the decision
|
||||||
decision, err := api.transcodeDecision.MakeDecision(ctx, mf, clientInfo, transcode.DecisionOptions{})
|
decision, err := api.transcodeDecision.MakeDecision(ctx, mf, clientInfo, stream.TranscodeOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx, "Failed to make transcode decision", "mediaID", mediaID, err)
|
log.Error(ctx, "Failed to make transcode decision", "mediaID", mediaID, err)
|
||||||
return nil, newError(responses.ErrorGeneric, "failed to make transcode decision")
|
return nil, newError(responses.ErrorGeneric, "failed to make transcode decision")
|
||||||
@@ -343,13 +343,23 @@ func (api *Router) GetTranscodeStream(w http.ResponseWriter, r *http.Request) (*
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch the media file
|
||||||
|
mf, err := api.ds.MediaFile(ctx).Get(mediaID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, model.ErrNotFound) {
|
||||||
|
http.Error(w, "Not Found", http.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
log.Error(ctx, "Error retrieving media file", "mediaID", mediaID, err)
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Validate the token and resolve streaming parameters
|
// Validate the token and resolve streaming parameters
|
||||||
streamReq, mf, err := api.transcodeDecision.ResolveRequestFromToken(ctx, transcodeParamsToken, mediaID, p.IntOr("offset", 0))
|
streamReq, err := api.transcodeDecision.ResolveRequestFromToken(ctx, transcodeParamsToken, mf, p.IntOr("offset", 0))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, transcode.ErrMediaNotFound):
|
case errors.Is(err, stream.ErrTokenInvalid), errors.Is(err, stream.ErrTokenStale):
|
||||||
http.Error(w, "Not Found", http.StatusNotFound)
|
|
||||||
case errors.Is(err, transcode.ErrTokenInvalid), errors.Is(err, transcode.ErrTokenStale):
|
|
||||||
http.Error(w, "Gone", http.StatusGone)
|
http.Error(w, "Gone", http.StatusGone)
|
||||||
default:
|
default:
|
||||||
log.Error(ctx, "Error validating transcode params", err)
|
log.Error(ctx, "Error validating transcode params", err)
|
||||||
@@ -358,8 +368,8 @@ func (api *Router) GetTranscodeStream(w http.ResponseWriter, r *http.Request) (*
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create stream (use DoStream to avoid duplicate DB fetch)
|
// Create stream
|
||||||
stream, err := api.streamer.DoStream(ctx, mf, streamReq)
|
stream, err := api.streamer.NewStream(ctx, mf, streamReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx, "Error creating stream", "mediaID", mediaID, err)
|
log.Error(ctx, "Error creating stream", "mediaID", mediaID, err)
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/core/transcode"
|
"github.com/navidrome/navidrome/core/stream"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
"github.com/navidrome/navidrome/tests"
|
"github.com/navidrome/navidrome/tests"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
@@ -156,10 +156,10 @@ var _ = Describe("Transcode endpoints", func() {
|
|||||||
mockMFRepo.SetData(model.MediaFiles{
|
mockMFRepo.SetData(model.MediaFiles{
|
||||||
{ID: "song-1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100},
|
{ID: "song-1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100},
|
||||||
})
|
})
|
||||||
mockTD.decision = &transcode.Decision{
|
mockTD.decision = &stream.TranscodeDecision{
|
||||||
MediaID: "song-1",
|
MediaID: "song-1",
|
||||||
CanDirectPlay: true,
|
CanDirectPlay: true,
|
||||||
SourceStream: transcode.StreamDetails{
|
SourceStream: stream.Details{
|
||||||
Container: "mp3", Codec: "mp3", Bitrate: 320,
|
Container: "mp3", Codec: "mp3", Bitrate: 320,
|
||||||
SampleRate: 44100, Channels: 2,
|
SampleRate: 44100, Channels: 2,
|
||||||
},
|
},
|
||||||
@@ -184,18 +184,18 @@ var _ = Describe("Transcode endpoints", func() {
|
|||||||
mockMFRepo.SetData(model.MediaFiles{
|
mockMFRepo.SetData(model.MediaFiles{
|
||||||
{ID: "song-2", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24},
|
{ID: "song-2", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24},
|
||||||
})
|
})
|
||||||
mockTD.decision = &transcode.Decision{
|
mockTD.decision = &stream.TranscodeDecision{
|
||||||
MediaID: "song-2",
|
MediaID: "song-2",
|
||||||
CanDirectPlay: false,
|
CanDirectPlay: false,
|
||||||
CanTranscode: true,
|
CanTranscode: true,
|
||||||
TargetFormat: "mp3",
|
TargetFormat: "mp3",
|
||||||
TargetBitrate: 256,
|
TargetBitrate: 256,
|
||||||
TranscodeReasons: []string{"container not supported"},
|
TranscodeReasons: []string{"container not supported"},
|
||||||
SourceStream: transcode.StreamDetails{
|
SourceStream: stream.Details{
|
||||||
Container: "flac", Codec: "flac", Bitrate: 1000,
|
Container: "flac", Codec: "flac", Bitrate: 1000,
|
||||||
SampleRate: 96000, BitDepth: 24, Channels: 2,
|
SampleRate: 96000, BitDepth: 24, Channels: 2,
|
||||||
},
|
},
|
||||||
TranscodeStream: &transcode.StreamDetails{
|
TranscodeStream: &stream.Details{
|
||||||
Container: "mp3", Codec: "mp3", Bitrate: 256,
|
Container: "mp3", Codec: "mp3", Bitrate: 256,
|
||||||
SampleRate: 96000, Channels: 2,
|
SampleRate: 96000, Channels: 2,
|
||||||
},
|
},
|
||||||
@@ -231,7 +231,8 @@ var _ = Describe("Transcode endpoints", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("returns 410 for invalid or mismatched token", func() {
|
It("returns 410 for invalid or mismatched token", func() {
|
||||||
mockTD.resolveErr = transcode.ErrTokenInvalid
|
mockMFRepo.SetData(model.MediaFiles{{ID: "123"}})
|
||||||
|
mockTD.resolveErr = stream.ErrTokenInvalid
|
||||||
r := newGetRequest("mediaId=123", "mediaType=song", "transcodeParams=bad-token")
|
r := newGetRequest("mediaId=123", "mediaType=song", "transcodeParams=bad-token")
|
||||||
resp, err := router.GetTranscodeStream(w, r)
|
resp, err := router.GetTranscodeStream(w, r)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
@@ -240,7 +241,7 @@ var _ = Describe("Transcode endpoints", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("returns 404 when media file not found", func() {
|
It("returns 404 when media file not found", func() {
|
||||||
mockTD.resolveErr = transcode.ErrMediaNotFound
|
// mockMFRepo has no data, so Get() returns ErrNotFound
|
||||||
r := newGetRequest("mediaId=gone-id", "mediaType=song", "transcodeParams=valid-token")
|
r := newGetRequest("mediaId=gone-id", "mediaType=song", "transcodeParams=valid-token")
|
||||||
resp, err := router.GetTranscodeStream(w, r)
|
resp, err := router.GetTranscodeStream(w, r)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
@@ -249,7 +250,8 @@ var _ = Describe("Transcode endpoints", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("returns 410 when media file has changed (stale token)", func() {
|
It("returns 410 when media file has changed (stale token)", func() {
|
||||||
mockTD.resolveErr = transcode.ErrTokenStale
|
mockMFRepo.SetData(model.MediaFiles{{ID: "song-1"}})
|
||||||
|
mockTD.resolveErr = stream.ErrTokenStale
|
||||||
r := newGetRequest("mediaId=song-1", "mediaType=song", "transcodeParams=stale-token")
|
r := newGetRequest("mediaId=song-1", "mediaType=song", "transcodeParams=stale-token")
|
||||||
resp, err := router.GetTranscodeStream(w, r)
|
resp, err := router.GetTranscodeStream(w, r)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
@@ -260,14 +262,13 @@ var _ = Describe("Transcode endpoints", func() {
|
|||||||
It("builds correct StreamRequest for direct play", func() {
|
It("builds correct StreamRequest for direct play", func() {
|
||||||
fakeStreamer := &fakeMediaStreamer{}
|
fakeStreamer := &fakeMediaStreamer{}
|
||||||
router = New(ds, nil, fakeStreamer, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, mockTD)
|
router = New(ds, nil, fakeStreamer, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, mockTD)
|
||||||
mockTD.resolvedReq = transcode.StreamRequest{ID: "song-1"}
|
mockMFRepo.SetData(model.MediaFiles{{ID: "song-1"}})
|
||||||
mockTD.resolvedMF = &model.MediaFile{ID: "song-1"}
|
mockTD.resolvedReq = stream.Request{}
|
||||||
|
|
||||||
r := newGetRequest("mediaId=song-1", "mediaType=song", "transcodeParams=valid-token")
|
r := newGetRequest("mediaId=song-1", "mediaType=song", "transcodeParams=valid-token")
|
||||||
_, _ = router.GetTranscodeStream(w, r)
|
_, _ = router.GetTranscodeStream(w, r)
|
||||||
|
|
||||||
Expect(fakeStreamer.captured).ToNot(BeNil())
|
Expect(fakeStreamer.captured).ToNot(BeNil())
|
||||||
Expect(fakeStreamer.captured.ID).To(Equal("song-1"))
|
|
||||||
Expect(fakeStreamer.captured.Format).To(BeEmpty())
|
Expect(fakeStreamer.captured.Format).To(BeEmpty())
|
||||||
Expect(fakeStreamer.captured.BitRate).To(BeZero())
|
Expect(fakeStreamer.captured.BitRate).To(BeZero())
|
||||||
Expect(fakeStreamer.captured.SampleRate).To(BeZero())
|
Expect(fakeStreamer.captured.SampleRate).To(BeZero())
|
||||||
@@ -278,21 +279,19 @@ var _ = Describe("Transcode endpoints", func() {
|
|||||||
It("builds correct StreamRequest for transcoding", func() {
|
It("builds correct StreamRequest for transcoding", func() {
|
||||||
fakeStreamer := &fakeMediaStreamer{}
|
fakeStreamer := &fakeMediaStreamer{}
|
||||||
router = New(ds, nil, fakeStreamer, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, mockTD)
|
router = New(ds, nil, fakeStreamer, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, mockTD)
|
||||||
mockTD.resolvedReq = transcode.StreamRequest{
|
mockMFRepo.SetData(model.MediaFiles{{ID: "song-2"}})
|
||||||
ID: "song-2",
|
mockTD.resolvedReq = stream.Request{
|
||||||
Format: "mp3",
|
Format: "mp3",
|
||||||
BitRate: 256,
|
BitRate: 256,
|
||||||
SampleRate: 44100,
|
SampleRate: 44100,
|
||||||
BitDepth: 16,
|
BitDepth: 16,
|
||||||
Channels: 2,
|
Channels: 2,
|
||||||
}
|
}
|
||||||
mockTD.resolvedMF = &model.MediaFile{ID: "song-2"}
|
|
||||||
|
|
||||||
r := newGetRequest("mediaId=song-2", "mediaType=song", "transcodeParams=valid-token", "offset=10")
|
r := newGetRequest("mediaId=song-2", "mediaType=song", "transcodeParams=valid-token", "offset=10")
|
||||||
_, _ = router.GetTranscodeStream(w, r)
|
_, _ = router.GetTranscodeStream(w, r)
|
||||||
|
|
||||||
Expect(fakeStreamer.captured).ToNot(BeNil())
|
Expect(fakeStreamer.captured).ToNot(BeNil())
|
||||||
Expect(fakeStreamer.captured.ID).To(Equal("song-2"))
|
|
||||||
Expect(fakeStreamer.captured.Format).To(Equal("mp3"))
|
Expect(fakeStreamer.captured.Format).To(Equal("mp3"))
|
||||||
Expect(fakeStreamer.captured.BitRate).To(Equal(256))
|
Expect(fakeStreamer.captured.BitRate).To(Equal(256))
|
||||||
Expect(fakeStreamer.captured.SampleRate).To(Equal(44100))
|
Expect(fakeStreamer.captured.SampleRate).To(Equal(44100))
|
||||||
@@ -353,38 +352,37 @@ func newJSONPostRequest(queryParams string, jsonBody string) *http.Request {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// mockTranscodeDecision is a test double for transcode.Decider
|
// mockTranscodeDecision is a test double for stream.TranscodeDecider
|
||||||
type mockTranscodeDecision struct {
|
type mockTranscodeDecision struct {
|
||||||
decision *transcode.Decision
|
decision *stream.TranscodeDecision
|
||||||
token string
|
token string
|
||||||
tokenErr error
|
tokenErr error
|
||||||
resolvedReq transcode.StreamRequest
|
resolvedReq stream.Request
|
||||||
resolvedMF *model.MediaFile
|
|
||||||
resolveErr error
|
resolveErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockTranscodeDecision) MakeDecision(_ context.Context, _ *model.MediaFile, _ *transcode.ClientInfo, _ transcode.DecisionOptions) (*transcode.Decision, error) {
|
func (m *mockTranscodeDecision) MakeDecision(_ context.Context, _ *model.MediaFile, _ *stream.ClientInfo, _ stream.TranscodeOptions) (*stream.TranscodeDecision, error) {
|
||||||
if m.decision != nil {
|
if m.decision != nil {
|
||||||
return m.decision, nil
|
return m.decision, nil
|
||||||
}
|
}
|
||||||
return &transcode.Decision{}, nil
|
return &stream.TranscodeDecision{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockTranscodeDecision) ResolveRequest(_ context.Context, _ *model.MediaFile, _ string, _ int, _ int) transcode.StreamRequest {
|
func (m *mockTranscodeDecision) ResolveRequest(_ context.Context, _ *model.MediaFile, _ string, _ int, _ int) stream.Request {
|
||||||
return transcode.StreamRequest{Format: "raw"}
|
return stream.Request{Format: "raw"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockTranscodeDecision) CreateTranscodeParams(_ *transcode.Decision) (string, error) {
|
func (m *mockTranscodeDecision) CreateTranscodeParams(_ *stream.TranscodeDecision) (string, error) {
|
||||||
return m.token, m.tokenErr
|
return m.token, m.tokenErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockTranscodeDecision) ResolveRequestFromToken(_ context.Context, _ string, _ string, offset int) (transcode.StreamRequest, *model.MediaFile, error) {
|
func (m *mockTranscodeDecision) ResolveRequestFromToken(_ context.Context, _ string, _ *model.MediaFile, offset int) (stream.Request, error) {
|
||||||
if m.resolveErr != nil {
|
if m.resolveErr != nil {
|
||||||
return transcode.StreamRequest{}, nil, m.resolveErr
|
return stream.Request{}, m.resolveErr
|
||||||
}
|
}
|
||||||
req := m.resolvedReq
|
req := m.resolvedReq
|
||||||
req.Offset = offset
|
req.Offset = offset
|
||||||
return req, m.resolvedMF, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// fakeMediaStreamer captures the StreamRequest and returns a sentinel error,
|
// fakeMediaStreamer captures the StreamRequest and returns a sentinel error,
|
||||||
@@ -392,15 +390,10 @@ func (m *mockTranscodeDecision) ResolveRequestFromToken(_ context.Context, _ str
|
|||||||
var errStreamCaptured = errors.New("stream request captured")
|
var errStreamCaptured = errors.New("stream request captured")
|
||||||
|
|
||||||
type fakeMediaStreamer struct {
|
type fakeMediaStreamer struct {
|
||||||
captured *transcode.StreamRequest
|
captured *stream.Request
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeMediaStreamer) NewStream(_ context.Context, req transcode.StreamRequest) (*transcode.Stream, error) {
|
func (f *fakeMediaStreamer) NewStream(_ context.Context, _ *model.MediaFile, req stream.Request) (*stream.Stream, error) {
|
||||||
f.captured = &req
|
|
||||||
return nil, errStreamCaptured
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeMediaStreamer) DoStream(_ context.Context, _ *model.MediaFile, req transcode.StreamRequest) (*transcode.Stream, error) {
|
|
||||||
f.captured = &req
|
f.captured = &req
|
||||||
return nil, errStreamCaptured
|
return nil, errStreamCaptured
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user