aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCody Robibero <cody@robibe.ro>2026-08-05 18:39:42 -0400
committerGitHub <noreply@github.com>2026-08-05 18:39:42 -0400
commitd5a5b56484103e20bf36c558439048aa8f85c259 (patch)
tree25e75012449229cdf9280c8f502973e178713da4
parentef1ce3d6eb95e482a97af1ba858bf6c2ec02cc83 (diff)
parent3578e9a332c64d1aeef6be5e4f2c1a8d271169a8 (diff)
Merge pull request #17537 from vdatanet/fix/pcm-wav-transcode
Fix PCM audio transcoding to wav returning HTTP 500 and headerless output
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs5
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs14
-rw-r--r--tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs41
3 files changed, 50 insertions, 10 deletions
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 6a6aac1327..60c5bb2ef6 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -198,11 +198,6 @@ public static class StreamingHelpers
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
}
- if (outputAudioCodec.StartsWith("pcm_", StringComparison.Ordinal))
- {
- containerInternal = ".pcm";
- }
-
if (state.VideoRequest is not null)
{
state.OutputVideoCodec = state.Request.VideoCodec;
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 1b0bbe9ea0..9a68889352 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -7864,10 +7864,16 @@ namespace MediaBrowser.Controller.MediaEncoding
audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
}
- if (GetAudioEncoder(state).StartsWith("pcm_", StringComparison.Ordinal))
- {
- audioTranscodeParams.Add(string.Concat("-f ", GetAudioEncoder(state).AsSpan(4)));
- audioTranscodeParams.Add("-ar " + state.BaseRequest.AudioBitRate);
+ // The pcm_* encoders emit raw samples that carry no header of their own, so the header
+ // has to come from the muxer. Only force the matching raw muxer when the client actually
+ // asked for a raw container (added in #10321 for I2S/MCU clients): applying it to every
+ // pcm_* codec also strips the RIFF header from a `stream.wav` request, which then serves
+ // headerless PCM behind an audio/wav content type.
+ var audioEncoder = GetAudioEncoder(state);
+ if (audioEncoder.StartsWith("pcm_", StringComparison.Ordinal)
+ && string.Equals(state.OutputContainer, "pcm", StringComparison.OrdinalIgnoreCase))
+ {
+ audioTranscodeParams.Add(string.Concat("-f ", audioEncoder.AsSpan(4)));
}
var sampleRate = state.OutputAudioSampleRate;
diff --git a/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs b/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs
index 71b6551d0f..2b009b4673 100644
--- a/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs
+++ b/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs
@@ -223,12 +223,51 @@ public class EncodingHelperTests
Assert.Contains("-ar " + expectedSampleRate, args, StringComparison.Ordinal);
}
- private static EncodingJobInfo BuildAudioState(string audioCodec, int requestedSampleRate)
+ [Theory]
+ [InlineData("wav")]
+ [InlineData("flac")]
+ [InlineData("mp3")]
+ public void GetProgressiveAudioFullCommandLine_PcmInRealContainer_KeepsContainerMuxer(string outputContainer)
+ {
+ // A pcm_* encoder must not drag the raw muxer into a container that writes its own header,
+ // or the client gets headerless PCM behind the container's content type.
+ var state = BuildAudioState("pcm_s16le", 48000, outputContainer);
+ var args = CreateHelper().GetProgressiveAudioFullCommandLine(state, new EncodingOptions(), "/tmp/out");
+
+ Assert.DoesNotContain("-f s16le", args, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void GetProgressiveAudioFullCommandLine_PcmInPcmContainer_ForcesRawMuxer()
+ {
+ // The raw-PCM route added in #10321 for I2S/MCU clients must keep working.
+ var state = BuildAudioState("pcm_s16le", 48000, "pcm");
+ var args = CreateHelper().GetProgressiveAudioFullCommandLine(state, new EncodingOptions(), "/tmp/out");
+
+ Assert.Contains("-f s16le", args, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void GetProgressiveAudioFullCommandLine_PcmWithoutBitrate_EmitsNoEmptySampleRate()
+ {
+ // AudioBitRate is optional; it used to be emitted as `-ar <null>`, producing a bare `-ar`
+ // that made ffmpeg abort with "Expected number for ar" and the request fail with HTTP 500.
+ var state = BuildAudioState("pcm_s16le", 48000, "wav");
+ state.BaseRequest.AudioBitRate = null;
+ var args = CreateHelper().GetProgressiveAudioFullCommandLine(state, new EncodingOptions(), "/tmp/out");
+
+ Assert.DoesNotContain("-ar -", args, StringComparison.Ordinal);
+ Assert.DoesNotContain("-ar ", args, StringComparison.Ordinal);
+ Assert.Contains("-ar 48000", args, StringComparison.Ordinal);
+ }
+
+ private static EncodingJobInfo BuildAudioState(string audioCodec, int requestedSampleRate, string? outputContainer = null)
{
var audio = new MediaStream { Index = 0, Type = MediaStreamType.Audio, Codec = "flac", SampleRate = 96000 };
return new EncodingJobInfo(TranscodingJobType.Progressive)
{
+ OutputContainer = outputContainer,
MediaSource = new MediaSourceInfo
{
Container = "flac",