mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
refactor(transcode): streamline transcoding command lookup and format resolution
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
b99092a697
commit
3e3f4e0e50
@ -12,6 +12,7 @@ import (
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/core/ffmpeg"
|
||||
"github.com/navidrome/navidrome/core/transcode"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
@ -218,9 +219,9 @@ func NewTranscodingCache() TranscodingCache {
|
||||
consts.TranscodingCacheDir, consts.DefaultTranscodingCacheMaxItems,
|
||||
func(ctx context.Context, arg cache.Item) (io.Reader, error) {
|
||||
job := arg.(*streamJob)
|
||||
t, err := job.ms.ds.Transcoding(ctx).FindByFormat(job.format)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error loading transcoding command", "format", job.format, err)
|
||||
command := transcode.LookupTranscodeCommand(ctx, job.ms.ds, job.format)
|
||||
if command == "" {
|
||||
log.Error(ctx, "No transcoding command available", "format", job.format)
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
@ -237,7 +238,7 @@ func NewTranscodingCache() TranscodingCache {
|
||||
}
|
||||
|
||||
out, err := job.ms.transcoder.Transcode(transcodingCtx, ffmpeg.TranscodeOptions{
|
||||
Command: t.Command,
|
||||
Command: command,
|
||||
Format: job.format,
|
||||
FilePath: job.filePath,
|
||||
BitRate: job.bitRate,
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
"github.com/navidrome/navidrome/core/ffmpeg"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
@ -196,11 +197,17 @@ func (s *deciderService) computeTranscodedStream(ctx context.Context, src *Strea
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
responseContainer, targetFormat := s.resolveTargetFormat(ctx, profile)
|
||||
responseContainer, targetFormat := resolveTargetFormat(profile)
|
||||
if targetFormat == "" {
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
// Verify we have a transcoding command available (DB custom or built-in default)
|
||||
if LookupTranscodeCommand(ctx, s.ds, targetFormat) == "" {
|
||||
log.Trace(ctx, "Skipping transcoding profile: no transcoding command available", "targetFormat", targetFormat)
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
targetIsLossless := isLosslessFormat(targetFormat)
|
||||
|
||||
// Reject lossy to lossless conversion
|
||||
@ -247,34 +254,46 @@ func (s *deciderService) computeTranscodedStream(ctx context.Context, src *Strea
|
||||
return ts, targetFormat
|
||||
}
|
||||
|
||||
// LookupTranscodeCommand returns the ffmpeg command for the given format.
|
||||
// It checks the DB first (for user-customized commands), then falls back to
|
||||
// the built-in default command. Returns "" if the format is unknown.
|
||||
func LookupTranscodeCommand(ctx context.Context, ds model.DataStore, format string) string {
|
||||
t, err := ds.Transcoding(ctx).FindByFormat(format)
|
||||
if err == nil && t.Command != "" {
|
||||
return t.Command
|
||||
}
|
||||
// Fall back to built-in defaults
|
||||
for _, dt := range consts.DefaultTranscodings {
|
||||
if dt.TargetFormat == format {
|
||||
return dt.Command
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// resolveTargetFormat determines the response container and internal target format
|
||||
// by looking up transcoding configs. Returns ("", "") if no config found.
|
||||
func (s *deciderService) resolveTargetFormat(ctx context.Context, profile *Profile) (responseContainer, targetFormat string) {
|
||||
// from the profile's Container and AudioCodec fields. When an AudioCodec is specified
|
||||
// it is preferred as targetFormat (e.g. container "mp4" with audioCodec "aac" → targetFormat "aac").
|
||||
func resolveTargetFormat(profile *Profile) (responseContainer, targetFormat string) {
|
||||
responseContainer = strings.ToLower(profile.Container)
|
||||
targetFormat = responseContainer
|
||||
if targetFormat == "" {
|
||||
|
||||
// Prefer the audioCodec as targetFormat when provided (handles container-to-codec
|
||||
// mapping like "mp4" → "aac", "ogg" → "opus").
|
||||
if profile.AudioCodec != "" {
|
||||
targetFormat = strings.ToLower(profile.AudioCodec)
|
||||
}
|
||||
|
||||
// If neither container nor audioCodec is set, we can't resolve a format.
|
||||
if targetFormat == "" {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// When no container was specified, use the targetFormat as container too.
|
||||
if responseContainer == "" {
|
||||
responseContainer = targetFormat
|
||||
}
|
||||
|
||||
// Try the container first, then fall back to the audioCodec (e.g. "ogg" → "opus", "mp4" → "aac").
|
||||
_, err := s.ds.Transcoding(ctx).FindByFormat(targetFormat)
|
||||
if errors.Is(err, model.ErrNotFound) && profile.AudioCodec != "" && !strings.EqualFold(targetFormat, profile.AudioCodec) {
|
||||
codec := strings.ToLower(profile.AudioCodec)
|
||||
log.Trace(ctx, "No transcoding config for container, trying audioCodec", "container", targetFormat, "audioCodec", codec)
|
||||
_, err = s.ds.Transcoding(ctx).FindByFormat(codec)
|
||||
if err == nil {
|
||||
targetFormat = codec
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if !errors.Is(err, model.ErrNotFound) {
|
||||
log.Error(ctx, "Error looking up transcoding config", "format", targetFormat, err)
|
||||
} else {
|
||||
log.Trace(ctx, "Skipping transcoding profile: no transcoding config", "targetFormat", targetFormat)
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
return responseContainer, targetFormat
|
||||
}
|
||||
|
||||
|
||||
@ -247,7 +247,7 @@ var _ = Describe("Decider", func() {
|
||||
Expect(decision.TargetBitrate).To(Equal(192)) // source bitrate in kbps
|
||||
})
|
||||
|
||||
It("rejects unsupported transcoding format", func() {
|
||||
It("rejects format with no transcoding command available", func() {
|
||||
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2})
|
||||
ci := &ClientInfo{
|
||||
TranscodingProfiles: []Profile{
|
||||
@ -294,8 +294,6 @@ var _ = Describe("Decider", func() {
|
||||
|
||||
Context("Lossless to lossless transcoding", func() {
|
||||
It("allows lossless to lossless when samplerate needs downsampling", func() {
|
||||
// MockTranscodingRepo doesn't support "flac" format, so this would fail to find a config.
|
||||
// This test documents the behavior: lossless→lossless requires server transcoding config.
|
||||
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 176400, BitDepth: 1})
|
||||
ci := &ClientInfo{
|
||||
MaxAudioBitrate: 1000,
|
||||
@ -313,13 +311,7 @@ var _ = Describe("Decider", func() {
|
||||
})
|
||||
|
||||
It("sets IsLossless=true on transcoded stream when target is lossless", func() {
|
||||
// Simulate DSD→FLAC transcoding by using a mock that supports "flac"
|
||||
mockTranscoding := &tests.MockTranscodingRepo{}
|
||||
ds.MockedTranscoding = mockTranscoding
|
||||
svc = NewDecider(ds, ff)
|
||||
|
||||
// Transcoding to mp3 (lossy) should result in IsLossless=false.
|
||||
// Use mp3 profile to test that lossy output is correctly identified.
|
||||
mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24})
|
||||
ci := &ClientInfo{
|
||||
MaxTranscodingAudioBitrate: 320,
|
||||
@ -811,7 +803,7 @@ var _ = Describe("Decider", func() {
|
||||
decision, err := svc.MakeDecision(ctx, mf, ci)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(decision.CanTranscode).To(BeTrue())
|
||||
// TargetFormat is the internal format used for DB lookup ("aac")
|
||||
// TargetFormat is the internal format used for transcoding ("aac")
|
||||
Expect(decision.TargetFormat).To(Equal("aac"))
|
||||
// Container in the response preserves what the client asked ("mp4")
|
||||
Expect(decision.TranscodeStream.Container).To(Equal("mp4"))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user