3

I'm trying to encode a video file with FFmpeg from frames generated by my program and then redirect the output of FFmpeg back to my program to avoid having an intermediate video file.

However, I've run into what seems to be a rather common problem when redirecting outputs in System.Diagnostic.Process, mentioned in the remarks of the documentation here, which is that it causes a deadlock if run synchronously.

After tearing my hair out over this for a day, and trying several proposed solutions found online, I still cannot find a way to make it work. I get some data out, but the process always freezes before it finishes.


Here is a code snippet that produces said problem:

static void Main(string[] args)
    {

        Process proc = new Process();

        proc.StartInfo.FileName = @"ffmpeg.exe";
        proc.StartInfo.Arguments = String.Format("-f rawvideo -vcodec rawvideo -s {0}x{1} -pix_fmt rgb24 -r {2} -i - -an -codec:v libx264 -preset veryfast -f mp4 -movflags frag_keyframe+empty_moov -",
            16, 9, 30);
        proc.StartInfo.UseShellExecute = false;
        proc.StartInfo.RedirectStandardInput = true;
        proc.StartInfo.RedirectStandardOutput = true;

        FileStream fs = new FileStream(@"out.mp4", FileMode.Create, FileAccess.Write);

        //read output asynchronously
        using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
        {
            proc.OutputDataReceived += (sender, e) =>
            {
                if (e.Data == null)
                {
                    outputWaitHandle.Set();
                }
                else
                {
                    string str = e.Data;
                    byte[] bytes = new byte[str.Length * sizeof(char)];
                    System.Buffer.BlockCopy(str.ToCharArray(), 0, bytes, 0, bytes.Length);
                    fs.Write(bytes, 0, bytes.Length);
                }
            };
        }


        proc.Start();
        proc.BeginOutputReadLine();

        //Generate frames and write to stdin
        for (int i = 0; i < 30*60*60; i++)
        {
            byte[] myArray = Enumerable.Repeat((byte)Math.Min(i,255), 9*16*3).ToArray();
            proc.StandardInput.BaseStream.Write(myArray, 0, myArray.Length);
        }

        proc.WaitForExit();
        fs.Close();
        Console.WriteLine("Done!");
        Console.ReadKey();

    }

Currently i'm trying to write the output to a file anyway for debugging purposes, but this is not how the data will eventually be used.

If anyone knows a solution it would be very much appreciated.

Nobbe
  • 279
  • 4
  • 11

1 Answers1

3

Your process hangs because you never tell ffmpeg that you're done writing to the StandardInput stream. So it's still sitting there waiting for you to send it more data.

Personally, I think it would be more reliable and easier to code to use regular files as the input and output options for ffmpeg (i.e. infile and outfile in the command line arguments). Then you don't need to redirect any stream.

But, if you do want or need to redirect yourself — for example, you are in fact generating the input data independent of some file, and you are consuming the output data in some way other than a file — you can get it to work by using the Process class and its properties correctly.

In particular, this means a couple of things:

  1. You can't treat the output data as characters when the program is producing raw byte data for the stream.
  2. You must close the input stream when you are done, so that the program knows that the end of the input stream has been reached.

Here is a version of your program that actually works:

static void Main(string[] args)
{
    Process proc = new Process();

    proc.StartInfo.FileName = @"ffmpeg.exe";
    proc.StartInfo.Arguments = String.Format("-f rawvideo -vcodec rawvideo -s {0}x{1} -pix_fmt rgb24 -r {2} -i - -an -codec:v libx264 -preset veryfast -f mp4 -movflags frag_keyframe+empty_moov -",
        16, 9, 30);
    proc.StartInfo.UseShellExecute = false;
    proc.StartInfo.RedirectStandardInput = true;
    proc.StartInfo.RedirectStandardOutput = true;

    FileStream fs = new FileStream(@"out.mp4", FileMode.Create, FileAccess.Write);

    proc.Start();
    var readTask = _ConsumeOutputAsync(fs, proc.StandardOutput.BaseStream);

    //Generate frames and write to stdin
    for (int i = 0; i < 30 * 60 * 60; i++)
    {
        byte[] myArray = Enumerable.Repeat((byte)Math.Min(i, 255), 9 * 16 * 3).ToArray();
        proc.StandardInput.BaseStream.Write(myArray, 0, myArray.Length);
    }

    proc.StandardInput.BaseStream.Close();

    readTask.Wait();
    fs.Close();
    Console.WriteLine("Done!");
    Console.ReadKey();
}

private static async Task _ConsumeOutputAsync(FileStream fs, Stream baseStream)
{
    int cb;
    byte[] rgb = new byte[4096];

    while ((cb = await baseStream.ReadAsync(rgb, 0, rgb.Length)) > 0)
    {
        fs.Write(rgb, 0, cb);
    }
}

Note that even if you are generating the data dynamically, your example shows you writing the output to a file. If you really want the output in a file, then I would definitely use the outfile command line parameter to specify the output file and not redirect StandardOutput yourself. Why inject your own code in the handling of the data when the external process you're running will handle all that work for you?

I still have no idea what you were trying to do with the AutoResetEvent object, since you never waited on the object, and your implementation of it was broken, because you disposed the object before you ever got around to using it. So nothing about the above revised example attempts to replicate that behavior. It works fine without it.

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • This did solve my problem, so thanks for that. I just want to clarify that I did actually specify in my question that I was only writing to file for debug purposes, I wouldn't be redirecting sdtout unless I needed to. You're right about the `AutoResetEvent` though, remnants from the suggested solution to use `OutputDataReceived`. I had already tried a solution using `async Task` that didn't work (because it failed to mention closing the input stream). Granted, closing the input stream does seem rather obvious, it didn't cross my mind since I didn't have to do that when not redirecting stdout. – Nobbe Jul 21 '17 at 05:53