using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Diagnostics; using System.Windows.Forms; using BizHawk.Client.Common; using BizHawk.Common; using BizHawk.Emulation.Common; namespace BizHawk.Client.EmuHawk { /// /// uses pipes to launch an external ffmpeg process and encode /// [VideoWriter("ffmpeg", "FFmpeg writer", "Uses an external FFMPEG process to encode video and audio. Various formats supported. Splits on resolution change.")] public class FFmpegWriter : IVideoWriter { /// /// handle to external ffmpeg process /// private Process _ffmpeg; /// /// the commandline actually sent to ffmpeg; for informative purposes /// private string _commandline; /// /// current file segment (for multires) /// private int _segment; /// /// base filename before segment number is attached /// private string _baseName; /// /// recent lines in ffmpeg's stderr, for informative purposes /// private Queue _stderr; /// /// number of lines of stderr to buffer /// private const int Consolebuffer = 5; /// /// muxer handle for the current segment /// private NutMuxer _muxer; /// /// codec token in use /// private FFmpegWriterForm.FormatPreset _token; /// /// file extension actually used /// private string _ext; public void SetFrame(int frame) { } public void OpenFile(string baseName) { _baseName = Path.Combine( Path.GetDirectoryName(baseName), Path.GetFileNameWithoutExtension(baseName)); _ext = Path.GetExtension(baseName); _segment = 0; OpenFileSegment(); } /// /// starts an ffmpeg process and sets up associated sockets /// private void OpenFileSegment() { try { _ffmpeg = OSTailoredCode.ConstructSubshell( OSTailoredCode.IsUnixHost ? "ffmpeg" : Path.Combine(PathManager.GetDllDirectory(), "ffmpeg.exe"), $"-y -f nut -i - {_token.Commandline} \"{_baseName}{(_segment == 0 ? string.Empty : $"_{_segment}")}{_ext}\"", checkStdout: false, checkStderr: true // ffmpeg sends informative display to stderr, and nothing to stdout ); _commandline = $"ffmpeg {_ffmpeg.StartInfo.Arguments}"; _ffmpeg.ErrorDataReceived += new DataReceivedEventHandler(StderrHandler); _stderr = new Queue(Consolebuffer); _ffmpeg.Start(); } catch { _ffmpeg.Dispose(); _ffmpeg = null; throw; } _ffmpeg.BeginErrorReadLine(); _muxer = new NutMuxer(width, height, fpsnum, fpsden, sampleRate, channels, _ffmpeg.StandardInput.BaseStream); } /// /// saves stderr lines from ffmpeg in a short queue /// private 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 /// private void CloseFileSegment() { _muxer.Finish(); //ffmpeg.StandardInput.Close(); // how long should we wait here? _ffmpeg.WaitForExit(20000); _ffmpeg.Dispose(); _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 /// private string ffmpeg_geterror() { if (_ffmpeg.StartInfo.RedirectStandardError) { _ffmpeg.CancelErrorRead(); } var s = new StringBuilder(); s.Append(_commandline); s.Append('\n'); while (_stderr.Count > 0) { var foo = _stderr.Dequeue(); s.Append(foo); } return s.ToString(); } /// FFmpeg call failed 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 video = source.GetVideoBuffer(); try { _muxer.WriteVideoFrame(video); } catch { 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(IWin32Window hwnd) { return FFmpegWriterForm.DoFFmpegWriterDlg(hwnd); } /// does not inherit public void SetVideoCodecToken(IDisposable token) { if (token is FFmpegWriterForm.FormatPreset) { _token = (FFmpegWriterForm.FormatPreset)token; } else { throw new ArgumentException($"{nameof(FFmpegWriter)} can only take its own codec tokens!"); } } /// /// video params /// private 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(); } } /// FFmpeg call failed public void AddSamples(short[] samples) { if (_ffmpeg.HasExited) { throw new Exception($"unexpected ffmpeg death:\n{ffmpeg_geterror()}"); } if (samples.Length == 0) { // has special meaning for the muxer, so don't pass on return; } try { _muxer.WriteAudioFrame(samples); } catch { MessageBox.Show($"Exception! ffmpeg history:\n{ffmpeg_geterror()}"); throw; } } /// is not 16 public void SetAudioParameters(int sampleRate, int channels, int bits) { if (bits != 16) { throw new ArgumentOutOfRangeException(nameof(bits), "Sampling depth must be 16 bits!"); } this.sampleRate = sampleRate; this.channels = channels; } public string DesiredExtension() { // this needs to interface with the codec token return _token.Extension; } public void SetDefaultVideoCodecToken() { _token = FFmpegWriterForm.FormatPreset.GetDefaultPreset(); } public bool UsesAudio => true; public bool UsesVideo => true; } }