Repository navigation
Releases: yartat/MediaToolkit.NET
Release list
A fix release for the GStreamer and mpv transcoders
MediaToolkit.NET 0.3.1
A fix release for the GStreamer and mpv transcoders. Both could accept an audio
request, report success, and write something other than what was asked: a
different channel count, rate or duration. Everything a request says now either
reaches the file or is reported, as an error before anything is written or as a
warning in TranscodeResult.Warnings.
The defects were found by generating the MP-MediaInfo test corpus — 220 Matroska
audio files over eleven formats — through each backend in turn and reading every
file back with the FFmpeg prober. The FFmpeg transcoder made all 220 as asked
before and after. The others:
| Backend | 0.3.0 | 0.3.1 | What is still not made |
|---|---|---|---|
| GStreamer | 121 | 193 | 19 TrueHD, refused; 8 AAC VBR, warned |
| mpv | 86 | 175 | 19 TrueHD, refused; 26 PCM, warned |
No public API changed.
Fixed
GStreamer
- Channel layouts past 5.1 were refused or wrong.
ChannelMaskholds
FFmpeg's speaker bits and was passed to GStreamer'schannel-maskunchanged,
but the two number the speakers differently from the side pair on (FFmpeg's
side left is bit 9, GStreamer's is 10). 7.1 and the side layouts DTS needs
failed to link. The mask is now translated, and a speaker GStreamer has no
position for is an error. - DTS and RealAudio had no encoder. They now use
avenc_dca(with
strict=experimental, as libavcodec requires) andavenc_real_144. - 24-bit PCM had no path.
PcmS24is now written raw asS24LE. - 24-bit FLAC was refused.
SampleFormat.S32is how libavcodec carries
24-bit FLAC; forflacencit now becomesS24_32LE, the format that element
takes. - A planar sample format was refused by every encoder. libav's elements and
matroskamuxtake interleaved audio only, so a planar request now pins the
sample width alone. - MP3 came out at half the rate asked for. At low bit rates
lamemp3enc
resamples unless the rate downstream is fixed (48 kHz at 64 kbit/s became
24 kHz); the rate is now pinned after the encoder. - MP2 failed at valid bit rates.
twolamemp2encrefuses combinations such
as 320 kbit/s mono that libavcodec accepts;avenc_mp2is now preferred. - TrueHD is refused with the reason.
avenc_truehdexists, but no GStreamer
muxer takes its output;Validatenow says so instead of the pipeline
failing to link.
mpv
- Channels, rate and sample format were ignored. mpv's encoder follows its
audio output, which keeps the input's layout and rate; asked for six channels
it wrote the input's two and reported success. The request now reaches
--audio-channels(as a speaker list),--audio-samplerateand
--audio-format. - TrueHD and MLP in Matroska are refused. mpv stamps their frames a
millisecond apart where each lasts 1/1200 s, so the file ran 20% long. - PCM output carries a warning. mpv writes uncompressed audio in blocks of
16384 samples and pads the last one with silence, so the file runs up to that
much past the end of the input or the trim. - The output check could not read RealAudio. mpv's Matroska demuxer does not
knowA_REAL/14_4; the check now uses the FFmpeg prober when FFmpeg is
available.
Known limitations added
- GStreamer: AAC at a variable quality is not mapped. gst-libav does not expose
libavcodec'sqscaleflag, so the setting is ignored with a warning; use a
bit rate or the FFmpeg backend. - GStreamer cannot write TrueHD; mpv cannot write it in Matroska. Use FFmpeg.
Add transcoding on FFmpeg, GStreamer and mpv
MediaToolkit.NET 0.3.0
The transcoding release. A file can now be probed, converted and read frame by
frame, and the conversion is described once and run by whichever backend can do
it: FFmpeg, GStreamer or mpv.
TranscodeRequest says what the output holds — which streams of which inputs,
copied or re-encoded, subtitles converted or burned in, a trim, tags and
chapters. Every backend resolves it into the same TranscodePlan, then says for
itself what it cannot do before anything is written. The facade asks FFmpeg
first, then GStreamer, then mpv, and when all three refuse the exception carries
each refusal.
Added
A common transcoding API (MediaToolkitNet.Abstractions)
await MediaToolkitNetBackends.TranscodeAsync(TranscodeRequest.Remux("in.mkv", "out.mp4"));
await MediaToolkitNetBackends.TranscodeAsync(new TranscodeRequest("out.mkv")
{
Inputs = ["film.mkv", "film.en.srt"],
Start = TimeSpan.FromMinutes(1),
End = TimeSpan.FromMinutes(2),
Streams =
[
OutputStream.Video(StreamSource.First(MediaStreamKind.Video),
new VideoOutputSettings(MediaCodec.H264) { Width = 1280, Quality = 23, Speed = EncoderSpeed.Fast }),
OutputStream.Audio(StreamSource.All(MediaStreamKind.Audio), new AudioOutputSettings(MediaCodec.Opus)),
OutputStream.Subtitles(StreamSource.First(MediaStreamKind.Subtitle, input: 1), MediaCodec.SubRip),
],
Metadata = new Dictionary<string, string> { ["title"] = "Film" },
});TranscodeRequest,OutputStream(copy, drop, re-encode video or audio,
convert subtitles, burn them in),StreamSourceselectors by index, by kind,
by language and by position,VideoOutputSettingsandAudioOutputSettings,
and typedVideoFilterandAudioFiltersteps with aCustomescape hatch
per backend.TranscodePlan.Resolve, the rules every backend shares: which stream a
selector means, that cover art is not "the first video", that a dropped stream
is taken out of "all", that burning in needs the video re-encoded, that the
output is not one of the inputs.IMediaTranscoderwithValidateandRunAsync, progress as
TranscodeProgress, cancellation that deletes the partial file, and
TranscodeRejectedExceptionlisting every issue.IMediaProberandMediaInfo: container, duration, streams with codec,
language, title, default and forced flags, tags and chapters. Streams are
numbered in container order on every backend, so an index read with one works
with another.MediaCodecgained the subtitle codecsSubRip,Ass,WebVtt,MovText,
DvdSubtitleandPgs, andISubtitleRecorderlets a recorder write
SubtitleCues.BackendCapabilities.ProbingandTranscoding, and
MediaToolkitNetBackends.Probe,CreateTranscoderandTranscodeAsync.
FFmpeg
- H.264 falls back to
libopenh264andh264_mf, and HEVC tohevc_amfand
hevc_mf, after the encoders tried before, so an LGPL build without x264
still encodes H.264. A trimmed video is encoded with the input's frame rate
as its time base, as the ffmpeg program does, rather than the container's. FFmpegTranscoderdoes everything the request can say: stream copy into any
container libavformat writes, re-encoding through libavfilter, several inputs,
subtitle conversion between SubRip, ASS, WebVTT and MP4 timed text, burn-in
through libass, trims, tags and chapters. Encoders are chosen by what they
accept (avcodec_get_supported_configon 7.1 and later), so Opus is fed
48 kHz without being asked.FFmpegProber.FFmpegMediaReader, in the manner of FFMediaToolkit: open a file and read
decoded frames in order or at a time, converted to the pixel format, size,
sample format and rate asked for, and subtitle cues as text.FFmpegMuxer, the packet-level layer the transcoder is built on, public for
whoever wants to mux packets directly.FFmpegRecorderimplementsISubtitleRecorder.
GStreamer
GStreamerProber, onGstDiscoverer.GStreamerTranscoderbuilds one pipeline from the plan:parsebinper input,
a branch per output stream, one muxer. It copies and re-encodes video and
audio from any number of inputs, burns in text subtitles withtextoverlay,
trims, and writes tags.GStreamerTranscoder.Describeprints the equivalent
gst-launch-1.0line.
mpv
MpvProber, andMpvTranscoderon mpv's encoding mode: one video and one
audio track re-encoded, subtitles burned in, external audio and subtitle
files, trims and tags.MpvTranscoder.CommandLineprints the equivalentmpv
command. A file of subtitles alone, which mpv refuses to play, is probed as
an external subtitle file beside a blank picture.
Sample commands
probe (with a backend name), transcode (with --vencoder and --aencoder
to name an encoder outright), read, mpv-args and gst-args. encoders now
lists the video encoder MFTs as well.
dotnet run --project samples/MediaToolkitNet.Sample.Cli -- transcode in.mkv out.mp4 --vcodec H264 --size 1280x-1 --start 60 --end 120
dotnet run --project samples/MediaToolkitNet.Sample.Cli -- transcode in.mkv out.mp4 --backend gstreamer --vcodec H264 --sub in.en.srt --scodec burn
dotnet run --project samples/MediaToolkitNet.Sample.Cli -- read in.mkv 12.5 --format Bgra32 --size 640x360Changed
GStreamerBackend.CapabilitiesisProbing | Transcodinginstead ofNone.MediaToolkitNetBackends.ProbeandCreateTranscodertry FFmpeg, then
GStreamer, then mpv, whatever the general registry order: FFmpeg reports and
does the most.MediaToolkitNet.MpvreferencesMediaToolkitNet.FFmpeg, for the encoder,
muxer and filter names mpv's encoding mode shares with it. No FFmpeg library
is loaded unless the FFmpeg backend is used.
Fixed
These are defects of earlier releases that running the code found.
Windows
- Media Foundation could not record video at all. Two GUIDs had been written
from memory and were wrong:MF_MT_INTERLACE_MODE, so every video media type
lacked its interlace mode andIMFSinkWriter::SetInputMediaTypefailed with
MF_E_INVALIDMEDIATYPE, andMFT_CATEGORY_VIDEO_ENCODER, so the check meant
to explain that failure found no video encoder and blamed the machine. Every
GUID in the source has now been checked against the Windows SDK headers, and
the camera records to H.264 and HEVC. - A bit rate of 0 overwrote the encoder's own default instead of leaving it
to the encoder, asBitrateBitsPerSeconddocuments. Most encoders did not
mind; Media Foundation's H.264 encoder refused to start. Encoders now keep
their default, which for libx264 is still constant quality.
FFmpeg
- libavfilter audio graphs failed on every series. The
abuffersource
takeschannel_layout, notch_layout. FFmpegDecoderreported zero channels for audio, read from an option that
is no longer set; the channel count now comes fromch_layout.
GStreamer
- Three
GstMessageTypevalues were wrong. The enum had been written from
memory; the probe forGstMessage::typecaught it the first time it ran on
Linux. The values now come from compiling againstgstmessage.h. GStreamerSink.NegotiatedFormatwas always empty, and a pull asked for the
wrong kind of frame dropped the sample silently. The format now comes from the
sink pad's current caps, a mismatch throws, and planar audio is delivered.gst_plugin_feature_get_nameis a macro in current GStreamer and was bound as
a function; element names now come fromgst_object_get_name.
Verified by running
On Debian 13 under WSL, against FFmpeg 7.1.5, GStreamer 1.26.2 and libmpv 2.5:
- All 206 unit tests and all 85 integration tests pass, three runs in a row.
The integration tests write their inputs with this library's recorder and
read every result back with the FFmpeg prober and reader, so no backend checks
its own output. - FFmpeg: a remux keeps every stream and language; SubRip converted to MP4
timed text reads back with the same text and timing asffmpeg's own
conversion; Matroska to MPEG-TS and back to MP4; trims whose cues straddle the
start and the end come out cut at both; scaling, frame rate, CRF, Opus from a
44.1 kHz source, volume; burn-in measured by the luma of the bottom of the
frame; VP9 with WebVTT in WebM; file names with colons, commas and quotes in a
burn-in path. - GStreamer: the prober numbers streams and reads their codecs, languages,
titles and chapters as FFmpeg does; H.264, VP9 and MJPEG with Opus and FLAC
encoded into MP4, Matroska and WebM; copies into Matroska, with and without
a trim, and into MPEG-TS; burn-in from the same file and from a separate SRT,
trimmed and not, checked by luma. - mpv, through libmpv: the prober agrees with FFmpeg's, flags included, and
reads a bare SRT or ASS file; H.264, VP9 and MJPEG with AAC, Opus and FLAC,
scaled and trimmed; burn-in from the same file and from a separate SRT,
trimmed and not, checked by luma; copy and several audio tracks refused with
the reason. Running it through libmpv found thatmpv_set_option_string
does not take thesub-files-appendthempvcommand line does, so external
files are now added withchange-list. It also found that mpv fails quietly:
an encoder it lacks leaves that stream out of a file it reports as written,
and one that cannot open stalls it for ever. The transcoder now picks
encoders from mpv'sencoder-list, gives up on a stall with mpv's own error
in the message, and checks its output for every stream it asked for.
On Windows 11, against FFmpeg 7.1.5 (an LGPL build) and libmpv 2.5:
- The unit tests, and the FFmpeg and mpv integr...
Filter graphs
MediaToolkit.NET 0.2.0
The filter graph release. Four native stacks can assemble a processing graph, and
all four are wrapped here: the DirectShow filter graph on Windows, libavfilter
through FFmpeg, mpv's vf and af chains, and GStreamer pipelines on Linux.
Each is wrapped in its own terms rather than behind one common abstraction. The
shapes are genuinely different — DirectShow links pins and pushes through a
callback, libavfilter takes a chain string and is pumped, mpv builds the graph
itself and only takes a property, GStreamer parses a whole pipeline and is
pulled — and flattening that would have cost more than it saved.
New package
MediaToolkitNet.GStreamer, for GStreamer 1.x on Linux.
dotnet add package MediaToolkitNet.GStreamerA pipeline built from a gst-launch description, its state, bus and seeking, the
element-factory registry, and video and audio frames taken out of an appsink:
using var pipeline = GStreamerPipeline.Parse(
"filesrc location=clip.mp4 ! decodebin ! videoconvert ! " +
"video/x-raw,format=BGR ! appsink name=sink");
var sink = pipeline.GetSink("sink");
sink.DropWhenFull(true);
pipeline.Pause(); // preroll, so the duration is known
Console.WriteLine($"{pipeline.Duration}, {sink.NegotiatedFormat}");
pipeline.Play();
while (!sink.IsEndOfStream)
{
sink.TryPullVideo(TimeSpan.FromMilliseconds(500), (in VideoFrame frame) => Save(frame));
}It implements no capture or playback interface of its own, because the pipeline
covers those roles in GStreamer's own terms. MediaToolkitNet.All pulls it in.
Added
DirectShow filter graph and Sample Grabber
MediaToolkitNet.Windows wrapped DirectShow for device enumeration only. It now
builds graphs: filters, pins, media types, intelligent connect, RenderFile,
transport control, seeking, bus events, and a text dump of what was built.
Thirty built-in filters are addressed by name through the DsFilter enum rather
than by class id — the splitters and parsers, the ACM and MPEG decoders, the DVD
Navigator, the tees, the renderers and the muxers.
The Sample Grabber is frame access from inside the graph:
using var graph = DirectShowGraph.Create();
var source = graph.AddSourceFilter(@"C:\clip.mp4");
var grabber = DirectShowSampleGrabber.AddTo(graph);
grabber.AcceptVideo(PixelFormat.Bgr24); // forces a decoder and a converter in
var sink = graph.AddFilter(DsFilter.NullRenderer);
graph.Connect(source.FirstFreeOutput!, grabber.Filter.InputPins[0]);
graph.Connect(grabber.Filter.FirstFreeOutput!, sink.InputPins[0]);
grabber.VideoGrabbed += (in VideoFrame frame) =>
{
// Borrowed, and bottom-up: Stride(0) is negative for a DIB.
ReadOnlySpan<byte> pixels = frame.GetPlane(0);
};
graph.Run();AcceptVideo and AcceptAudio pin the format the graph has to negotiate to, so
the builder inserts a decoder and a colour converter as needed. Bottom-up DIBs
arrive with a negative stride, which VideoFrame already understood. For a
thumbnail rather than a stream there is the buffered path: SetOneShot with
BufferSamples and CopyCurrentBuffer.
This is also the one place in the repository that implements a COM interface
instead of calling one. ISampleGrabberCB is a hand-built COM object: a struct
whose first field points at a vtable of [UnmanagedCallersOnly] function
pointers, with a GCHandle back to managed code. No [ComImport], nothing that
would cost AOT-safety.
libavfilter
FFmpegFilterGraph takes the chain in FFmpeg's own syntax — the same string the
command line takes after -vf or -af. The buffer source and sink at either end
are added for you, so the string holds only the filters in between:
var input = new VideoFormat(1920, 1080, PixelFormat.Bgr24, 30);
using var chain = FFmpegFilterGraph.ForVideo(input, "scale=640:-1,hflip");
chain.Push(decodedFrame); // an AVFrame, or a borrowed VideoFrame
while (chain.TryReceive((in VideoFrame frame) => Save(frame)))
{
}
chain.Flush();
Console.WriteLine(chain.OutputVideoFormat); // 640x360 Bgr24, as negotiatedTimestamps travel in TimeSpan ticks: the source is configured with a time base
of 1/10000000 so a tick is one unit, and the sink's own time base converts the
result back. A mid-stream format change is honoured, because a sample that
carries a media type is the only notice libavfilter gives.
FFmpegFilters lists what the loaded library was built with. Unlike the
DirectShow catalogue this cannot be written down in advance: libavfilter carries
its own registry and which filters exist depends on how it was configured.
mpv filter chains
mpv builds and rebuilds the graph itself, so what is wrapped is the vf and af
properties it is set through:
player.VideoFilters.Add(MpvFilterChain.Lavfi("hqdn3d,eq=contrast=1.1"));
player.AudioFilters.Set("lavfi=[volume=0.5]");Set, Add, Remove, Toggle and Clear, plus MpvFilterChain.Lavfi to hand
a chain written for libavfilter to mpv.
Sample commands
ds-filters, ds-graph, ds-grab, av-filters, av-filter, gst-elements
and gst-run.
dotnet run --project samples/MediaToolkitNet.Sample.Cli -- ds-filters
dotnet run --project samples/MediaToolkitNet.Sample.Cli -- ds-grab clip.mp4 5
dotnet run --project samples/MediaToolkitNet.Sample.Cli -- av-filter "scale=640:-1,hflip"
dotnet run --project samples/MediaToolkitNet.Sample.Cli -- gst-run "videotestsrc ! videoconvert ! appsink name=sink" 3Changed
FFmpegGenerationtakes anAvFiltermajor version, as its seventh positional
parameter. Code that constructs one has to pass it; nothing else moves.
| Series | avutil | avcodec | avformat | swscale | swresample | avfilter |
|---|---|---|---|---|---|---|
| 7.x | 59 | 61 | 61 | 8 | 5 | 10 |
| 8.x | 60 | 62 | 62 | 9 | 6 | 11 |
| 9.x | 61 | 63 | 63 | 10 | 7 | 12 |
libavfilteris optional, likelibavdevice, and is looked for only in the
major version belonging to the series that was already loaded. A graph built
by one major against the structs of another is exactly the mismatch the version
gate exists to prevent. When it is missing,FFmpegFilterGraph.IsAvailableis
false and nothing else changes.
Verified by running
On Windows 11:
- All thirty catalogued DirectShow class ids were instantiated. Twenty-eight
resolve.Line21DecoderandWavDestare not registered on current Windows —
the operating system's doing, not a bad GUID. This matters more than it looks:
a wrong class id is indistinguishable from an absent filter until you try it. - A WAVE file through the Sample Grabber: 23 buffers, 122 868 samples at
22 050 Hz. That is 5.5722 seconds, exactly the duration the graph reported, so
the sample counts and the timestamps agree with the container. - An H.264 file through decoder, colour converter and Sample Grabber to BGR24
at 1728×1080: thirty frames per second sustained, a negative stride as a
bottom-up DIB should have, and pixels read end to end of every plane rather
than at the corners — so a wrong pointer or a wrong stride could not pass as a
plausible frame count.
Everything 0.1.2 verified still stands: the FFmpeg backend was run on Debian 13
against FFmpeg 7.1.5 and on Windows 11 against FFmpeg 9.0.1.
Not exercised yet
Please read this before depending on it.
- libavfilter has not been run. The development machine has no FFmpeg
installed. The two struct layouts it needs —AVFilterInOutand the head of
AVFilter— are found at startup by parsing[in]null[out]and reading the
labels back, and the graph refuses to build when that probe does not pass, so a
mismatch should produce a clear error rather than memory corruption. That path
is untested too. - The mpv filter chains have not been run; libmpv was not installed either.
- Nothing in
MediaToolkitNet.GStreamerhas run at all. It binds the Linux
library names and the development machine is Windows.
GStreamer exposes four things only as macros, with no accessor at all:
GST_MESSAGE_TYPE, GST_BUFFER_PTS, the fields of GstMapInfo and the links of
a GList. None of them is hard-coded. The message type is found by posting a
GST_MESSAGE_APPLICATION, whose value (1 << 22) does not occur in the
reference counts and pointers around it; the buffer timestamp by the run of five
GST_CLOCK_TIME_NONE qwords a fresh buffer starts with; GstMapInfo by mapping
a buffer of a size chosen here and reading that size back; GList against a
one-element list built here. A probe that fails leaves the backend disabled.
Those probes and the floating-reference handling in Parse and
GStreamerElements.Exists are the parts most worth checking first on real
hardware.
That is why this is 0.2.0 rather than 1.0. The public API may still change.
Known limitations
- The GStreamer backend reports itself unavailable on Windows and macOS even
where GStreamer is installed, because it binds the Linux library names. Lifting
that is a matter of the file names and nothing else. - The Sample Grabber is registered for
SampleCBonly.BufferCBreturns
E_NOTIMPL:SampleCBcarries the sample itself, and therefore its timestamps
and any mid-stream format change. - A filter graph is not a player. None of the four renders audio or video on its
own — they hand frames back, and what happens next is the caller's. - Media Foundation video recording still needs a video encoder MFT registered on
the machine; editions such as Windows N without the Media Feature Pack have
none. MacOSBackendcannot write files, becauseAVAssetWriteris not wrapped.- `FFmpe...
Advanced support FFMPEG
MediaToolkit.NET 0.1.2
The FFmpeg backend release. Audio encoding did not work at all in 0.1.1 — on any
FFmpeg version — and the fix for that turned up a second defect underneath it, in
the struct offsets themselves. Both are fixed here, FFmpeg 8.x and 9.x are now
supported alongside 7.x, and the whole of it is covered by a test suite that
checks every offset against the library that is actually installed.
Fixed
Audio encoding never worked. The channel count was set on the encoder through
the ac AVOption, which has not existed on AVCodecContext since FFmpeg 7
removed the old channel API. The call returned "option not found", the return
value was not checked, and avcodec_open2 then refused the encoder with
Encoder requires channel layout to be set. The same mistake sat in the routine
that probes the AVFrame audio fields at startup, so AudioFrameLayoutVerified
stayed false and every audio stream was rejected with
audio encoding is unavailable. The layout now travels through ch_layout.
The AVStream offsets were wrong — in every FFmpeg from 6 to 9. codecpar
was read at 208 where it has always lived at 16, and time_base, start_time,
duration and avg_frame_rate were each eight bytes short. This was not a
reading fault only: the muxer writes codec parameters through codecpar, so
adding any stream to an output wrote through whatever the rubbish at +208 pointed
at. A new stream is now checked before anything is written through it.
Channel layouts were guessed arithmetically. (1 << channels) - 1 gives 3.1
for four channels rather than quad, and an invalid mask for eight rather than
7.1. Both were accepted by the AC-3 encoder, which then wrote a file whose
speakers were in the wrong places. The layout of a bare channel count now comes
from av_channel_layout_default.
Encoder flags were assigned rather than accumulated, so setting any second
flag dropped the global-header bit.
Matroska audio did not ask for a global header. .mka and .mks are the
same muxer as .mkv and are now treated as such.
Added
FFmpeg 8.x and 9.x, beside 7.x. The loader reads the versions it actually
got, matches them against a table of release series, and refuses a mixture that
belongs to none of them; the two struct offsets that differ between series come
from the matched series.
| Series | avutil | avcodec | avformat | swscale | swresample |
|---|---|---|---|---|---|
| 7.x | 59 | 61 | 61 | 8 | 5 |
| 8.x | 60 | 62 | 62 | 9 | 6 |
| 9.x | 61 | 63 | 63 | 10 | 7 |
AC-3 and DTS, as MediaCodec.Ac3 and MediaCodec.Dts. The DCA encoder is
marked experimental in FFmpeg and is opened as such without the caller having to
say so.
PCM at every width: MediaCodec.PcmU8, PcmS16, PcmS24 and PcmS32.
MediaCodec.Pcm remains a synonym for 16-bit.
Variable bitrate: AudioEncodingSettings.Quality, on the scale -q:a uses.
Per-stream encoder settings: EncoderName names an encoder outright and
overrides the codec, and Options passes AVOptions through to that stream alone.
FFmpegRecorder.EncoderOptions still reaches every stream, and a stream's own
options are applied afterwards and win.
Stated channel layouts: the new ChannelLayout constants and
AudioFormat.ChannelMask. Six channels are 5.1 twice over — the back
arrangement and the side one — and only a mask tells an encoder which.
Tests: 134 unit tests, plus 43 integration tests that check every struct
field this binding reads by offset against the installed library. Each check
establishes its expected value by a route that does not use the offset it is
checking, so a mismatch fails by name instead of corrupting memory. They skip
themselves where FFmpeg is not loadable.
Changed
- Six channels with no stated layout now default to 5.1 side, which is what
av_channel_layout_defaultgives, rather than 5.1 back. Ask for
ChannelLayout.FivePoint1Backwhere the back arrangement is meant. AudioFormattakes a fourth, optionalChannelMask. Source-compatible;
recompile against it.AbiLayout.CodecContextTimeBaseandCodecContextPixFmtare properties rather
than constants, because the series decides them.FFmpegLibraries.AvUtilMajor,AvCodecMajorandAvFormatMajorare gone.
FFmpegLibraries.GenerationandFFmpegGeneration.Knownreplace them.AbiLayout.PrepareAudioFrametakes the channel mask.
Requirements
- .NET 10
- A 64-bit process. Every mapped struct layout assumes a 64-bit pointer.
MediaToolkitNet.FFmpegneeds FFmpeg 7.x, 8.x or 9.x at runtime. No native
binary ships in any package.MediaToolkitNet.Mpvneeds libmpv at runtime.- The platform backends only call libraries that are already part of the
operating system.
Verified by running
On Windows 11 against FFmpeg 9.0.1 (avutil 61 / avcodec 63 / avformat 63):
- AC-3 at one, four and six channels; DTS; AAC at a fixed rate and at a quality;
PCM at 8, 16, 24 and 32 bits — each encoded into Matroska and read back through
this library's own demuxer as the codec, rate and channel count it was given; - MJPEG video encoded and muxed, which is the path that writes through
AVStream::codecpar; - every hard-coded offset checked against the running library.
The offset tests were themselves checked by breaking three offsets on purpose.
AVStream::codecpar back at 208 failed twenty tests and corrupted nothing;
AVCodecContext::time_base moved to FFmpeg 6's value failed eight; and
pix_fmt moved to FFmpeg 7's value failed exactly one, the test named for that
field — the startup self-check missed that one, because the neighbouring field in
FFmpeg 9 happens to hold the same sentinel.
Not exercised yet
- FFmpeg 7.x and 8.x have not been run. Their entries in the series table
were read withoffsetofagainst the headers of n7.1.5 and n8.1.2, and the
fields they name are the same ones 9.x uses, but no library of those series was
loaded. Runningdotnet teston a machine that has one is the quickest way to
confirm them, and a report either way is welcome. - The Linux and macOS backends have still never been run, nor has the mpv
backend. - Media Foundation video recording is still untested: the development machine
has no video encoder MFT registered. Audio recording on that path does work.
This is why the release is still 0.1.x. The public API may still change.
Known limitations
- An encoder's own constraints are the caller's to meet: the DCA encoder takes
only the side arrangement of 5.1 and refuses stereo below 384 kbps, and AC-3
does not do 7.1 at all. The failure names the encoder, the sample rate and the
layout that was asked for. The FFmpeg command line hides this behind an
automatic conversion filter; this API does not. - Matroska stores no channel mask, so a PCM stream read back from it reports an
unspecified layout, and AC-3 or DTS report whatever their own bitstream says.
The mask matters for encoding, not for what the container records. - Media Foundation video recording needs a video encoder MFT registered on the
machine. MacOSBackendcannot write files —AVAssetWriteris not wrapped.FFmpegPlayeronly decodes: no renderer, no audio output.- Only the mpv client API is wrapped; the render API is not.
- DirectShow is wrapped for device enumeration only; no filter graph.
Trying it
dotnet add package MediaToolkitNet.All// Five point one Dolby Digital into a Matroska audio file. The layout is stated
// because six channels alone do not say where the speakers are.
using var recorder = MediaToolkitNetBackends.CreateRecorder("surround.mka");
var format = new AudioFormat(48000, 6, SampleFormat.S16, ChannelLayout.FivePoint1Back);
var stream = recorder.AddAudioStream(new AudioEncodingSettings(format, MediaCodec.Ac3, 448_000));
recorder.Start();
// … recorder.WriteAudio(stream, frame) …
recorder.Stop();# The ABI tests, against whichever FFmpeg is installed.
dotnet test
# Or against a particular build.
MEDIATOOLKITNET_FFMPEG_DIR=/opt/ffmpeg-9/lib dotnet testFull documentation: README
First public release
MediaToolkit.NET 0.1.1
First usable release: low-level wrappers over the native media stacks for
.NET 10, plus one cross-platform API on top of them.
There is no 0.1.0. Two packages of that version reached nuget.org before
the rest of the set was rejected. nuget.org refuses an id that matches an
existing one once.and-are stripped, soMediaToolkitNet.Corecollided
withMediaToolkit.NetCoreand the facadeMediaToolkitNetcollided with
MediaToolkit.NET. They are nowMediaToolkitNet.Abstractionsand
MediaToolkitNet.All.MediaToolkitNet.Interop0.1.0 and
MediaToolkitNet.FFmpeg0.1.0 depend on an id that never shipped, so they are
unlisted and must not be used.
All interop goes through [LibraryImport] and unmanaged function pointers. COM
on Windows is called directly through vtable slots, without [ComImport] or the
built-in COM marshaller, which keeps the assemblies AOT- and trim-safe and keeps
the exact native signature visible at every call site.
Install
dotnet add package MediaToolkitNet.AllThat pulls in every backend and picks one at runtime. Take a single backend
instead when you do not want the rest:
dotnet add package MediaToolkitNet.Windows| Package | Contents |
|---|---|
MediaToolkitNet.All |
Facade: backend registry and per-platform selection |
MediaToolkitNet.Abstractions |
Abstractions only; no native code, no dependencies |
MediaToolkitNet.Interop |
Native loading, UTF-8 marshalling, COM vtable access |
MediaToolkitNet.FFmpeg |
FFmpeg 7.x: demux, decode, encode, mux |
MediaToolkitNet.Mpv |
libmpv client API |
MediaToolkitNet.Windows |
Media Foundation, WASAPI, DirectShow |
MediaToolkitNet.Linux |
V4L2, ALSA, PulseAudio |
MediaToolkitNet.MacOS |
AVFoundation, CoreAudio |
What is in it
| Capability | Windows | Linux | macOS | FFmpeg | mpv |
|---|---|---|---|---|---|
| Device enumeration | MF + WASAPI + DShow | ALSA + V4L2 | CoreAudio + AVF | avdevice | — |
| Video capture | IMFSourceReader |
V4L2 mmap | AVCaptureSession |
— | — |
| Audio capture | WASAPI (+ loopback) | ALSA / PulseAudio | AudioQueue | — | — |
| Audio render | WASAPI | ALSA / PulseAudio | AudioQueue | — | — |
| Playback | — | — | AVPlayer |
decode to frames | full |
| Recording to file | IMFSinkWriter |
— | — | encode + mux | — |
A dash means the backend does not take that role: FFmpeg owns no audio output,
mpv never hands frames back, and on Linux and macOS writing files is the FFmpeg
backend's job.
Requirements
- .NET 10
- A 64-bit process. Every mapped struct layout assumes a 64-bit pointer.
MediaToolkitNet.FFmpegneeds FFmpeg 7.x present at runtime
(avutil 59 / avcodec 61 / avformat 61). No native binary ships in any package.MediaToolkitNet.Mpvneeds libmpv present at runtime.- The platform backends only call libraries that are already part of the
operating system.
Verified by running
Exercised against real hardware on Windows 11:
- device enumeration through Media Foundation, WASAPI and DirectShow, including
default-endpoint flags; - video capture from a USB camera, 640x480 NV12 at 30 fps, sustained ~23 fps
delivered to the frame handler; - audio capture through WASAPI at 48 kHz stereo float;
- recording live audio to AAC/MP4 through
IMFSinkWriter, producing a
structurally complete MP4 —ftyp/mdat/moov, brandmp42— which
confirms the writer was finalised rather than merely fed.
The Windows COM vtable slot tables were corrected twice during development by
running them, not by reading them: a wrong slot number compiles, raises no
exception, and silently calls the neighbouring method.
Not exercised yet
Please read this before depending on it.
- The Linux and macOS backends have never been run. They compile and are
guarded at runtime, but the V4L2 struct sizes and the Objective-C runtime
layout have not been confirmed on real hardware. - The FFmpeg and mpv backends have never been run, because those native
libraries were not installed on the development machine. The FFmpeg ABI
self-check refuses to proceed on a layout it cannot confirm, so a mismatch
should produce a clear error rather than memory corruption, but that path is
untested too. - Media Foundation video recording is untested: the development machine has
no video encoder MFT registered at all.MediaFoundationRecorderdetects that
case and reports it plainly. Audio recording on that path does work.
This is why the release is 0.1.1. The public API may still change.
Known limitations
- Media Foundation video recording needs a video encoder MFT registered on the
machine. Editions such as Windows N without the Media Feature Pack have none;
theencoderssample command reports what a machine actually has. MacOSBackendcannot write files —AVAssetWriteris not wrapped. Use FFmpeg.FFmpegPlayeronly decodes: no renderer, no audio output.- Only the mpv client API is wrapped; the render API is not.
- DirectShow is wrapped for device enumeration only; no filter graph.
Design notes
Frames are borrowed. VideoFrame and AudioFrame are ref structs over
memory the backend owns, valid only inside the handler that received them. The
compiler refuses to let you store one. Zero copies and zero allocations per
frame; copy the data out if you need to keep it.
FFmpeg is pinned to 7.x, and checks itself. FFmpeg keeps no stable ABI for
its public structs across majors, so every direct field access lives in a single
file, the loader gates on the major versions, and the layout is validated at
startup against documented post-allocation defaults. The AVCodecParameters
offsets and the AVFrame audio fields are discovered by runtime probing rather
than hard-coded, and codecs are resolved by name instead of by the unstable
AVCodecID values.
Trying it
The sample CLI covers every capability and is how the library was validated:
dotnet run --project samples/MediaToolkitNet.Sample.Cli -- backends
dotnet run --project samples/MediaToolkitNet.Sample.Cli -- devices
dotnet run --project samples/MediaToolkitNet.Sample.Cli -- rec-video 2
dotnet run --project samples/MediaToolkitNet.Sample.Cli -- rec-audio-file 3 out.m4aReports from Linux and macOS are especially welcome.
Full documentation: README