miniav_tools_ffmpeg 0.5.1
miniav_tools_ffmpeg: ^0.5.1 copied to clipboard
FFmpeg-backed codecs/muxers for miniav_tools (libavcodec/libavformat).
miniav_tools_ffmpeg #
FFmpeg backend for miniav_tools.
Wraps libavcodec / libavformat / libavutil over FFI and self-registers
with the miniav_tools_platform_interface registry on import.
Call
registerFfmpegBackend()once at startup (idempotent) — importing the library alone does not register it, because Dart top-level finals are lazy. Apps usingminiav_recorderget this for free:Recorder.warmup()andRecorder.start()both register the backend. Once registered,MiniAVTools.createEncoder(...)etc. will pick this backend whenever it can satisfy the requested codec/container.
What it provides #
- Software encoders: H.264 / HEVC / VP9 / VP8 / AV1 / MJPEG / ProRes via libx264, libx265, libvpx, libaom, etc.
- Stage A hardware encoders (CPU frame in → encoded packet out): NVENC, AMF, QSV, VideoToolbox, MediaFoundation, V4L2 M2M.
- Stage B zero-copy D3D11 encoder on Windows: takes an
ID3D11Texture2DNT shared handle directly. No PCIe transfer, no colour conversion, no readback. See Stage B. - Muxers: MP4, Matroska/WebM, MPEG-TS — with file, byte-buffer, and streaming-callback outputs.
- Decoder + demuxer for the same set of codecs/containers.
Auto-download of FFmpeg shared libraries #
On first ensureFFmpegLoaded() (called implicitly by createEncoder etc.)
the package downloads BtbN's LGPL shared FFmpeg build for the current OS
into a per-user cache and loads the DLLs / .so from there. macOS has no
BtbN shared build — install via brew install ffmpeg; the loader probes the
Homebrew lib directories automatically.
Why LGPL (and what it costs) #
We pull the lgpl build, not the gpl one, so that products dynamically
linking these libraries are not subject to GPL copyleft — the LGPL build
keeps libav* under LGPL-2.1, which is safe for proprietary downstream use.
The trade-off: the LGPL build omits the GPL-only software encoders
libx264 / libx265, so there is no CPU-side H.264/HEVC fallback. Hardware
H.264/HEVC (NVENC / QSV / AMF / MediaFoundation / VideoToolbox) and software
VP8/VP9 (libvpx), AV1 (SVT-AV1), MJPEG and ProRes are all still present. On
Windows the h264_mf / hevc_mf MediaFoundation encoders act as the
universal H.264/HEVC fallback when no vendor SDK is available. The licence
variant is controlled by kFfmpegLicense in ffmpeg_downloader.dart; the
cache is namespaced per-variant so changing it forces a fresh download.
Default cache root (the release tag is appended as a subdirectory):
| OS | Cache root |
|---|---|
| Windows | %LOCALAPPDATA%\miniav_tools\ffmpeg |
| Linux | $XDG_CACHE_HOME/miniav_tools/ffmpeg (or ~/.cache/miniav_tools/ffmpeg) |
| macOS | ~/Library/Caches/miniav_tools/ffmpeg (auto-download not available — see above) |
Environment variables:
| Variable | Effect |
|---|---|
MINIAV_TOOLS_FFMPEG_NO_AUTODOWNLOAD=1 |
Disable auto-download — caller must set FFMPEG_LIB_DIR or install FFmpeg system-wide |
MINIAV_TOOLS_FFMPEG_CACHE=<path> |
Override the cache root |
FFMPEG_LIB_DIR=<path> |
Probe FFmpeg libs from a specific directory first |
Concurrent processes are safe: downloaders serialise on an OS-level file lock inside the cache and re-probe after acquiring it, so two app instances starting at once produce one download.
Warming up FFmpeg #
On a fresh machine the first createEncoder / Recorder.start() blocks on
the download + extraction (tens of MB — seconds to minutes depending on the
connection). Warm up at app startup instead so the first recording starts
instantly.
Simplest — load (and download if needed) ahead of time:
import 'package:miniav_tools_ffmpeg/miniav_tools_ffmpeg.dart';
// At app startup. Returns true once libav* are loaded. Safe to call
// repeatedly; a no-op when the cache is already populated.
final ok = await ensureFFmpegLoaded(
onDownloadProgress: (received, total) =>
print('ffmpeg: $received / $total'),
);
With UI progress — MiniAVTools.warmup():
Runs every registered backend's warmup tasks (this backend reports a
"Downloading FFmpeg" task) and streams WarmupProgress events. The stream
never errors — failures arrive as events with error set — so no onError
handler is needed.
import 'package:miniav_tools/miniav_tools.dart';
import 'package:miniav_tools_ffmpeg/miniav_tools_ffmpeg.dart';
registerFfmpegBackend(); // required — warmup only covers registered backends
MiniAVTools.warmup().listen(
(p) {
if (p.fraction != null) {
setState(() => _downloadProgress = p.fraction!); // 0.0 – 1.0
}
},
onDone: () => setState(() => _ready = true),
);
// Or block until everything is warm:
await MiniAVTools.warmup().last;
Apps built on miniav_recorder should call Recorder.warmup() instead —
same stream, but it registers this backend first and needs no extra import.
Lower level — direct downloader control:
final result = await FfmpegDownloader.ensureFfmpeg(
progress: (received, total) { /* total is -1 when unknown */ },
force: true, // re-download even if a cached install exists
);
print(result?.libDir); // directory the DLLs were loaded from
On Windows, hardware-encoder SDKs (QSV / MF / NVENC / AMF) also have a
one-time driver cold-start. The recorder triggers ffmpegD3d11WarmUp
automatically when zero-copy is enabled, so the first session doesn't fall
back to CPU; direct users of FfmpegD3d11HwEncoder can call it themselves
after ensureFFmpegLoaded().
Logging #
All Dart-side diagnostics (downloader, encoder selection, vendor probing, fallbacks) flow through one hook:
import 'package:miniav_tools_ffmpeg/miniav_tools_ffmpeg.dart';
setFfmpegToolsLogLevel(MiniAVLogLevel.debug); // default: info
setFfmpegToolsLogCallback((level, msg) => myLogger.log(level, msg));
Without a callback, messages go to print — deliberately not dart:io
stderr/stdout: in a console-less Windows GUI app (a packaged Flutter
desktop build) the OS stdio handles are invalid and dart:io stdio writes
crash with an uncatchable async FileSystemException (errno 6). Custom
callbacks should avoid stderr for the same reason.
Native av_log messages from the FFmpeg libraries are a separate stream,
bridged via FfmpegShim.setFfmpegLogCallback.
Apps using miniav_recorder don't need any of this directly:
Recorder.setLogCallback / Recorder.setLogLevel wire both hooks (plus
MiniAV and minigpu) automatically, tagging messages from this package as
RecorderLogSource.ffmpeg.
Stage B — zero-copy D3D11 (Windows) #
When the source frame already lives in a D3D11 texture (DXGI screen
capture, minigpu compute output, browser GPU canvas via D3D11 interop, …)
the encoder pulls it straight into the hwframes pool with a single
CopySubresourceRegion between two GPU-resident textures.
import 'package:miniav_tools_ffmpeg/miniav_tools_ffmpeg.dart';
await ensureFFmpegLoaded();
final enc = FfmpegD3d11HwEncoder.open(EncoderConfig(
codec: VideoCodec.hevc,
width: 1920, height: 1080,
bitrateBps: 6_000_000,
hwAccel: HwAccelPreference.required,
));
final pkt = await enc.encode(FrameSource.d3d11Texture(
texturePtr: ntHandle.address, // DXGI NT shared handle
width: 1920, height: 1080,
pixelFormat: MiniAVPixelFormat.bgra32,
timestampUs: i * 33333,
));
Vendor selection #
open(cfg, vendorOrder: ...) walks the list in priority order, opens the
first vendor that succeeds, and returns the encoder. The default order is
AMF → QSV → MediaFoundation. NVENC has its own CUDA path and is
selected automatically by FfmpegHwEncoder.open when zero-copy is
requested with an NVIDIA GPU.
To force a single vendor and surface the raw failure, call:
final enc = FfmpegD3d11HwEncoder.openWith(cfg, D3d11HwVendor.amf);
Sharing a ID3D11Device with another GPU API (existingD3d11Device) #
Cross-API NT-handle sharing only works when both producer and consumer
are on the same DXGI adapter. Different adapters fail with
E_INVALIDARG from OpenSharedResource1. To pin FFmpeg's D3D11 device to
a specific adapter — typically the one Dawn / WebGPU is already using —
pass an existing ID3D11Device*:
// 1) Get the cached D3D11 device that minigpu created on the Dawn adapter.
final d3d11DevicePtr = gpu.createD3D11DeviceOnDawnAdapter();
// 2) Hand it to FFmpeg. FFmpeg AddRef's the device; you may continue to
// use it for your own work.
final enc = FfmpegD3d11HwEncoder.openWith(
cfg,
D3d11HwVendor.nvenc, // or .amf / .qsv / .mediaFoundation
existingD3d11Device: d3d11DevicePtr, // address of an ID3D11Device*
);
When existingD3d11Device == 0 (the default) FFmpeg creates its own
device on adapter 0 (the display adapter) — fine for the single-adapter
case.
Source texture format (sourceTextureFormat) #
The hwframes pool the encoder allocates is bound to a single DXGI format.
CopySubresourceRegion requires the source and destination textures to
be in the same DXGI type group (BGRA cannot be copied to RGBA), so
the pool's sw_format must match the format of the textures the caller
will hand the encoder in encode(...).
sourceTextureFormat |
DXGI source format | Use for |
|---|---|---|
D3d11HwSourceFormat.bgra (default) |
DXGI_FORMAT_B8G8R8A8_UNORM |
DXGI Desktop Duplication, Windows.Graphics.Capture, miniav screen capture, minigpu SharedOutputTexture (BGRA8 storage) |
D3d11HwSourceFormat.rgba |
DXGI_FORMAT_R8G8B8A8_UNORM |
Direct copies from RGBA8 storage textures (some custom WebGPU pipelines) |
Not every driver accepts RGBA in a D3D11VA hwframes pool — if it doesn't,
av_hwframe_ctx_init returns an error and openWith raises a
CodecInitException describing the format. BGRA is universally supported
on AMF / QSV / NVENC / MediaFoundation, which is why it is the default.
Tests #
dart test # full suite
dart test test/d3d11_hw_encoder_test.dart # Stage B only
Tests are tagged windows-gpu and skip cleanly on machines without
FFmpeg, the shim asset, or a D3D11VA-capable encoder. The end-to-end test
synthesises an NT-shared BGRA texture via the test-only shim helpers — no
display or capture device required.
License #
Apache 2.0