fix: use curl pipe to bypass FFmpeg HTTP header buffer limit

The previous -max_alloc approach does not fix the 'overlong headers' error
because FFmpeg 5.1 (Debian bookworm) uses a compile-time stack-allocated
buffer (MAX_URL_SIZE=4096) for HTTP response headers, which -max_alloc
cannot change.

Instead, when the stream URL is HTTP/HTTPS, launch curl to handle the
HTTP connection and pipe its stdout to FFmpeg via stdin (pipe:0). Curl
does not have the 4096-byte header limit, so it handles oversized CDN
response headers from Douyin without error.

Additional changes:
- RequestStopAsync kills curl first (instead of sending 'q' to FFmpeg),
  causing the pipe to close and FFmpeg to exit gracefully on EOF.
- SessionProcessRuntime tracks the curl process for cleanup.
This commit is contained in:
2026-07-09 12:19:34 +08:00
parent 8a079b4698
commit ad86c080e3
3 changed files with 132 additions and 21 deletions
@@ -195,6 +195,69 @@ public sealed partial class FfmpegService : IFfmpegService
throw new InvalidOperationException("A running ffmpeg process already exists for the recording session.");
}
// When the stream URL is HTTP, launch curl to handle the HTTP connection and
// pipe its stdout into FFmpeg's stdin. This bypasses FFmpeg's built-in HTTP
// handler which has a hard-coded 4096-byte response header buffer that triggers
// "overlong headers" errors with CDNs that return oversized headers.
if (IsHttpInput(streamUrlResult.SelectedUrl))
{
var curlProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "curl",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
},
EnableRaisingEvents = true
};
foreach (var arg in BuildCurlArgumentList(streamUrlResult.SelectedUrl, streamUrlResult.InputHeaders))
{
curlProcess.StartInfo.ArgumentList.Add(arg);
}
curlProcess.ErrorDataReceived += (_, args) =>
{
if (!string.IsNullOrWhiteSpace(args.Data))
{
_logger.LogDebug("curl[{SessionId}] {Line}", recordSession.Id, args.Data);
}
};
if (!curlProcess.Start())
{
process.Kill(true);
process.Dispose();
throw new InvalidOperationException("curl failed to start.");
}
curlProcess.BeginErrorReadLine();
runtime.AttachCurlProcess(curlProcess);
// Pipe curl stdout → FFmpeg stdin in the background.
// When curl exits or is killed, the pipe closes and FFmpeg sees EOF on stdin,
// which causes it to exit gracefully after finalizing the output.
_ = Task.Run(async () =>
{
try
{
await curlProcess.StandardOutput.BaseStream.CopyToAsync(
process.StandardInput.BaseStream);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "curl→ffmpeg pipe ended for session {RecordSessionId}", recordSession.Id);
}
finally
{
try { process.StandardInput.Close(); } catch { /* stdin may already be closed */ }
}
});
}
await PersistProcessBindingAsync(recordSession.Id, initialTask.Id, process.Id, cancellationToken);
await PersistStartupProfileAsync(runtime, cancellationToken);
await StartDanmakuAsync(runtime, initialTask, cancellationToken);
@@ -714,6 +777,23 @@ public sealed partial class FfmpegService : IFfmpegService
return;
}
// When curl is piping the stream, kill curl first so the pipe closes
// and FFmpeg sees EOF on stdin, causing a graceful exit.
var curlProcess = runtime.CurlProcess;
if (curlProcess is not null && !curlProcess.HasExited)
{
try
{
curlProcess.Kill(true);
}
catch (Exception curlEx)
{
_logger.LogWarning(curlEx, "Kill curl process failed for session {RecordSessionId}", recordSessionId);
}
return;
}
await process.StandardInput.WriteLineAsync("q");
await process.StandardInput.FlushAsync();
}