从 C# 写入两个标准输入管道

Writing to two standard input pipes from C#

我正在使用我的 C# 应用程序中的 FFMPEG 从原始未编码帧构建视频流。对于一个输入流,这是相当简单的:

var argumentBuilder = new List<string>();
argumentBuilder.Add("-loglevel panic");
argumentBuilder.Add("-f h264");
argumentBuilder.Add("-i pipe:");
argumentBuilder.Add("-c:v libx264");
argumentBuilder.Add("-bf 0");
argumentBuilder.Add("-pix_fmt yuv420p");
argumentBuilder.Add("-an");
argumentBuilder.Add(filename);

startInfo.Arguments = string.Join(" ", argumentBuilder.ToArray());

var _ffMpegProcess = new Process();
_ffMpegProcess.EnableRaisingEvents = true;
_ffMpegProcess.OutputDataReceived += (s, e) => { Debug.WriteLine(e.Data); };
_ffMpegProcess.ErrorDataReceived += (s, e) => { Debug.WriteLine(e.Data); };

_ffMpegProcess.StartInfo = startInfo;

Console.WriteLine($"[log] Starting write to {filename}...");

_ffMpegProcess.Start();
_ffMpegProcess.BeginOutputReadLine();
_ffMpegProcess.BeginErrorReadLine();

for (int i = 0; i < videoBuffer.Count; i++)
{
    _ffMpegProcess.StandardInput.BaseStream.Write(videoBuffer[i], 0, videoBuffer[i].Length);
}

_ffMpegProcess.StandardInput.BaseStream.Close();

我试图解决的挑战之一是写入两个输入管道,类似于我可以通过引用 pipe:4 或 [= 从 Node.js 做到这一点12=]。 似乎我只能直接写入标准输入而不能将其拆分为“通道”。

在 C# 中执行此操作的方法是什么?

根据所写内容 here 和一夜安眠(我梦见我可以使用 Stream.CopyAsync),这是解决方案的框架:

string pathToFFmpeg = @"C:\ffmpeg\bin\ffmpeg.exe";

string[] inputs = new[] { "video.m4v", "audio.mp3" };

string output = "output2.mp4";

var npsss = new NamedPipeServerStream[inputs.Length];
var fss = new FileStream[inputs.Length];

try
{
    for (int i = 0; i < fss.Length; i++)
    {
        fss[i] = File.OpenRead(inputs[i]);
    }

    // We use Guid for pipeNames
    var pipeNames = Array.ConvertAll(inputs, x => Guid.NewGuid().ToString("N"));

    for (int i = 0; i < npsss.Length; i++)
    {
        npsss[i] = new NamedPipeServerStream(pipeNames[i], PipeDirection.Out, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
    }

    string pipeNamesFFmpeg = string.Join(" ", pipeNames.Select(x => $@"-i \.\pipe\{x}"));

    using (var proc = new Process
    {
        StartInfo = new ProcessStartInfo
        {
            FileName = pathToFFmpeg,
            Arguments = $@"-loglevel debug -y {pipeNamesFFmpeg} -c:v copy -c:a copy ""{output}""",
            UseShellExecute = false,
        }
    })
    {
        Console.WriteLine($"FFMpeg path: {pathToFFmpeg}");
        Console.WriteLine($"Arguments: {proc.StartInfo.Arguments}");

        proc.EnableRaisingEvents = false;
        proc.Start();

        var tasks = new Task[npsss.Length];

        for (int i = 0; i < npsss.Length; i++)
        {
            var pipe = npsss[i];
            var fs = fss[i];

            pipe.WaitForConnection();

            tasks[i] = fs.CopyToAsync(pipe)
                // .ContinueWith(_ => pipe.FlushAsync()) // Flush does nothing on Pipes
                .ContinueWith(x => {
                    pipe.WaitForPipeDrain();
                    pipe.Disconnect();
                });
        }

        Task.WaitAll(tasks);

        proc.WaitForExit();
    }
}
finally
{
    foreach (var fs in fss)
    {
        fs?.Dispose();
    }

    foreach (var npss in npsss)
    {
        npss?.Dispose();
    }
}

有各种注意点:

  • 并非所有格式都与管道兼容。例如,许多 .mp4 不是,因为它们的 moov 原子位于文件末尾,但 ffmpeg 立即需要它,并且管道不可搜索(ffmpeg 无法到达管道末尾,请阅读 moov atom 然后转到管道的开头)。例如参见 [​​=18=]

  • 我在流媒体结束时收到错误消息。该文件似乎是正确的。我不知道为什么。其他人发信号了,但我没看到任何解释

    \.\pipeafc0c8e95f4a4c9cec5ae492bc518a: 参数无效 \.\pipe205c79c26a410aa46b9b35eb3bbff6: 无效参数

  • 我一般不使用TaskAsync,所以我不能100%确定我写的是否正确。此代码不起作用,例如:

    tasks[i] = pipe.WaitForConnectionAsync().ContinueWith(x => fs.CopyToAsync(pipe, 4096)).ContinueWith(...);
    

    emmmh也许最后可以解决:

    tasks[i] = ConnectAndCopyToPipe(fs, pipe);
    

    然后

    public static async Task ConnectAndCopyToPipe(FileStream fs, NamedPipeServerStream pipe)
    {
        await pipe.WaitForConnectionAsync();
        await fs.CopyToAsync(pipe);
        // await fs.FlushAsync(); // Does nothing
        pipe.WaitForPipeDrain();
        pipe.Disconnect();
    }