mirror of
https://github.com/navidrome/navidrome.git
synced 2026-03-04 06:35:52 +00:00
207 lines
5.4 KiB
Go
207 lines
5.4 KiB
Go
package transcode
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/navidrome/navidrome/model"
|
|
)
|
|
|
|
// adjustResult represents the outcome of applying a limitation to a transcoded stream value
|
|
type adjustResult int
|
|
|
|
const (
|
|
adjustNone adjustResult = iota // Value already satisfies the limitation
|
|
adjustAdjusted // Value was changed to fit the limitation
|
|
adjustCannotFit // Cannot satisfy the limitation (reject this profile)
|
|
)
|
|
|
|
// checkLimitations checks codec profile limitations against source media.
|
|
// Returns "" if all limitations pass, or a typed reason string for the first failure.
|
|
func checkLimitations(mf *model.MediaFile, sourceBitrate int, limitations []Limitation) string {
|
|
for _, lim := range limitations {
|
|
var ok bool
|
|
var reason string
|
|
|
|
switch lim.Name {
|
|
case LimitationAudioChannels:
|
|
ok = checkIntLimitation(mf.Channels, lim.Comparison, lim.Values)
|
|
reason = "audio channels not supported"
|
|
case LimitationAudioSamplerate:
|
|
ok = checkIntLimitation(mf.SampleRate, lim.Comparison, lim.Values)
|
|
reason = "audio samplerate not supported"
|
|
case LimitationAudioBitrate:
|
|
ok = checkIntLimitation(sourceBitrate, lim.Comparison, lim.Values)
|
|
reason = "audio bitrate not supported"
|
|
case LimitationAudioBitdepth:
|
|
ok = checkIntLimitation(mf.BitDepth, lim.Comparison, lim.Values)
|
|
reason = "audio bitdepth not supported"
|
|
case LimitationAudioProfile:
|
|
// TODO: populate source profile when MediaFile has audio profile info
|
|
ok = checkStringLimitation("", lim.Comparison, lim.Values)
|
|
reason = "audio profile not supported"
|
|
default:
|
|
continue
|
|
}
|
|
|
|
if !ok && lim.Required {
|
|
return reason
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// applyLimitation adjusts a transcoded stream parameter to satisfy the limitation.
|
|
// Returns the adjustment result.
|
|
func applyLimitation(sourceBitrate int, lim *Limitation, ts *StreamDetails) adjustResult {
|
|
switch lim.Name {
|
|
case LimitationAudioChannels:
|
|
return applyIntLimitation(lim.Comparison, lim.Values, ts.Channels, func(v int) { ts.Channels = v })
|
|
case LimitationAudioBitrate:
|
|
current := ts.Bitrate
|
|
if current == 0 {
|
|
current = sourceBitrate
|
|
}
|
|
return applyIntLimitation(lim.Comparison, lim.Values, current, func(v int) { ts.Bitrate = v })
|
|
case LimitationAudioSamplerate:
|
|
return applyIntLimitation(lim.Comparison, lim.Values, ts.SampleRate, func(v int) { ts.SampleRate = v })
|
|
case LimitationAudioBitdepth:
|
|
if ts.BitDepth > 0 {
|
|
return applyIntLimitation(lim.Comparison, lim.Values, ts.BitDepth, func(v int) { ts.BitDepth = v })
|
|
}
|
|
case LimitationAudioProfile:
|
|
// TODO: implement when audio profile data is available
|
|
}
|
|
return adjustNone
|
|
}
|
|
|
|
// applyIntLimitation applies a limitation comparison to a value.
|
|
// If the value needs adjusting, calls the setter and returns the result.
|
|
func applyIntLimitation(comparison string, values []string, current int, setter func(int)) adjustResult {
|
|
if len(values) == 0 {
|
|
return adjustNone
|
|
}
|
|
|
|
switch comparison {
|
|
case ComparisonLessThanEqual:
|
|
limit, ok := parseInt(values[0])
|
|
if !ok {
|
|
return adjustNone
|
|
}
|
|
if current <= limit {
|
|
return adjustNone
|
|
}
|
|
setter(limit)
|
|
return adjustAdjusted
|
|
case ComparisonGreaterThanEqual:
|
|
limit, ok := parseInt(values[0])
|
|
if !ok {
|
|
return adjustNone
|
|
}
|
|
if current >= limit {
|
|
return adjustNone
|
|
}
|
|
// Cannot upscale
|
|
return adjustCannotFit
|
|
case ComparisonEquals:
|
|
// Check if current value matches any allowed value
|
|
for _, v := range values {
|
|
if limit, ok := parseInt(v); ok && current == limit {
|
|
return adjustNone
|
|
}
|
|
}
|
|
// Find the closest allowed value below current (don't upscale)
|
|
var closest int
|
|
found := false
|
|
for _, v := range values {
|
|
if limit, ok := parseInt(v); ok && limit < current {
|
|
if !found || limit > closest {
|
|
closest = limit
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
if found {
|
|
setter(closest)
|
|
return adjustAdjusted
|
|
}
|
|
return adjustCannotFit
|
|
case ComparisonNotEquals:
|
|
for _, v := range values {
|
|
if limit, ok := parseInt(v); ok && current == limit {
|
|
return adjustCannotFit
|
|
}
|
|
}
|
|
return adjustNone
|
|
}
|
|
|
|
return adjustNone
|
|
}
|
|
|
|
func checkIntLimitation(value int, comparison string, values []string) bool {
|
|
if len(values) == 0 {
|
|
return true
|
|
}
|
|
|
|
switch comparison {
|
|
case ComparisonLessThanEqual:
|
|
limit, ok := parseInt(values[0])
|
|
if !ok {
|
|
return true
|
|
}
|
|
return value <= limit
|
|
case ComparisonGreaterThanEqual:
|
|
limit, ok := parseInt(values[0])
|
|
if !ok {
|
|
return true
|
|
}
|
|
return value >= limit
|
|
case ComparisonEquals:
|
|
for _, v := range values {
|
|
if limit, ok := parseInt(v); ok && value == limit {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
case ComparisonNotEquals:
|
|
for _, v := range values {
|
|
if limit, ok := parseInt(v); ok && value == limit {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return true
|
|
}
|
|
|
|
// checkStringLimitation checks a string value against a limitation.
|
|
// Only Equals and NotEquals comparisons are meaningful for strings.
|
|
// LessThanEqual/GreaterThanEqual are not applicable and always pass.
|
|
func checkStringLimitation(value string, comparison string, values []string) bool {
|
|
switch comparison {
|
|
case ComparisonEquals:
|
|
for _, v := range values {
|
|
if strings.EqualFold(value, v) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
case ComparisonNotEquals:
|
|
for _, v := range values {
|
|
if strings.EqualFold(value, v) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return true
|
|
}
|
|
|
|
func parseInt(s string) (int, bool) {
|
|
v, err := strconv.Atoi(s)
|
|
if err != nil || v < 0 {
|
|
return 0, false
|
|
}
|
|
return v, true
|
|
}
|