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";
}
}
}