Merge pull request #17537 from vdatanet/fix/pcm-wav-transcode

Fix PCM audio transcoding to wav returning HTTP 500 and headerless output
This commit is contained in:
Cody Robibero
2026-08-05 18:39:42 -04:00
committed by GitHub
3 changed files with 49 additions and 9 deletions
-5
View File
@@ -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;
@@ -7864,10 +7864,16 @@ namespace MediaBrowser.Controller.MediaEncoding
audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
}
if (GetAudioEncoder(state).StartsWith("pcm_", StringComparison.Ordinal))
// 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 ", GetAudioEncoder(state).AsSpan(4)));
audioTranscodeParams.Add("-ar " + state.BaseRequest.AudioBitRate);
audioTranscodeParams.Add(string.Concat("-f ", audioEncoder.AsSpan(4)));
}
var sampleRate = state.OutputAudioSampleRate;
@@ -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",