2012-06-10 00:53:19 +00:00
using System ;
using System.Collections.Generic ;
2017-04-18 17:27:44 +00:00
using System.IO ;
2012-06-10 00:53:19 +00:00
using System.Text ;
using System.Diagnostics ;
2017-04-18 17:27:44 +00:00
using System.Windows.Forms ;
2012-06-10 00:53:19 +00:00
2013-10-27 07:54:00 +00:00
using BizHawk.Client.Common ;
2019-05-18 10:17:02 +00:00
using BizHawk.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.")]
2017-04-18 17:27:44 +00:00
public class FFmpegWriter : IVideoWriter
2012-06-10 00:53:19 +00:00
{
/// <summary>
/// handle to external ffmpeg process
/// </summary>
2017-04-18 17:27:44 +00:00
private Process _ffmpeg ;
2012-06-10 00:53:19 +00:00
/// <summary>
/// the commandline actually sent to ffmpeg; for informative purposes
/// </summary>
2017-04-18 17:27:44 +00:00
private string _commandline ;
2012-06-10 00:53:19 +00:00
/// <summary>
/// current file segment (for multires)
/// </summary>
2017-04-18 17:27:44 +00:00
private int _segment ;
2012-06-10 00:53:19 +00:00
/// <summary>
/// base filename before segment number is attached
/// </summary>
2017-04-18 17:27:44 +00:00
private string _baseName ;
2012-06-10 00:53:19 +00:00
/// <summary>
/// recent lines in ffmpeg's stderr, for informative purposes
/// </summary>
2017-04-18 17:27:44 +00:00
private Queue < string > _stderr ;
2012-06-10 00:53:19 +00:00
/// <summary>
/// number of lines of stderr to buffer
/// </summary>
2017-04-18 17:27:44 +00:00
private const int Consolebuffer = 5 ;
2012-06-10 00:53:19 +00:00
2012-06-13 17:22:45 +00:00
/// <summary>
/// muxer handle for the current segment
/// </summary>
2017-04-18 17:27:44 +00:00
private NutMuxer _muxer ;
2012-06-13 17:22:45 +00:00
2012-06-16 16:51:47 +00:00
/// <summary>
/// codec token in use
/// </summary>
2017-04-18 17:27:44 +00:00
private FFmpegWriterForm . FormatPreset _token ;
2012-06-16 16:51:47 +00:00
/// <summary>
/// file extension actually used
/// </summary>
2017-04-18 17:27:44 +00:00
private string _ext ;
2012-06-16 16:51:47 +00:00
2017-04-18 17:27:44 +00:00
public void SetFrame ( int frame )
{
}
2014-06-18 02:28:07 +00:00
2012-06-13 17:22:45 +00:00
public void OpenFile ( string baseName )
2012-06-10 00:53:19 +00:00
{
2017-04-18 17:27:44 +00:00
_baseName = Path . Combine (
Path . GetDirectoryName ( baseName ) ,
Path . GetFileNameWithoutExtension ( baseName ) ) ;
2012-06-10 00:53:19 +00:00
2017-04-18 17:27:44 +00:00
_ext = Path . GetExtension ( baseName ) ;
2012-06-10 00:53:19 +00:00
2017-04-18 17:27:44 +00:00
_segment = 0 ;
2012-06-10 00:53:19 +00:00
OpenFileSegment ( ) ;
}
/// <summary>
/// starts an ffmpeg process and sets up associated sockets
/// </summary>
2017-04-18 17:27:44 +00:00
private void OpenFileSegment ( )
2012-06-10 00:53:19 +00:00
{
2014-07-30 01:35:48 +00:00
try
{
2019-08-12 10:00:42 +00:00
_ffmpeg = OSTailoredCode . ConstructSubshell (
2019-11-04 04:30:05 +00:00
OSTailoredCode . IsUnixHost ? "ffmpeg" : Path . Combine ( PathManager . GetDllDirectory ( ) , "ffmpeg.exe" ) ,
2019-08-12 10:00:42 +00:00
$"-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
) ;
2012-06-10 00:53:19 +00:00
2019-03-18 14:06:37 +00:00
_commandline = $"ffmpeg {_ffmpeg.StartInfo.Arguments}" ;
2012-06-10 00:53:19 +00:00
2017-04-18 17:27:44 +00:00
_ffmpeg . ErrorDataReceived + = new DataReceivedEventHandler ( StderrHandler ) ;
2012-06-10 00:53:19 +00:00
2017-04-18 17:27:44 +00:00
_stderr = new Queue < string > ( Consolebuffer ) ;
2012-06-10 00:53:19 +00:00
2017-04-18 17:27:44 +00:00
_ffmpeg . Start ( ) ;
2014-07-30 01:35:48 +00:00
}
catch
{
2017-04-18 17:27:44 +00:00
_ffmpeg . Dispose ( ) ;
_ffmpeg = null ;
2014-07-30 01:35:48 +00:00
throw ;
}
2012-06-13 17:22:45 +00:00
2017-04-18 17:27:44 +00:00
_ffmpeg . BeginErrorReadLine ( ) ;
_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>
2017-04-18 17:27:44 +00:00
private void StderrHandler ( object p , DataReceivedEventArgs line )
2012-06-10 00:53:19 +00:00
{
2017-04-18 17:27:44 +00:00
if ( ! string . IsNullOrEmpty ( line . Data ) )
2012-06-10 00:53:19 +00:00
{
2017-04-18 17:27:44 +00:00
if ( _stderr . Count = = Consolebuffer )
{
_stderr . Dequeue ( ) ;
}
2019-03-18 14:06:37 +00:00
_stderr . Enqueue ( $"{line.Data}\n" ) ;
2012-06-10 00:53:19 +00:00
}
}
/// <summary>
/// finishes an ffmpeg process
/// </summary>
2017-04-18 17:27:44 +00:00
private void CloseFileSegment ( )
2012-06-10 00:53:19 +00:00
{
2017-04-18 17:27:44 +00:00
_muxer . Finish ( ) ;
2012-06-13 17:22:45 +00:00
//ffmpeg.StandardInput.Close();
2012-06-10 00:53:19 +00:00
// how long should we wait here?
2017-04-18 17:27:44 +00:00
_ffmpeg . WaitForExit ( 20000 ) ;
_ffmpeg . Dispose ( ) ;
_ffmpeg = null ;
_stderr = null ;
_commandline = null ;
_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 ( ) ;
2017-04-18 17:27:44 +00:00
_baseName = null ;
2012-06-10 00:53:19 +00:00
}
/// <summary>
/// returns a string containing the commandline sent to ffmpeg and recent console (stderr) output
/// </summary>
2017-04-18 17:27:44 +00:00
private string ffmpeg_geterror ( )
2012-06-10 00:53:19 +00:00
{
2017-04-18 17:27:44 +00:00
if ( _ffmpeg . StartInfo . RedirectStandardError )
2012-06-10 00:53:19 +00:00
{
2017-04-18 17:27:44 +00:00
_ffmpeg . CancelErrorRead ( ) ;
2012-06-10 00:53:19 +00:00
}
2017-04-18 17:27:44 +00:00
var s = new StringBuilder ( ) ;
s . Append ( _commandline ) ;
2012-06-10 00:53:19 +00:00
s . Append ( '\n' ) ;
2017-04-18 17:27:44 +00:00
while ( _stderr . Count > 0 )
2012-06-10 00:53:19 +00:00
{
2017-04-18 17:27:44 +00:00
var foo = _stderr . Dequeue ( ) ;
2012-06-10 00:53:19 +00:00
s . Append ( foo ) ;
}
2017-04-18 17:27:44 +00:00
2012-06-10 00:53:19 +00:00
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 )
2017-04-18 17:27:44 +00:00
{
2012-06-10 00:53:19 +00:00
SetVideoParameters ( source . BufferWidth , source . BufferHeight ) ;
2017-04-18 17:27:44 +00:00
}
2012-06-10 00:53:19 +00:00
2017-04-18 17:27:44 +00:00
if ( _ffmpeg . HasExited )
{
2019-03-18 14:06:37 +00:00
throw new Exception ( $"unexpected ffmpeg death:\n{ffmpeg_geterror()}" ) ;
2017-04-18 17:27:44 +00:00
}
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
{
2017-04-18 17:27:44 +00:00
_muxer . WriteVideoFrame ( video ) ;
2012-06-16 16:51:47 +00:00
}
catch
{
2019-03-18 14:06:37 +00:00
MessageBox . Show ( $"Exception! ffmpeg history:\n{ffmpeg_geterror()}" ) ;
2012-06-16 16:51:47 +00:00
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
}
2017-04-18 17:27:44 +00:00
public IDisposable AcquireVideoCodecToken ( 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 )
2017-04-18 17:27:44 +00:00
{
_token = ( FFmpegWriterForm . FormatPreset ) token ;
}
2012-06-16 16:51:47 +00:00
else
2017-04-18 17:27:44 +00:00
{
2019-03-28 03:17:14 +00:00
throw new ArgumentException ( $"{nameof(FFmpegWriter)} can only take its own codec tokens!" ) ;
2017-04-18 17:27:44 +00:00
}
2012-06-10 00:53:19 +00:00
}
/// <summary>
/// video params
/// </summary>
2017-04-18 17:27:44 +00:00
private 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 .
* /
2017-04-18 17:27:44 +00:00
if ( _ffmpeg ! = null )
2012-06-10 00:53:19 +00:00
{
CloseFileSegment ( ) ;
2017-04-18 17:27:44 +00:00
_segment + + ;
2012-06-10 00:53:19 +00:00
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 ( )
{
2017-04-18 17:27:44 +00:00
if ( _ffmpeg ! = null )
{
2012-07-23 00:33:30 +00:00
CloseFile ( ) ;
2017-04-18 17:27:44 +00:00
}
2012-06-13 17:22:45 +00:00
}
public void AddSamples ( short [ ] samples )
{
2017-04-18 17:27:44 +00:00
if ( _ffmpeg . HasExited )
{
2019-03-18 14:06:37 +00:00
throw new Exception ( $"unexpected ffmpeg death:\n{ffmpeg_geterror()}" ) ;
2017-04-18 17:27:44 +00:00
}
2014-10-11 03:33:09 +00:00
if ( samples . Length = = 0 )
{
// has special meaning for the muxer, so don't pass on
return ;
}
2017-04-18 17:27:44 +00:00
2012-06-16 16:51:47 +00:00
try
{
2017-04-18 17:27:44 +00:00
_muxer . WriteAudioFrame ( samples ) ;
2012-06-16 16:51:47 +00:00
}
catch
{
2019-03-18 14:06:37 +00:00
MessageBox . Show ( $"Exception! ffmpeg history:\n{ffmpeg_geterror()}" ) ;
2012-06-16 16:51:47 +00:00
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 )
2017-04-18 17:27:44 +00:00
{
2017-04-10 12:36:42 +00:00
throw new ArgumentOutOfRangeException ( nameof ( bits ) , "Sampling depth must be 16 bits!" ) ;
2017-04-18 17:27:44 +00:00
}
2012-06-13 17:22:45 +00:00
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
2018-09-15 21:31:09 +00:00
return _token . Extension ;
2012-06-13 19:50:50 +00:00
}
2012-07-23 00:33:30 +00:00
public void SetDefaultVideoCodecToken ( )
{
2017-04-18 17:27:44 +00:00
_token = FFmpegWriterForm . FormatPreset . GetDefaultPreset ( ) ;
2012-07-23 00:33:30 +00:00
}
2016-03-05 23:19:12 +00:00
2017-04-18 17:27:44 +00:00
public bool UsesAudio = > true ;
public bool UsesVideo = > true ;
2012-06-10 00:53:19 +00:00
}
}