using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Diagnostics; namespace BizHawk.MultiClient { /// /// uses pipes to launch an external ffmpeg process and encode /// class FFmpegWriter : IVideoWriter { /// /// handle to external ffmpeg process /// Process ffmpeg; /// /// the commandline actually sent to ffmpeg; for informative purposes /// string commandline; /// /// current file segment (for multires) /// int segment; /// /// base filename before segment number is attached /// string baseName; /// /// recent lines in ffmpeg's stderr, for informative purposes /// Queue stderr; /// /// number of lines of stderr to buffer /// const int consolebuffer = 5; /// /// muxer handle for the current segment /// NutMuxer muxer; /// /// codec token in use /// FFmpegWriterForm.FormatPreset token; /// /// file extension actually used /// string ext; public void OpenFile(string baseName) { string s = System.IO.Path.GetFileNameWithoutExtension(baseName); ext = System.IO.Path.GetExtension(baseName); this.baseName = s; segment = 0; OpenFileSegment(); } /// /// starts an ffmpeg process and sets up associated sockets /// void OpenFileSegment() { ffmpeg = new Process(); ffmpeg.StartInfo.FileName = "ffmpeg"; string filename = String.Format("{0}_{1,4:D4}{2}", baseName, segment, ext); ffmpeg.StartInfo.Arguments = String.Format("-y -f nut -i - {1} \"{0}\"", filename, token.commandline); ffmpeg.StartInfo.CreateNoWindow = true; // ffmpeg sends informative display to stderr, and nothing to stdout ffmpeg.StartInfo.RedirectStandardError = true; ffmpeg.StartInfo.RedirectStandardInput = true; ffmpeg.StartInfo.UseShellExecute = false; commandline = "ffmpeg " + ffmpeg.StartInfo.Arguments; ffmpeg.ErrorDataReceived += new DataReceivedEventHandler(StderrHandler); stderr = new Queue(consolebuffer); ffmpeg.Start(); ffmpeg.BeginErrorReadLine(); muxer = new NutMuxer(width, height, fpsnum, fpsden, sampleRate, channels, ffmpeg.StandardInput.BaseStream); } /// /// saves stderr lines from ffmpeg in a short queue /// /// /// void StderrHandler(object p, DataReceivedEventArgs line) { if (!String.IsNullOrEmpty(line.Data)) { if (stderr.Count == consolebuffer) stderr.Dequeue(); stderr.Enqueue(line.Data + "\n"); } } /// /// finishes an ffmpeg process /// void CloseFileSegment() { muxer.Finish(); //ffmpeg.StandardInput.Close(); // how long should we wait here? ffmpeg.WaitForExit(20000); ffmpeg = null; stderr = null; commandline = null; muxer = null; } public void CloseFile() { CloseFileSegment(); baseName = null; } /// /// returns a string containing the commandline sent to ffmpeg and recent console (stderr) output /// /// string ffmpeg_geterror() { if (ffmpeg.StartInfo.RedirectStandardError) { ffmpeg.CancelErrorRead(); } StringBuilder s = new StringBuilder(); s.Append(commandline); s.Append('\n'); while (stderr.Count > 0) { var foo = stderr.Dequeue(); s.Append(foo); } return s.ToString(); } public void AddFrame(IVideoProvider source) { if (source.BufferWidth != width || source.BufferHeight != height) SetVideoParameters(source.BufferWidth, source.BufferHeight); if (ffmpeg.HasExited) throw new Exception("unexpected ffmpeg death:\n" + ffmpeg_geterror()); var a = source.GetVideoBuffer(); var b = new byte[a.Length * sizeof (int)]; Buffer.BlockCopy(a, 0, b, 0, b.Length); try { muxer.writevideoframe(b); } catch { System.Windows.Forms.MessageBox.Show("Exception! ffmpeg history:\n" + ffmpeg_geterror()); throw; } // have to do binary write! //ffmpeg.StandardInput.BaseStream.Write(b, 0, b.Length); } public IDisposable AcquireVideoCodecToken(System.Windows.Forms.IWin32Window hwnd) { return FFmpegWriterForm.DoFFmpegWriterDlg(hwnd); } public void SetVideoCodecToken(IDisposable token) { if (token is FFmpegWriterForm.FormatPreset) this.token = (FFmpegWriterForm.FormatPreset)token; else throw new ArgumentException("FFmpegWriter can only take its own codec tokens!"); } /// /// video params /// int fpsnum, fpsden, width, height, sampleRate, channels; public void SetMovieParameters(int fpsnum, int fpsden) { this.fpsnum = fpsnum; this.fpsden = fpsden; } public void SetVideoParameters(int width, int height) { this.width = width; this.height = height; /* ffmpeg theoretically supports variable resolution videos, but in practice that's not handled very well. * so we start a new segment. */ if (ffmpeg != null) { CloseFileSegment(); segment++; OpenFileSegment(); } } public void SetMetaData(string gameName, string authors, ulong lengthMS, ulong rerecords) { // can be implemented with ffmpeg "-metadata" parameter??? // nyi } public void Dispose() { if (ffmpeg != null) CloseFile(); } public void AddSamples(short[] samples) { if (ffmpeg.HasExited) throw new Exception("unexpected ffmpeg death:\n" + ffmpeg_geterror()); try { muxer.writeaudioframe(samples); } catch { System.Windows.Forms.MessageBox.Show("Exception! ffmpeg history:\n" + ffmpeg_geterror()); throw; } } public void SetAudioParameters(int sampleRate, int channels, int bits) { if (bits != 16) throw new ArgumentOutOfRangeException("sampling depth must be 16 bits!"); this.sampleRate = sampleRate; this.channels = channels; } public override string ToString() { return "ffmpeg writer"; } public string WriterDescription() { return "Uses an external FFMPEG process to encode video and audio. Various formats supported. Splits on resolution change."; } public string DesiredExtension() { // this needs to interface with the codec token return token.defaultext; } public void SetDefaultVideoCodecToken() { this.token = FFmpegWriterForm.FormatPreset.GetDefaultPreset(); } public string ShortName() { return "ffmpeg"; } } }