refactor(transcoding): enhance logging for transcode decision process and client info conversion

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2026-02-05 23:17:56 -05:00
parent 216d0c6c6c
commit 2e02e92cc4
2 changed files with 32 additions and 6 deletions

View File

@ -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
}
}

View File

@ -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")
}