* fix(stream): cap DSD transcoded to lossless PCM at 88.2kHz
DSD64 was sent as 352.8kHz FLAC/WAV, which many clients cannot play.
Client and codec profile limits below 88.2kHz still apply.
ffprobe already reports DSD at rate/8 with 8 bits per sample, so only
rates read from tags are divided, and any DSD depth maps to 24-bit PCM.
Probed files used to come out at 44.1kHz with a bit depth of 8.
Fixes#6288
Co-authored-by: Olli Sulopuisto <1051255+ollisulopuisto@users.noreply.github.com>
Signed-off-by: Junker der Provinz <133605895+junkerderprovinz@users.noreply.github.com>
* fix(stream): let a client's sample rate limit replace the DSD cap
The 88.2kHz cap on DSD transcoded to lossless PCM is meant for clients that
say nothing about sample rates, like the legacy stream endpoint. Applied
unconditionally, it also overrode clients that declared their own
audioSamplerate limit: a client accepting up to 192kHz got 88.2kHz, and one
accepting only 96kHz or 192kHz got no compatible profile at all.
The cap now applies only when the client has no audioSamplerate limitation
for the target codec. Otherwise the client's limit decides the rate.
* refactor(stream): share the audio codec profile matcher
Direct play checks, transcode limitations and the new DSD cap check each
looped over the client's codec profiles with the same audio-type and codec
alias filter. They now use one audioCodecProfiles iterator, so the DSD cap
decision cannot drift from the limitations that are actually applied.
targetSampleRate takes a single capDSD flag instead of two adjacent bools.
* test(stream): table-drive the DSD to FLAC sample rate specs
Six specs repeated the same DSD source and FLAC transcoding profile and
differed only in the source rate, the client's codec profiles and the
expected rate. A DescribeTable lists the cap policy case by case, and
every entry now also checks the 24-bit depth.
---------
Signed-off-by: Junker der Provinz <133605895+junkerderprovinz@users.noreply.github.com>
Co-authored-by: Olli Sulopuisto <1051255+ollisulopuisto@users.noreply.github.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
getTranscodeDecision was copying the source file's bit depth into the
transcodeStream details, so a 24-bit FLAC negotiated to Opus reported
audioBitdepth=24. Lossy codecs (Opus, MP3, AAC) have no PCM bit depth,
and ffmpeg only honors a bit depth constraint (-sample_fmt) for lossless
outputs, so the value was both meaningless and misleading to clients
that use it as a quality indicator.
Only set the transcoded stream's bit depth when the target format is
lossless; a zero value omits audioBitdepth from the response. This also
makes audioBitdepth codec limitations a no-op for lossy targets instead
of rejecting the profile. Lossless targets (e.g. FLAC->FLAC downconvert)
keep reporting and clamping bit depth as before.
* fix(transcoding): clamp target channels to codec limit (#5336)
When transcoding a multi-channel source (e.g. 6-channel FLAC) to MP3, the
decider passed the source channel count through to ffmpeg unchanged. The
default MP3 command path then emitted `-ac 6`, and the template path injected
`-ac 6` after the template's own `-ac 2`, causing ffmpeg to honor the last
occurrence and fail with exit code 234 since libmp3lame only supports up to
2 channels.
Introduce `codecMaxChannels()` in core/stream/codec.go (mp3→2, opus→8),
mirroring the existing `codecMaxSampleRate` pattern, and apply the clamp in
`computeTranscodedStream` right after the sample-rate clamps. Also fix a
pre-existing ordering bug where the profile's MaxAudioChannels check compared
against src.Channels rather than ts.Channels, which would have let a looser
profile setting raise the codec-clamped value back up. Comparing against the
already-clamped ts.Channels makes profile limits strictly narrowing, which
matches how the sample-rate block already behaves.
The ffmpeg buildTemplateArgs comment is refreshed to point at the new upstream
clamp, since the flags it injects are now always codec-safe.
Adds unit tests for codecMaxChannels and four decider scenarios covering the
literal issue repro (6-ch FLAC→MP3 clamps to 2), a stricter profile limit
winning over the codec clamp, a looser profile limit leaving the codec clamp
intact, and a codec with no hard limit (AAC) passing 6 channels through.
* test(e2e): pin codec channel clamp at the Subsonic API surface (#5336)
Add a 6-channel FLAC fixture to the e2e test suite and use it to assert the
codec channel clamp end-to-end on both Subsonic streaming endpoints:
- getTranscodeDecision (mp3OnlyClient, no MaxAudioChannels in profile):
expects TranscodeStream.AudioChannels == 2 for the 6-channel source. This
exercises the new codecMaxChannels() helper through the OpenSubsonic
decision endpoint, with no profile-level channel limit masking the bug.
- /rest/stream (legacy): requests format=mp3 against the multichannel
fixture and asserts streamerSpy.LastRequest.Channels == 2, confirming
the clamp propagates through ResolveRequest into the stream.Request that
the streamer receives.
The fixture is metadata-only (channels: 6 plumbed via the existing
storagetest.File helper) — no real audio bytes required, since the e2e
suite uses a spy streamer rather than invoking ffmpeg. Bumps the empty-query
search3 song count expectation from 13 to 14 to account for the new fixture.
* test(decider): clarify codec-clamp comment terminology
Distinguish "transcoding profile MaxAudioChannels" (Profile.MaxAudioChannels
field) from "LimitationAudioChannels" (CodecProfile rule constant). The
regression test bypasses the former, not the latter.
* refactor: rename core/transcode directory to core/stream
* refactor: update all imports from core/transcode to core/stream
* refactor: rename exported symbols to fit core/stream package name
* refactor: simplify MediaStreamer interface to single NewStream method
Remove the two-method interface (NewStream + DoStream) in favor of a
single NewStream(ctx, mf, req) method. Callers are now responsible for
fetching the MediaFile before calling NewStream. This removes the
implicit DB lookup from the streamer, making it a pure streaming
concern.
* refactor: update all callers from DoStream to NewStream
* chore: update wire_gen.go and stale comment for core/stream rename
* refactor: update wire command to handle GO_BUILD_TAGS correctly
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: distinguish not-found from internal errors in public stream handler
* refactor: remove unused ID field from stream.Request
* refactor: simplify ResolveRequestFromToken to receive *model.MediaFile
Move MediaFile fetching responsibility to callers, making the method
focused on token validation and request resolution. Remove ErrMediaNotFound
(no longer produced). Update GetTranscodeStream handler to fetch the
media file before calling ResolveRequestFromToken.
* refactor: extend tokenTTL from 12 to 48 hours
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>