2012-06-10 00:53:19 +00:00
using System ;
using System.Collections.Generic ;
using System.Text ;
using System.Diagnostics ;
2013-10-27 07:54:00 +00:00
using BizHawk.Client.Common ;
2013-11-04 01:39:19 +00:00
using BizHawk.Emulation.Common ;
2013-10-27 07:54:00 +00:00
2013-11-03 03:54:37 +00:00
namespace BizHawk.Client.EmuHawk
2012-06-10 00:53:19 +00:00
{
/// <summary>
2012-06-13 17:22:45 +00:00
/// uses pipes to launch an external ffmpeg process and encode
2012-06-10 00:53:19 +00:00
/// </summary>
2014-10-10 18:09:00 +00:00
[VideoWriter("ffmpeg", "FFmpeg writer", "Uses an external FFMPEG process to encode video and audio. Various formats supported. Splits on resolution change.")]
2012-06-13 17:22:45 +00:00
class FFmpegWriter : IVideoWriter
2012-06-10 00:53:19 +00:00
{
/// <summary>
/// handle to external ffmpeg process
/// </summary>
Process ffmpeg ;
/// <summary>
/// the commandline actually sent to ffmpeg; for informative purposes
/// </summary>
string commandline ;
/// <summary>
/// current file segment (for multires)
/// </summary>
int segment ;
/// <summary>
/// base filename before segment number is attached
/// </summary>
string baseName ;
/// <summary>
/// recent lines in ffmpeg's stderr, for informative purposes
/// </summary>
Queue < string > stderr ;
/// <summary>
/// number of lines of stderr to buffer
/// </summary>
const int consolebuffer = 5 ;
2012-06-13 17:22:45 +00:00
/// <summary>
/// muxer handle for the current segment
/// </summary>
NutMuxer muxer ;
2012-06-16 16:51:47 +00:00
/// <summary>
/// codec token in use
/// </summary>
FFmpegWriterForm . FormatPreset token ;
/// <summary>
/// file extension actually used
/// </summary>
string ext ;
2014-06-18 02:28:07 +00:00
public void SetFrame ( int frame ) { }
2012-06-13 17:22:45 +00:00
public void OpenFile ( string baseName )
2012-06-10 00:53:19 +00:00
{
2012-10-15 13:36:06 +00:00
this . baseName = System . IO . Path . Combine (
System . IO . Path . GetDirectoryName ( baseName ) ,
System . IO . Path . GetFileNameWithoutExtension ( baseName ) ) ;
2012-06-10 00:53:19 +00:00
2013-04-14 23:56:45 +00:00
ext = System . IO . Path . GetExtension ( baseName ) ;
2012-06-10 00:53:19 +00:00
segment = 0 ;
OpenFileSegment ( ) ;
}
/// <summary>
/// starts an ffmpeg process and sets up associated sockets
/// </summary>
void OpenFileSegment ( )
{
2014-07-30 01:35:48 +00:00
try
{
ffmpeg = new Process ( ) ;
2012-10-09 21:25:58 +00:00
#if WINDOWS
2014-07-30 01:35:48 +00:00
ffmpeg . StartInfo . FileName = System . IO . Path . Combine ( PathManager . GetBasePathAbsolute ( ) , "dll" , "ffmpeg.exe" ) ;
2012-10-09 21:25:58 +00:00
#else
2014-07-30 01:35:48 +00:00
ffmpeg . StartInfo . FileName = "ffmpeg" ; // expecting native version to be in path
2012-10-09 21:25:58 +00:00
#endif
2012-06-10 00:53:19 +00:00
2014-07-30 01:35:48 +00:00
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 ;
2012-06-10 00:53:19 +00:00
2014-07-30 01:35:48 +00:00
// ffmpeg sends informative display to stderr, and nothing to stdout
ffmpeg . StartInfo . RedirectStandardError = true ;
ffmpeg . StartInfo . RedirectStandardInput = true ;
ffmpeg . StartInfo . UseShellExecute = false ;
2012-06-10 00:53:19 +00:00
2014-07-30 01:35:48 +00:00
commandline = "ffmpeg " + ffmpeg . StartInfo . Arguments ;
2012-06-10 00:53:19 +00:00
2014-07-30 01:35:48 +00:00
ffmpeg . ErrorDataReceived + = new DataReceivedEventHandler ( StderrHandler ) ;
2012-06-10 00:53:19 +00:00
2014-07-30 01:35:48 +00:00
stderr = new Queue < string > ( consolebuffer ) ;
2012-06-10 00:53:19 +00:00
2014-07-30 01:35:48 +00:00
ffmpeg . Start ( ) ;
}
catch
{
ffmpeg . Dispose ( ) ;
ffmpeg = null ;
throw ;
}
2012-06-10 00:53:19 +00:00
ffmpeg . BeginErrorReadLine ( ) ;
2012-06-13 17:22:45 +00:00
muxer = new NutMuxer ( width , height , fpsnum , fpsden , sampleRate , channels , ffmpeg . StandardInput . BaseStream ) ;
2012-06-10 00:53:19 +00:00
}
/// <summary>
/// saves stderr lines from ffmpeg in a short queue
/// </summary>
/// <param name="p"></param>
/// <param name="line"></param>
void StderrHandler ( object p , DataReceivedEventArgs line )
{
if ( ! String . IsNullOrEmpty ( line . Data ) )
{
if ( stderr . Count = = consolebuffer )
stderr . Dequeue ( ) ;
stderr . Enqueue ( line . Data + "\n" ) ;
}
}
/// <summary>
/// finishes an ffmpeg process
/// </summary>
void CloseFileSegment ( )
{
2012-06-13 17:22:45 +00:00
muxer . Finish ( ) ;
//ffmpeg.StandardInput.Close();
2012-06-10 00:53:19 +00:00
// how long should we wait here?
ffmpeg . WaitForExit ( 20000 ) ;
2014-07-30 01:35:48 +00:00
ffmpeg . Dispose ( ) ;
2012-06-10 00:53:19 +00:00
ffmpeg = null ;
stderr = null ;
commandline = null ;
2012-06-13 17:22:45 +00:00
muxer = null ;
2012-06-10 00:53:19 +00:00
}
2012-06-13 17:22:45 +00:00
public void CloseFile ( )
2012-06-10 00:53:19 +00:00
{
CloseFileSegment ( ) ;
baseName = null ;
}
/// <summary>
/// returns a string containing the commandline sent to ffmpeg and recent console (stderr) output
/// </summary>
/// <returns></returns>
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 ( ) ;
}
2012-06-13 17:22:45 +00:00
public void AddFrame ( IVideoProvider source )
2012-06-10 00:53:19 +00:00
{
if ( source . BufferWidth ! = width | | source . BufferHeight ! = height )
SetVideoParameters ( source . BufferWidth , source . BufferHeight ) ;
if ( ffmpeg . HasExited )
throw new Exception ( "unexpected ffmpeg death:\n" + ffmpeg_geterror ( ) ) ;
2012-06-13 17:22:45 +00:00
2014-07-30 03:12:21 +00:00
var video = source . GetVideoBuffer ( ) ;
2012-06-16 16:51:47 +00:00
try
{
2014-07-30 03:12:21 +00:00
muxer . WriteVideoFrame ( video ) ;
2012-06-16 16:51:47 +00:00
}
catch
{
System . Windows . Forms . MessageBox . Show ( "Exception! ffmpeg history:\n" + ffmpeg_geterror ( ) ) ;
throw ;
}
2012-06-13 17:22:45 +00:00
2012-06-10 00:53:19 +00:00
// have to do binary write!
2012-06-13 17:22:45 +00:00
//ffmpeg.StandardInput.BaseStream.Write(b, 0, b.Length);
2012-06-10 00:53:19 +00:00
}
2012-06-16 16:51:47 +00:00
public IDisposable AcquireVideoCodecToken ( System . Windows . Forms . IWin32Window hwnd )
2012-06-10 00:53:19 +00:00
{
2012-06-16 16:51:47 +00:00
return FFmpegWriterForm . DoFFmpegWriterDlg ( hwnd ) ;
2012-06-10 00:53:19 +00:00
}
2012-06-13 17:22:45 +00:00
public void SetVideoCodecToken ( IDisposable token )
2012-06-10 00:53:19 +00:00
{
2012-06-16 16:51:47 +00:00
if ( token is FFmpegWriterForm . FormatPreset )
this . token = ( FFmpegWriterForm . FormatPreset ) token ;
else
throw new ArgumentException ( "FFmpegWriter can only take its own codec tokens!" ) ;
2012-06-10 00:53:19 +00:00
}
/// <summary>
/// video params
/// </summary>
2012-06-13 17:22:45 +00:00
int fpsnum , fpsden , width , height , sampleRate , channels ;
2012-06-10 00:53:19 +00:00
2012-06-13 17:22:45 +00:00
public void SetMovieParameters ( int fpsnum , int fpsden )
2012-06-10 00:53:19 +00:00
{
this . fpsnum = fpsnum ;
this . fpsden = fpsden ;
}
2012-06-13 17:22:45 +00:00
public void SetVideoParameters ( int width , int height )
2012-06-10 00:53:19 +00:00
{
this . width = width ;
this . height = height ;
2012-06-13 17:22:45 +00:00
/ * ffmpeg theoretically supports variable resolution videos , but in practice that ' s not handled very well .
* so we start a new segment .
* /
2012-06-10 00:53:19 +00:00
if ( ffmpeg ! = null )
{
CloseFileSegment ( ) ;
segment + + ;
OpenFileSegment ( ) ;
}
}
2012-06-13 17:22:45 +00:00
public void SetMetaData ( string gameName , string authors , ulong lengthMS , ulong rerecords )
2012-06-10 00:53:19 +00:00
{
// can be implemented with ffmpeg "-metadata" parameter???
// nyi
}
2012-06-13 17:22:45 +00:00
public void Dispose ( )
{
2012-07-23 00:33:30 +00:00
if ( ffmpeg ! = null )
CloseFile ( ) ;
2012-06-13 17:22:45 +00:00
}
public void AddSamples ( short [ ] samples )
{
2012-06-16 16:51:47 +00:00
if ( ffmpeg . HasExited )
throw new Exception ( "unexpected ffmpeg death:\n" + ffmpeg_geterror ( ) ) ;
2014-10-11 03:33:09 +00:00
if ( samples . Length = = 0 )
{
// has special meaning for the muxer, so don't pass on
return ;
}
2012-06-16 16:51:47 +00:00
try
{
2014-07-30 03:12:21 +00:00
muxer . WriteAudioFrame ( samples ) ;
2012-06-16 16:51:47 +00:00
}
catch
{
System . Windows . Forms . MessageBox . Show ( "Exception! ffmpeg history:\n" + ffmpeg_geterror ( ) ) ;
throw ;
}
2012-06-13 17:22:45 +00:00
}
public void SetAudioParameters ( int sampleRate , int channels , int bits )
2012-06-10 00:53:19 +00:00
{
2012-06-13 17:22:45 +00:00
if ( bits ! = 16 )
throw new ArgumentOutOfRangeException ( "sampling depth must be 16 bits!" ) ;
this . sampleRate = sampleRate ;
this . channels = channels ;
2012-06-10 00:53:19 +00:00
}
2012-06-13 19:50:50 +00:00
public string DesiredExtension ( )
{
// this needs to interface with the codec token
2012-06-16 16:51:47 +00:00
return token . defaultext ;
2012-06-13 19:50:50 +00:00
}
2012-07-23 00:33:30 +00:00
public void SetDefaultVideoCodecToken ( )
{
2013-04-14 23:56:45 +00:00
token = FFmpegWriterForm . FormatPreset . GetDefaultPreset ( ) ;
2012-07-23 00:33:30 +00:00
}
2012-06-10 00:53:19 +00:00
}
}