FFMPEG .Net Wrapper – Part 3. Working with Events

By | March 6, 2018

The way to start encoding with command line tool is by using Process class. In basic form we have to provide arguments and a path to executable file. We have to also initiate Start and WaitForExit methods. Now all we have to do is to wait for entire encoding process to finish. If we write some simple method that instantiates Process class in Windows Form or WPF application, we will see console window with FFMPEG statistics. However, if we would like to send some message to console before or after encoding process starts, we will see that there are actually two different console windows. One is used exclusively by FFMPEG, and the other one is used by our program. At this point things get bit complicated because we have to capture and parse FFMPEG output.

The proper way to do it is by redirecting FFMPEG output to our primary console window and read the stream internally. There are actually two different streams of data that we have to consider. One of them is called Standard Output and the second one is called Standard Error. In most cases command line tools use Standard Output for displaying current information to a user. FFMPEG is different in that regard. All the logging and progress information is tunneled through Standard Error.

There is an article on MSDN about redirecting Standard Output property. It describes in details things that we have to pay attention to. One thing we have to be aware is that we cannot and should not read both Standard Output and Standard Error outputs at the same. This would lead to the FFMPEG locks up. To avoid these potential deadlocks we will have to do reading operations asynchronously.

Process class allows us to raise events. The way events work is that we can send data through a pipeline called delegate. First we need to raise an event and verify that there is another object on other side that is listening. In order to receive the data, both object that sends data and listener must use the same data type. There are already predefined event types that we can use, but we can also create our own data types depending on what we want to communicate to other objects.

The flow of the operation is that we will subscribe to ErrorDataReceived event available to us in Process class. We will read this data in a private method that would raise event OnVideoEncoding defined in our class and transform data into a custom Event object called EncodingEventArgs.

Our class has two additional events, VideoEncoded and Exited. The first one would send object called EncodingJob when conversion process is completed. The Exited event would send Process exit code. The success code is 0, failed code is 1.



public class EncodingEngine
    {
        private Process _process;
        private string _encoderPath;
        
        public event EventHandler VideoEncoded;
        public event EventHandler VideoEncoding;
        public event EventHandler Exited;
        
        public EncodingEngine(string encoderPath)
        {
            _encoderPath = encoderPath;
            _process = new Process();

        }

        public void Cancel()
        {

            StreamWriter myStreamWriter = this._process.StandardInput;
            myStreamWriter.WriteLine("q");


        }

        public void DoWork(EncodingJob encodingJob)
        {

            this._process.EnableRaisingEvents = true;
            
	   this._process.OutputDataReceived += new DataReceivedEventHandler(this.GetStandardOutputDataReceived);
           
		//subscribe to event
            this._process.ErrorDataReceived += new DataReceivedEventHandler(this.GetStandardErrorDataReceived);

            this._process.Exited += new EventHandler(ProcessExited);

            this._process.StartInfo.FileName = _encoderPath;

            this._process.StartInfo.Arguments = encodingJob.Arguments;

            this._process.StartInfo.UseShellExecute = false;
            this._process.StartInfo.RedirectStandardError = true;
            this._process.StartInfo.RedirectStandardOutput = true;
            this._process.StartInfo.RedirectStandardInput = true;
            this._process.StartInfo.CreateNoWindow = true;
            this._process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;

            this._process.Start();
            this._process.BeginErrorReadLine();
            this._process.BeginOutputReadLine();

            this._process.WaitForExit();
            this._process.Close();
            
		//raise event
             OnVideoEncoded(new EncodedEventArgs() {EncodingJob = encodingJob });

        }


        protected virtual void OnVideoEncoded(EncodedEventArgs e)
        {
		
            VideoEncoded?.Invoke(this,e);

        }

       
        protected virtual void OnVideoEncoding(EncodingEventArgs e)
        {

            VideoEncoding?.Invoke(this, e);

        }

        protected virtual void OnExit(ExitedEventArgs e)
        {

            Exited?.Invoke(this, e);

        }


        protected virtual void OnErrorReceived(ErrorEventArgs e)
        {

            ErrorReceived?.Invoke(this, e);

        }


        private void ProcessExited(object sender, EventArgs e)
        {
            OnExit(new ExitedEventArgs() {ExitCode = _process.ExitCode.ToString()});
          
        }

        private void GetStandardErrorDataReceived(object sender, DataReceivedEventArgs e)
        {

             //raise event        
            OnVideoEncoding(new EncodingEventArgs() {

                Frame = e.Data.GetRegexValue(RegexKey.Frame,RegexGroup.Two),             
                Fps = e.Data.GetRegexValue(RegexKey.Fps, RegexGroup.Two),
                Size = e.Data.GetRegexValue(RegexKey.Size, RegexGroup.Two),
                Time = e.Data.GetRegexValue(RegexKey.Time, RegexGroup.Two),
                Bitrate = e.Data.GetRegexValue(RegexKey.Bitrate, RegexGroup.Two),
                Speed = e.Data.GetRegexValue(RegexKey.Speed, RegexGroup.Two),
                Quantizer = e.Data.GetRegexValue(RegexKey.Quantizer, RegexGroup.Two),
                Progress = e.Data.GetRegexValue(RegexKey.Time, RegexGroup.Two).ParseTotalSeconds(),
                Data = e.Data } );
        
        }

        private void GetStandardOutputDataReceived(object sender, DataReceivedEventArgs e)
        {
            //Console.WriteLine(e.Data);

        }


    }

public class EncodingEventArgs: EventArgs
    {  
        public string Frame { get; set; }
        public string Fps { get; set; }
        public string Size { get; set; }
        public string Time { get; set; }  
        public string Bitrate { get; set; }
        public string Speed { get; set;}
        public string Quantizer { get; set; }
        public string Data { get; set;}
        public double Progress { get; set; }
    }

public class EncodedEventArgs: EventArgs
    {          
        public EncodingJob EncodingJob {get;set;}
    }

public class ExitedEventArgs
    {
        public string ExitCode { get; set; }

    }

There is one more thing we have to talk about and that is EncodingJob.


public class EncodingJob
    {
        public string Arguments { get; set; }
        public object Metadata {get;set;}
    }

It has only two properties. Arguments property is rather straight forward. It would include input and output file plus required parameters. Metadata property could be any additional object that we would like to send to other objects that subscribe to VideoEncoded event. This could be useful if we would like to do some post-processing after video conversion is completed. The only thing we need to remember is that we have to cast Metadata back to the object that we initially send.

There is one more method in this class that we did not talk about and it is Cancel method. It would allow us to halt encoding process. In order to do that we have to send to FFMPEG keyboard key “q”. Unfortunately our interaction with FFMPEG is very limited. Note that for this to work we have redirected Standard Input too.

Leave a Reply

Your email address will not be published. Required fields are marked *