Search code examples
c#ffmpegprocessconsole-applicationstdin

Writing to two standard input pipes from C#


I am using FFMPEG from my C# application to build out the video stream from raw unencoded frames. For just one input stream this is fairly straightforward:

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();

One of the challenges that I am trying to address is writing to two input pipes, similar to how I could do that from, say, Node.js, by referring to pipe:4 or pipe:5. It seems that I can only write to standard input directly but not split it into "channels".

What's the approach to do this in C#?


Solution

  • Based on what is written here and on a good night of sleep (where I dreamt that I could use Stream.CopyAsync), this is the skeleton of the solution:

    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();
        }
    }
    

    There are various attention points:

    • Not all formats are compatible with pipes. For example many .mp4 aren't, because they have their moov atom towards the end of the file, but ffmpeg needs it immediately, and pipes aren't searchable (ffmpeg can't go to the end of the pipe, read the moov atom and then go to the beginning of the pipe). See here for example

    • I receive an error at the end of the streaming. The file seems to be correct. I don't know why. Some other persons signaled it but I haven't seen any explanation

      \.\pipe\55afc0c8e95f4a4c9cec5ae492bc518a: Invalid argument \.\pipe\92205c79c26a410aa46b9b35eb3bbff6: Invalid argument

    • I don't normally use Task and Async, so I'm not 100% sure if what I wrote is correct. This code doesn't work for example:

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

      Mmmmh perhaps the last can be solved:

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

      and then

      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();
      }