FFmpeg adapter for the mediadecode abstraction
layer, built on top of
ffmpeg-next.
Implements mediadecode's VideoAdapter / AudioAdapter /
SubtitleAdapter / ImageAdapter traits, the matching push-style
*StreamDecoder traits, the one-shot ImageDecoder, and Demuxer.
Every byte a frame or packet carries is copied once, at the FFmpeg
boundary, into an FfmpegBytes — mediadecode 0.9's D-seat
amputation contract. A delivered frame is owned, Send + Sync, and
cheap to clone (a refcount bump); it holds nothing of libavcodec's
open, so it can cross a channel, be read from several threads, and
outlive the decoder that produced it. Through 0.8 the planes were
refcounted views into AVBufferRef behind an FfmpegBuffer type,
which meant every consumer inherited an FFmpeg lifetime it could not
see. That type is gone.
FfmpegVideoStreamDecoder mirrors the send_packet / receive_frame
shape of ffmpeg::decoder::Video, auto-probes the host's HW backends,
and falls through to a software decoder when none open. Audio and
subtitles use parallel FfmpegAudioStreamDecoder /
FfmpegSubtitleStreamDecoder types.
Backends
FfmpegVideoStreamDecoder::open walks this probe order, opening the
first backend that accepts the stream:
| Target | Probe order |
|---|---|
| macOS / iOS / tvOS | VideoToolbox → software |
| Linux | VAAPI → CUDA → software |
| Windows | D3D11VA → CUDA → software |
| other | software |
Output frames are CPU-side, downloaded with av_hwframe_transfer_data
(NV12 for 8-bit, P010/P012/P016/P210/P212/P216/P410/P412/P416 for
10/12/16-bit). Pixel-format conversion is intentionally out of scope
— downstream
colconv handles it.
If every HW backend opens but later fails at decode time and the
software backend is also unavailable, the error surfaces as
VideoDecodeError::Decode(Error::AllBackendsFailed(p)) carrying any
packets the decoder had already accepted from the demuxer (accessible
via p.unconsumed_packets() / p.into_unconsumed_packets()) — so
non-seekable callers (live streams, pipes, network sources) can replay
them through their own software decoder without re-demuxing.
Usage
use ffmpeg_next as ffmpeg;
use ;
use ;
use ;
Audio and subtitle decoding share the shape — see
examples/decode_via_trait.rs and
tests/audio_subtitle_via_trait.rs
for end-to-end demuxer-driven runs that cover all three streams.
Public surface map
- Decoders:
FfmpegVideoStreamDecoder,FfmpegAudioStreamDecoder,FfmpegSubtitleStreamDecoder. Plus their error types:VideoDecodeError,AudioDecodeError,SubtitleDecodeError. - Demuxer:
FfmpegDemuxer—mediadecode'sDemuxeroverlibavformat, opened from a path (open) or from anyRead + Seekbyte source through a customAVIOContext(open_reader). PlusDemuxError. - Resampler (
resamplefeature, on by default):FfmpegResampler—mediadecode'sAudioResampleroverswresample, built from two explicitResampleSpecs (the source read off a track or off the opened decoder, the target the caller's). PlusResampleError, whoseAgainvariant is the "needs more input" signal and whoseSourceChangedvariant is the mid-stream refusal. Disabling the feature drops the type and thelibswresamplelink along with it. FfmpegImageDecoder: the one-shotImageDecoder— cover art in,ImageFrameout. Opened from an attachment track's codec parameters, which the demuxer's cover-art reclassification retains in full. The picture's EXIF orientation comes back typed onImageFrameExtra, read off the display matrix libavcodec emits for it.- Type aliases:
VideoPacket,AudioPacket,SubtitlePacket,DataPacket,AttachmentPacket,DemuxedPacket,VideoFrame,AudioFrame,SubtitleFrame,ImageFrame,TrackInfo,TrackParams— themediadecodegeneric types pre-parameterized with this crate's adapter / carrier / extras, so you don't have to spell them out. - Carrier:
FfmpegBytes— owned,Send + Sync,AsRef<[u8]>, cloning by refcount. Opaque over anArc<[u8]>, because the storage gains a pooled strategy later (#35) and a consumer must not have to recompile for it. Construct one withFfmpegBytes::copy_from_slice/::emptywhen feeding a packet back into a decoder. - Boundary helpers:
video_packet_from_ffmpeg,audio_packet_from_ffmpeg,subtitle_packet_from_ffmpeg— convert a borrowedffmpeg::Packetinto the matchingmediadecodepacket, copying the compressed payload out. Their*_insiblings (video_packet_from_ffmpeg_in,audio_packet_from_ffmpeg_in,subtitle_packet_from_ffmpeg_in,data_packet_from_ffmpeg_in) take the stream's timebase, so the producedTimestampsays what its ticks mean instead of carrying the 1/1 placeholder anAVPacketalone leaves you with.attachment_packet_from_ffmpegwraps a cover-art packet, which has no timestamps to carry. - Empty-frame builders:
empty_video_frame,empty_audio_frame,empty_subtitle_frame— well-formed destinations forreceive_frame.
Running tests and benches
The fixture-gated tests and the benchmark expect real media files,
named by five environment variables. The unit tests run
unconditionally; the gated ones are #[ignore]d, so --ignored
is what opts into them.
| Variable | Feeds |
|---|---|
HWDECODE_SAMPLE_VIDEO |
tests/decode.rs, tests/hw_smoke.rs, benches/decode.rs, and the three decoder::tests backend cases |
MEDIADECODE_SAMPLE_VIDEO |
tests/decode_via_trait.rs |
MEDIADECODE_SAMPLE_AUDIO |
the audio-through-trait case (any container with an audio track) |
MEDIADECODE_SAMPLE_SUBTITLE |
the subtitle-through-trait case (needs a container that really carries a subtitle track) |
MEDIADECODE_FX3_SAMPLE |
the Sony FX3 H.264 High 4:2:2 10-bit mid-stream HW→SW fallback case |
HWDECODE_SAMPLE_VIDEO=/path/to/clip.mp4
HWDECODE_SAMPLE_VIDEO=/path/to/clip.mp4
# The whole fixture-gated set at once.
HWDECODE_SAMPLE_VIDEO=/path/to/clip.mp4 \
MEDIADECODE_SAMPLE_VIDEO=/path/to/clip.mp4 \
MEDIADECODE_SAMPLE_AUDIO=/path/to/clip.mp4 \
MEDIADECODE_SAMPLE_SUBTITLE=/path/to/subtitled.mkv \
MEDIADECODE_FX3_SAMPLE=/path/to/12_sony_fx3_xavc.mp4 \
A variable left unset is not always a quiet skip: the FX3 case prints
a notice and returns, but the trait cases panic on a missing path
once --ignored has opted into them.
Build requirements
- A system FFmpeg ≥ 5.1 linkable via
pkg-config(we referenceAV_PIX_FMT_P212LE/AV_PIX_FMT_P412LE, which were added in 5.1). Tested against 9.0. Verify withffmpeg -hwaccelsthat your build has the backends you expect compiled in (e.g.videotoolboxon macOS,vaapi/cudaon Linux,d3d11va/cudaon Windows). - Rust ≥ 1.95, edition 2024.
License
mediadecode-ffmpeg is under the terms of both the MIT license and the
Apache License (Version 2.0).
See LICENSE-APACHE, LICENSE-MIT for details.
Copyright (c) 2026 FinDIT Studio authors.