using NativeEngine;
namespace Sandbox;
///
/// Allows the creation of video content by encoding a sequence of frames.
///
public sealed class VideoWriter : IDisposable
{
public struct Config
{
// note: keeping file save path out of this
// because we'll probably only want to expose that
// carefully, and allow other write options
public int Width;
public int Height;
public int FrameRate;
public int Bitrate;
public Codec Codec;
public Container Container;
///
/// Can this container support the codec.
///
public bool IsCodecSupported()
{
// Validate codec support based on container
return Container switch
{
Container.MP4 => Codec == Codec.H264 || Codec == Codec.H265,
Container.WebM => Codec == Codec.VP8 || Codec == Codec.VP9,
Container.WebP => Codec == Codec.WebP,
_ => false,
};
}
internal string CodecName => Codec switch
{
Codec.H264 => "h264_vulkan",
Codec.H265 => "hevc_vulkan",
Codec.VP8 => "libvpx",
Codec.VP9 => "libvpx-vp9",
Codec.WebP => "libwebp_anim",
_ => null,
};
internal string ContainerName => Container.ToString().ToLower();
}
[Expose]
public enum Codec
{
///
/// H.264 codec (does not support transparency)
///
H264,
///
/// H.265 codec (does not support transparency)
/// Only supported on modern GPUS, will fallback to H.264 if not supported.
///
H265,
///
/// VP8 codec (does not support transparency)
///
VP8,
///
/// VP9 codec (supports transparency)
///
VP9,
///
/// WebP codec (supports transparency)
///
WebP,
}
[Expose]
public enum Container
{
///
/// MP4 container (does not support transparency)
///
MP4,
///
/// WebM container (supports transparency)
///
WebM,
///
/// WebP container (supports transparency)
///
WebP,
}
private CVideoRecorder native;
private readonly string path;
private readonly int width;
private readonly int height;
private readonly int frameRate;
private readonly int bitrate;
public int Width => width;
public int Height => height;
internal VideoWriter( string path, Config config )
{
if ( !config.IsCodecSupported() )
throw new ArgumentException( $"{config.Container} container does not support {config.Codec} codec" );
this.path = path;
width = config.Width;
height = config.Height;
frameRate = config.FrameRate > 0 ? config.FrameRate : 60;
bitrate = config.Bitrate > 0 ? config.Bitrate : 8;
var audioSampleRate = 44100;
var audioChannels = 2;
native = CVideoRecorder.Create();
native.Initialize( path, width, height, frameRate, bitrate, audioSampleRate, audioChannels, config.CodecName );
}
~VideoWriter()
{
MainThread.QueueDispose( this );
}
///
/// Dispose this recorder, the encoder will be flushed and video finalized.
///
public void Dispose()
{
if ( native.IsValid )
{
native.Destroy();
native = IntPtr.Zero;
}
GC.SuppressFinalize( this );
}
///
/// Finish creating this video. The encoder will be flushed and video finalized.
///
public async Task FinishAsync()
{
if ( !native.IsValid )
return;
GC.SuppressFinalize( this );
var n = native;
native = IntPtr.Zero;
await Task.Run( () => n.Destroy() );
}
///
/// Add a frame of data to be encoded. Timestamp is in microseconds.
/// If a timestamp is not specified, it will use an incremented
/// frame count as the timestamp.
///
/// The frame data to be encoded.
/// The timestamp for the frame in microseconds. If not specified, an incremented frame count will be used.
public unsafe bool AddFrame( ReadOnlySpan data, TimeSpan? timestamp = default )
{
if ( !native.IsValid )
return false;
long mcs = (long)(timestamp?.TotalMicroseconds ?? -1);
if ( data.Length != (width * height * 4) )
throw new ArgumentException( $"Invalid frame data" );
fixed ( byte* dataPtr = data )
{
native.AddVideoFrame( (IntPtr)dataPtr, mcs );
}
return true;
}
///
/// Add a frame of data to be encoded. Timestamp is in microseconds.
/// If a timestamp is not specified, it will use an incremented
/// frame count as the timestamp.
///
/// The frame data to be encoded.
/// The timestamp for the frame in microseconds. If not specified, an incremented frame count will be used.
public unsafe bool AddFrame( Bitmap bitmap, TimeSpan? timestamp = default )
{
return AddFrame( bitmap.GetBuffer(), timestamp );
}
///
/// Internal for now as I have no idea, how to expose audio recording in a good way yet.
///
internal void AddAudioSamples( CAudioMixDeviceBuffers buffers )
{
native.AddAudioSamples( buffers );
}
}