From 2e02e92cc4c6b02afbbdea4d2d9faabd82924be8 Mon Sep 17 00:00:00 2001 From: Deluan Date: Thu, 5 Feb 2026 23:17:56 -0500 Subject: [PATCH] refactor(transcoding): enhance logging for transcode decision process and client info conversion Signed-off-by: Deluan --- core/transcode_decision.go | 30 ++++++++++++++++++++++++++++-- server/subsonic/transcode.go | 8 ++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/core/transcode_decision.go b/core/transcode_decision.go index 388691953..ecf32bd90 100644 --- a/core/transcode_decision.go +++ b/core/transcode_decision.go @@ -8,6 +8,7 @@ import ( "time" "github.com/navidrome/navidrome/core/auth" + "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" ) @@ -24,7 +25,7 @@ type TranscodeDecision interface { } // ClientInfo represents client playback capabilities. -// All bitrate values are in kilobits per second (kbps), matching Navidrome conventions. +// All bitrate values are in kilobits per second (kbps) type ClientInfo struct { Name string Platform string @@ -151,6 +152,10 @@ func (s *transcodeDecisionService) MakeDecision(ctx context.Context, mf *model.M sourceBitrate := mf.BitRate // kbps + log.Trace(ctx, "Making transcode decision", "mediaID", mf.ID, "container", mf.Suffix, + "codec", mf.AudioCodec(), "bitrate", sourceBitrate, "channels", mf.Channels, + "sampleRate", mf.SampleRate, "lossless", mf.IsLossless(), "client", clientInfo.Name) + // Build source stream details decision.SourceStream = StreamDetails{ Container: mf.Suffix, @@ -166,6 +171,8 @@ func (s *transcodeDecisionService) MakeDecision(ctx context.Context, mf *model.M // Check global bitrate constraint first. if clientInfo.MaxAudioBitrate > 0 && sourceBitrate > clientInfo.MaxAudioBitrate { + log.Trace(ctx, "Global bitrate constraint exceeded, skipping direct play", + "sourceBitrate", sourceBitrate, "maxAudioBitrate", clientInfo.MaxAudioBitrate) decision.TranscodeReasons = append(decision.TranscodeReasons, "audio bitrate not supported") // Skip direct play profiles entirely — global constraint fails } else { @@ -183,6 +190,7 @@ func (s *transcodeDecisionService) MakeDecision(ctx context.Context, mf *model.M // If direct play is possible, we're done if decision.CanDirectPlay { + log.Debug(ctx, "Transcode decision: direct play", "mediaID", mf.ID, "container", mf.Suffix, "codec", mf.AudioCodec()) return decision, nil } @@ -198,9 +206,17 @@ func (s *transcodeDecisionService) MakeDecision(ctx context.Context, mf *model.M } } + if decision.CanTranscode { + log.Debug(ctx, "Transcode decision: transcode", "mediaID", mf.ID, + "targetFormat", decision.TargetFormat, "targetBitrate", decision.TargetBitrate, + "targetChannels", decision.TargetChannels, "reasons", decision.TranscodeReasons) + } + // If neither direct play nor transcode is possible if !decision.CanDirectPlay && !decision.CanTranscode { decision.ErrorReason = "no compatible playback profile found" + log.Warn(ctx, "Transcode decision: no compatible profile", "mediaID", mf.ID, + "container", mf.Suffix, "codec", mf.AudioCodec(), "reasons", decision.TranscodeReasons) } return decision, nil @@ -290,6 +306,7 @@ const ( func (s *transcodeDecisionService) computeTranscodedStream(ctx context.Context, mf *model.MediaFile, sourceBitrate int, profile *TranscodingProfile, clientInfo *ClientInfo) *StreamDetails { // Check protocol (only http for now) if profile.Protocol != "" && !strings.EqualFold(profile.Protocol, ProtocolHTTP) { + log.Trace(ctx, "Skipping transcoding profile: unsupported protocol", "protocol", profile.Protocol) return nil } @@ -301,6 +318,7 @@ func (s *transcodeDecisionService) computeTranscodedStream(ctx context.Context, // Verify we have a transcoding config for this format tc, err := s.ds.Transcoding(ctx).FindByFormat(targetFormat) if err != nil || tc == nil { + log.Trace(ctx, "Skipping transcoding profile: no transcoding config", "targetFormat", targetFormat) return nil } @@ -308,6 +326,7 @@ func (s *transcodeDecisionService) computeTranscodedStream(ctx context.Context, // Reject lossy to lossless conversion if !mf.IsLossless() && targetIsLossless { + log.Trace(ctx, "Skipping transcoding profile: lossy to lossless not allowed", "targetFormat", targetFormat) return nil } @@ -334,7 +353,9 @@ func (s *transcodeDecisionService) computeTranscodedStream(ctx context.Context, } else { // Lossless to lossless: check if bitrate is under the global max if clientInfo.MaxAudioBitrate > 0 && sourceBitrate > clientInfo.MaxAudioBitrate { - return nil // Cannot guarantee bitrate within limit for lossless + log.Trace(ctx, "Skipping transcoding profile: lossless target exceeds bitrate limit", + "targetFormat", targetFormat, "sourceBitrate", sourceBitrate, "maxAudioBitrate", clientInfo.MaxAudioBitrate) + return nil } // No explicit bitrate for lossless target (leave 0) } @@ -366,9 +387,14 @@ func (s *transcodeDecisionService) computeTranscodedStream(ctx context.Context, result := applyLimitation(sourceBitrate, &lim, ts) // For lossless codecs, adjusting bitrate is not valid if strings.EqualFold(lim.Name, LimitationAudioBitrate) && targetIsLossless && result == adjustAdjusted { + log.Trace(ctx, "Skipping transcoding profile: cannot adjust bitrate for lossless target", + "targetFormat", targetFormat, "codec", targetCodec, "limitation", lim.Name) return nil } if result == adjustCannotFit { + log.Trace(ctx, "Skipping transcoding profile: codec limitation cannot be satisfied", + "targetFormat", targetFormat, "codec", targetCodec, "limitation", lim.Name, + "comparison", lim.Comparison, "values", lim.Values) return nil } } diff --git a/server/subsonic/transcode.go b/server/subsonic/transcode.go index 91a7e1d3d..a213e2f9f 100644 --- a/server/subsonic/transcode.go +++ b/server/subsonic/transcode.go @@ -56,9 +56,9 @@ type limitationReq struct { Required bool `json:"required,omitempty"` } -// toCore converts the API request struct to the core ClientInfo struct. +// toCoreClientInfo converts the API request struct to the core.ClientInfo struct. // The OpenSubsonic spec uses bps for bitrate values; core uses kbps. -func (r *clientInfoRequest) toCore() *core.ClientInfo { +func (r *clientInfoRequest) toCoreClientInfo() *core.ClientInfo { ci := &core.ClientInfo{ Name: r.Name, Platform: r.Platform, @@ -223,7 +223,7 @@ func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request) if err := clientInfoReq.validate(); err != nil { return nil, newError(responses.ErrorGeneric, "%v", err) } - clientInfo := clientInfoReq.toCore() + clientInfo := clientInfoReq.toCoreClientInfo() // Get media file mf, err := api.ds.MediaFile(ctx).Get(mediaID) @@ -308,7 +308,7 @@ func (api *Router) GetTranscodeStream(w http.ResponseWriter, r *http.Request) (* // Parse and validate the token params, err := api.transcodeDecision.ParseTranscodeParams(transcodeParams) if err != nil { - log.Debug(ctx, "Failed to parse transcode token", err) + log.Warn(ctx, "Failed to parse transcode token", err) return nil, newError(responses.ErrorDataNotFound, "invalid or expired transcodeParams token") }