A FFmpeg.AutoGen wrapper library.
This is NOT a ffmpeg command-line library. Please use FFmpeg shared libraries version >= 5.
Either download from ffmpeg.org according to the license you need, or pull a NuGet redistributable:
NuGet\Install-Package FFmpeg.GPL
NuGet\Install-Package FFmpeg.LGPL
NuGet\Install-Package FFmpeg4Sharp
Namespaces:
using FFmpeg.AutoGen;
using FFmpeg.Sharp;Create a muxer, build an encoder with the fluent builder, then feed frames to EncodeFrame and write the resulting packets.
const string output = "out.mp4";
using var muxer = MediaMuxer.Create(output);
using var encoder = MediaEncoder.Video()
.OutputFormat(muxer.Format)
.Size(800, 600)
.Fps(29.97)
.Configure(c => c.Ref.thread_count = 0) // 0 = auto (one per core)
.Build();
var stream = muxer.AddStream(encoder);
muxer.WriteHeader();
using var frame = MediaFrame.CreateVideoFrame(800, 600, encoder.Ref.pix_fmt);
for (int i = 0; i < 300; i++)
{
frame.MakeWritable(); // the encoder may still hold a reference to the frame
// ... fill frame.Ref.data[plane] ...
frame.Ref.pts = i; // pts in encoder time_base (1/fps)
foreach (var packet in encoder.EncodeFrame(frame))
{
packet.Ref.stream_index = stream.Ref.index;
muxer.WritePacket(packet, encoder.Ref.time_base); // rescales encoder time_base → stream time_base
}
}
muxer.FlushCodecs(new[] { encoder }); // drain the encoder
muxer.WriteTrailer();Use MediaDemuxer.ReadFrames for the common case: route packets to per-stream decoders and yield decoded frames in one call.
var input = "input.mp4";
var output = "frames-out";
using var demuxer = MediaDemuxer.Open(input);
var decoders = demuxer
.Select(s => (s.Index, decoder: MediaDecoder.CreateDecoder(s.CodecparRef)))
.Where(p => p.decoder != null)
.ToDictionary(p => p.Index, p => p.decoder);
using var convert = new Swscale();
using var bgrFrame = MediaFrame.CreateVideoFrame(decoders.First().Value.Ref.width,
decoders.First().Value.Ref.height,
AVPixelFormat.AV_PIX_FMT_BGR24);
try
{
foreach (var (streamIndex, frame) in demuxer.ReadFrames(decoders, AVMediaType.AVMEDIA_TYPE_VIDEO))
{
convert.Convert(frame, bgrFrame); // single-frame, no array allocation; auto-resets on size change
// save bgrFrame somewhere
}
}
finally
{
foreach (var d in decoders.Values) d.Dispose();
}using var demuxer = MediaDemuxer.Open("input.mp4");
MediaCodec dec = null;
var vi = demuxer.FindBestStream(AVMediaType.AVMEDIA_TYPE_VIDEO, ref dec);
using var hwDecoder = new MediaDecoder(dec);
hwDecoder.SetCodecParameters(ref demuxer[vi].CodecparRef);
hwDecoder.Ref.thread_count = 0; // 0 = auto
hwDecoder.InitHWDeviceContext("d3d11va"); // or "cuda", "qsv", "vaapi", ...
hwDecoder.Open();
using var pkt = new MediaPacket();
using var recv = new MediaFrame();
using var sw = new MediaFrame(); // optional — omit to keep zero-copy GPU surface
foreach (var p in demuxer.ReadPackets(pkt))
{
if (p.Ref.stream_index != vi) continue;
foreach (var frame in hwDecoder.DecodePacket(p, recv, sw))
{
// `frame` is the SW download. Pass null for swFrame above to receive the raw HW surface instead.
}
}using var demuxer = MediaDemuxer.Open("input.mp4");
MediaCodec dec = null;
int vi = demuxer.FindBestStream(AVMediaType.AVMEDIA_TYPE_VIDEO, ref dec);
// One device, shared explicitly across decoder and encoder.
using var cuda = HWDeviceContext.Create(AVHWDeviceType.AV_HWDEVICE_TYPE_CUDA);
using var hwDecoder = new MediaDecoder(dec);
hwDecoder.SetCodecParameters(ref demuxer[vi].CodecparRef);
hwDecoder.InitHWDeviceContext(cuda);
hwDecoder.Open();
using var hwEncoder = MediaEncoder.Video()
.Codec("h264_nvenc")
.Size(demuxer[vi].CodecparRef.width, demuxer[vi].CodecparRef.height)
.Fps(30)
.UseHardware(AVPixelFormat.AV_PIX_FMT_CUDA, AVPixelFormat.AV_PIX_FMT_NV12, cuda)
.Bitrate(4_000_000)
.Build();using var resampler = AudioResampler.For(audioDecoder, audioEncoder);
foreach (var (_, decoded) in demuxer.ReadFrames(audioDecoders, AVMediaType.AVMEDIA_TYPE_AUDIO))
{
foreach (var sized in resampler.Convert(decoded))
using (sized)
{
foreach (var pkt in audioEncoder.EncodeFrame(sized))
muxer.WritePacket(pkt, audioEncoder.Ref.time_base);
}
}
foreach (var tail in resampler.Flush()) // drain
using (tail)
{
foreach (var pkt in audioEncoder.EncodeFrame(tail))
muxer.WritePacket(pkt, audioEncoder.Ref.time_base);
}More: example/.
- Codec threading — FFmpeg codecs default to a single thread. Set
thread_countbeforeOpen:.Configure(c => c.Ref.thread_count = 0)on the builder, ordecoder.Ref.thread_count = 0beforeOpen().0means auto (one per core). - Reuse frames/packets in hot loops —
DecodePacket(pkt, recvFrame),EncodeFrame(frame, recvPacket)andReadPackets(pkt)all accept a reusable receive object; passing one avoids a native alloc/free per call (ReadFramesalready does this internally). PlainforeachoverDecodePacket/EncodeFrameuses the zero-allocation struct enumerator; going through LINQ boxes it. - Dispose deterministically — a single 4K NV12 frame holds ~12 MB of native memory the GC cannot see. Rely on
using/Dispose, not finalizers, or native memory grows far ahead of any GC pressure. - Skip unwanted streams at the demuxer — for streams you never consume, set
demuxer[i].Ref.discard = AVDiscard.AVDISCARD_ALL:av_read_framethen drops their packets internally (mpegts skips parsing them entirely) instead of surfacing them just to be ignored. On inputs with many audio/subtitle tracks this cuts demux work several-fold. - Open latency —
MediaDemuxer.Open(..., findStreamInfo: false)skips the probing pass (which can pre-read megabytes) for known-format / low-latency inputs; callFindStreamInfolater or fill decoder parameters yourself. - Single-stream muxing —
WritePacketDirectwrites viaav_write_frame, bypassing the interleaving queue; use it when the output has one stream or you interleave yourself (dts must increase monotonically per stream). For many-stream live muxing, bound interleave memory withmuxer.Ref.max_interleave_delta. - Scaling —
Swscaleis single-threaded by default; setSwscaleOptions.Threads(FFmpeg ≥ 5.1), or run heavy scale/overlay chains in aMediaFilterGraphwithThreadCount = 0(auto). - Hardware pipelines — keep frames on the GPU: pass
swFrame: nulltoDecodePacketfor zero-copy surfaces, and share oneHWDeviceContext/HWFramesContextacross decoder → filters → encoder (see the HW sections above). Download (TransferToSoftware) only when CPU access is genuinely needed.
This release is a heavyweight cleanup driven by an audit (see docs/migration-7-to-8.md for full details and before/after snippets).
Highlights:
MediaFrame.Clone()/MediaPacket.Clone()no longer leak (disposedValuedefault flipped).MediaDemuxer.Open(Stream)/MediaMuxer.Create(Stream)no longer close your stream by default — passleaveOpen: falseto opt in.MediaIOContextcallbacks catch managed exceptions and surface them asIOExceptionon the next managed call (no more crashes from network blips).MediaDemuxer.ReadPacketsno longer yields a ghost packet at EOF; pair withReadPacketsCloned()for safe enqueuing.MediaCodecParserContext.ParserPackets— fixed NRE and dangling-pointer-on-byte[] bug.MediaEncoder.EncodeFrameno longer callsav_frame_make_writableinfinally(callframe.MakeWritable()yourself before reuse).DecodePacket/EncodeFramereturn allocation-free struct cursors (Frames/Packets) and send their input eagerly at call time — plainforeachcode is unaffected; see the migration guide.- New builder:
MediaEncoder.Video()/MediaEncoder.Audio()— the 14 legacyCreateVideoEncoder/CreateAudioEncoderoverloads are removed. TheMediaEncoder.CreateEncoder(codecpar)one-liner remains. - New
MediaCodecContext.Open(opts)instance method. TheAction<MediaCodecContext>-based lambda factories (MediaDecoder.Create,MediaEncoder.Create, theActionparameters onCreateDecoder/CreateEncoder) are removed — configure with straight-line code between the ctor andOpen(). - New hardware encoder path:
MediaEncoder.Video().UseHardware(...)+MediaCodecContext.AttachHWDevice/AttachHWFramesContext. - New
MediaFrame.IsHardwareFrame/TransferToSoftware/AllocateOnHWFrames. - New
AudioResampler,Swscale.Options,Swresample.Flush(...). IConverter.Convertnow returnsint(frames written), notIEnumerable<MediaFrame>. The old enumerable shape (ConvertEnumerable) has been removed.- Typo fixes:
MediaCodec.GetSampelFmts→GetSampleFormats,MediaFilter.GetGetFilters→GetFilters(old names removed). MediaDictionaryindexer returnsnullon miss instead of throwing.- PascalCase field-mirror shortcuts (
Width,Pts,StreamIndex, ...) are removed — raw field access goes through the.Ref.snake_caseescape hatch (p.Ref.stream_index,frame.Ref.width, ...). Take locals withref var x = ref frame.Ref;— a plainvarcopies the struct.
DllNotFoundException: avformat-XX.dll — FFmpeg.AutoGen does not ship native binaries. Either install FFmpeg.GPL / FFmpeg.LGPL NuGets, or set the loader's search root before any FFmpeg call:
ffmpeg.RootPath = @"C:\path\to\ffmpeg\bin";
// or AppDomain.CurrentDomain.BaseDirectory, or any folder that contains avcodec/avformat/avutil/swscale/swresample DLLsVersion mismatch — this library tracks FFmpeg shared libraries 7.x / 8.x (corresponding FFmpeg.AutoGen versions 7.x / 8.x). FFmpeg 6.x or earlier are not supported.
AV_DICT_DONT_STRDUP_KEY / AV_DICT_DONT_STRDUP_VAL — these flags transfer ownership of an av_malloc'd buffer to the dictionary, which the managed wrapper cannot do safely. They are marked [Obsolete(error=true)].
HW decode falls back to software silently — pass fallbackToSw: false (the default) to InitHWDeviceContext to make this fail instead. Use fallbackToSw: true to opt into the graceful fallback.
HW encode EINVAL on first frame — feed frames whose format matches the encoder's hwPixelFormat, allocated with frame.AllocateOnHWFrames(encoder.GetHWFrames()) instead of AllocateBuffer().
Stream gets unexpectedly closed — MediaDemuxer.Open(Stream) / MediaMuxer.Create(Stream) default to leaveOpen: true since 8.1.0, but if you upgraded from 7.x your old call sites may still be wiring the wrapper's lifecycle to your stream. Inspect the third (boolean) argument.
- Easy API for cut/seek/mute audio clip.
- Easy API for cut/seek video clip.
- More examples and tests.
- Filter graph parser (
avfilter_graph_parse2). - Subtitle support.
- Async/IAsyncEnumerable surface (demux/encode/mux).
- FFmpeg.AutoGen — the underlying P/Invoke bindings.
- FFmpeg API documentation.
This project is licensed under the MIT license.
If you use FFmpeg builds licensed under the GPL, that license is contagious.