mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
* fix(transcoding): don't apply server-side transcoding override on getTranscodeDecision The getTranscodeDecision endpoint was incorrectly applying server-side player transcoding overrides (forced format and MaxBitRate cap), which replaced the client's declared capabilities with synthetic profiles. This caused the endpoint to ignore what the client can actually play and return decisions for formats the client never requested (e.g. AAC when the client only supports FLAC/opus/mp3). The override is now gated behind an ApplyServerOverride flag in TranscodeOptions, which is only set by the legacy stream endpoint where this behavior is expected. Signed-off-by: Deluan <deluan@navidrome.org> * refactor: move server-side transcoding override to ResolveRequest Moved the server-side player transcoding override logic (forced format and MaxBitRate cap) from MakeDecision into ResolveRequest, where the legacy stream context is handled. This makes MakeDecision a pure function that only operates on the ClientInfo it receives, removing the ApplyServerOverride flag and all context-sniffing from the decision engine. Tests moved accordingly to legacy_client_test.go. * test(e2e): update transcode decision tests for server override removal Updated e2e tests to reflect that getTranscodeDecision no longer applies server-side player overrides (MaxBitRate cap and forced transcoding profile). The player MaxBitRate tests now verify the endpoint ignores the player cap and relies solely on client-declared capabilities. * test(e2e): assert opus default bitrate when player cap is ignored Added bitrate assertion to verify the player MaxBitRate cap is truly ignored: the target bitrate should be the opus format default (128kbps), not the player cap (320kbps). --------- Signed-off-by: Deluan <deluan@navidrome.org>
114 lines
3.9 KiB
Go
114 lines
3.9 KiB
Go
package stream
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
)
|
|
|
|
// buildLegacyClientInfo translates legacy Subsonic stream/download parameters
|
|
// into a ClientInfo for use with MakeDecision.
|
|
func buildLegacyClientInfo(mf *model.MediaFile, reqFormat string, reqBitRate int) *ClientInfo {
|
|
ci := &ClientInfo{Name: "legacy"}
|
|
|
|
// Determine target format for transcoding
|
|
var targetFormat string
|
|
switch {
|
|
case reqFormat != "":
|
|
targetFormat = reqFormat
|
|
case reqBitRate > 0 && reqBitRate < mf.BitRate && conf.Server.DefaultDownsamplingFormat != "":
|
|
targetFormat = conf.Server.DefaultDownsamplingFormat
|
|
}
|
|
|
|
if targetFormat != "" {
|
|
// Add a direct play profile for the source format when no explicit
|
|
// format was requested (bitrate-only downsampling) or when the
|
|
// requested format matches the source. When the client explicitly
|
|
// requests a different format, direct play must not match the
|
|
// source — otherwise the source is returned untranscoded.
|
|
if reqFormat == "" || strings.EqualFold(reqFormat, mf.Suffix) {
|
|
ci.DirectPlayProfiles = []DirectPlayProfile{
|
|
{Containers: []string{mf.Suffix}, AudioCodecs: []string{mf.AudioCodec()}, Protocols: []string{ProtocolHTTP}},
|
|
}
|
|
}
|
|
ci.TranscodingProfiles = []Profile{
|
|
{Container: targetFormat, AudioCodec: targetFormat, Protocol: ProtocolHTTP},
|
|
}
|
|
if reqBitRate > 0 {
|
|
ci.MaxAudioBitrate = reqBitRate
|
|
ci.MaxTranscodingAudioBitrate = reqBitRate
|
|
}
|
|
} else {
|
|
// No transcoding requested — direct play everything
|
|
ci.DirectPlayProfiles = []DirectPlayProfile{
|
|
{Protocols: []string{ProtocolHTTP}},
|
|
}
|
|
}
|
|
|
|
return ci
|
|
}
|
|
|
|
// ResolveRequest uses MakeDecision to resolve legacy Subsonic stream parameters
|
|
// into a fully specified Request.
|
|
func (s *deciderService) ResolveRequest(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int, offset int) Request {
|
|
var req Request
|
|
req.Offset = offset
|
|
|
|
if reqFormat == "raw" {
|
|
req.Format = "raw"
|
|
return req
|
|
}
|
|
|
|
clientInfo := buildLegacyClientInfo(mf, reqFormat, reqBitRate)
|
|
|
|
// Apply server-side player transcoding override before making the decision
|
|
if trc, ok := request.TranscodingFrom(ctx); ok && trc.TargetFormat != "" {
|
|
clientInfo = applyServerOverride(ctx, clientInfo, &trc)
|
|
} else if player, ok := request.PlayerFrom(ctx); ok && player.MaxBitRate > 0 {
|
|
if clientInfo.MaxAudioBitrate == 0 || player.MaxBitRate < clientInfo.MaxAudioBitrate {
|
|
modified := *clientInfo
|
|
modified.MaxAudioBitrate = player.MaxBitRate
|
|
clientInfo = &modified
|
|
log.Debug(ctx, "Applied player MaxBitRate cap", "playerMaxBitRate", player.MaxBitRate, "client", clientInfo.Name)
|
|
}
|
|
}
|
|
|
|
decision, err := s.MakeDecision(ctx, mf, clientInfo, TranscodeOptions{SkipProbe: true})
|
|
if err != nil {
|
|
log.Error(ctx, "Error making transcode decision, falling back to raw", "id", mf.ID, err)
|
|
req.Format = "raw"
|
|
return req
|
|
}
|
|
|
|
if decision.CanDirectPlay {
|
|
req.Format = "raw"
|
|
return req
|
|
}
|
|
|
|
if decision.CanTranscode {
|
|
req.Format = decision.TargetFormat
|
|
req.BitRate = decision.TargetBitrate
|
|
req.SampleRate = decision.TargetSampleRate
|
|
req.BitDepth = decision.TargetBitDepth
|
|
req.Channels = decision.TargetChannels
|
|
return req
|
|
}
|
|
|
|
// No compatible profile for the requested format — retry with DefaultDownsamplingFormat
|
|
// TODO: validate DefaultDownsamplingFormat at startup to warn about unsupported values
|
|
fallbackFormat := conf.Server.DefaultDownsamplingFormat
|
|
if reqFormat != "" && fallbackFormat != "" && !strings.EqualFold(reqFormat, fallbackFormat) {
|
|
log.Warn(ctx, "Requested format not available, falling back to default downsampling format",
|
|
"requestedFormat", reqFormat, "fallbackFormat", fallbackFormat, "id", mf.ID)
|
|
return s.ResolveRequest(ctx, mf, fallbackFormat, reqBitRate, offset)
|
|
}
|
|
|
|
// Ultimate fallback — raw
|
|
req.Format = "raw"
|
|
return req
|
|
}
|