mediadecode 0.19.0

The read faces of the mediaio family: the decoder traits, the frame sources and the Demuxer session, over the mediaspine vocabulary. no_std-friendly; backends for FFmpeg, WebCodecs, R3D, BRAW, ARRIRAW, X-OCN, ProRes RAW, Canon Cinema RAW Light.
Documentation

The read faces of the mediaio family: the decoders and the demuxer, no_std-friendly.

The read faces of the mediaio family: the push-style decoder traits, the pull-style frame sources, the one-shot image decoder, the Demuxer session, and their async mirrors under future. A backend implements them; a consumer programs against them and never names the backend.

The vocabulary the faces speak — frames, packets, subtitles, the push rhythm's Sent and Received, the demuxer's track types, the adapter traits, the resampler face, the time primitives — is mediaspine's. This crate re-exports it under the module names it has always used (mediadecode::frame, mediadecode::packet, mediadecode::demuxer, mediadecode::Received, …), so every path a consumer wrote still resolves, to the same item a mediaspine:: path names. The write faces are mediaencode's.

This crate ships no decoder code and no FFmpeg dependency. It is no_std-clean (with optional alloc / std features). Adapter implementations live in sibling crates: mediadecode-ffmpeg and mediadecode-webcodecs.

What's in the box

  • Decoder traits — VideoStreamDecoder, AudioStreamDecoder, SubtitleDecoder, ImageDecoder. The two *Stream* faces are push-style (send_packet / receive_frame / send_eof / flush), mirroring FFmpeg's decoder API while staying backend-agnostic; the other two are not, and their names say so — a subtitle cue and a still image each come out of exactly the packet that went in. VideoStreamDecoder also states whether it can decode to a smaller size (ScaledOutputCapability).
  • Frame sources — VideoFrameSource, AudioFrameSource: the pull-style faces of clip-oriented SDKs (R3D, BRAW, ARRIRAW, X-OCN, Canon Cinema RAW Light), which hand out frames by index.
  • The demux face — Demuxer, the pull session over an opened container (tracks() / next_packet() / seek(), with the chapter table, the container duration and the measured end provided). Opening is each backend's own, so the trait covers only the opened session. A session keeps its track table for life: tracks() hands out TrackHandles — the backend's own row carrier, Arc / Rc / a plain borrow — so a consumer that needs a row past the pull loop clones a refcount rather than taking the table away from the session.
  • Async faces — under the future feature, every decoder face again with async fn methods, in a !Send and a Send-bounded variant.
  • The vocabulary, re-exported — everything above is written against mediaspine's types, re-exported here: PixelFormat and the color and CFA vocabulary from mediaframe, the packet and frame households, the carrier amputation contract every backend's buffer type obeys (on the adapter module), the demuxer's TrackInfo / TrackParams / DemuxedPacket, the resampler face, and Timebase / Timestamp / TimeRange with the rest of mediatime as mediadecode::mediatime.

Cargo features

Feature Default Effect
std yes The standard library, here and in mediaspine.
alloc — Owning collections without std, here and in mediaspine.
future — The async faces under mediadecode::future.
serde — mediaspine's wire formats.
arbitrary — mediaspine's Arbitrary impls, for fuzzing.
quickcheck — mediaspine's quickcheck impls.

no_std builds: disable defaults and pick alloc if you need Vec-backed payloads:

[dependencies]
mediadecode = { version = "0.19", default-features = false, features = ["alloc"] }

Usage

This crate defines the surface; concrete decoding happens in adapter crates. A backend-agnostic consumer programs against the traits, and both faces answer states: send_packet answers Sent — accepted or must drain — and receive_frame answers Received — produced, needs input, or ended. Err means a fault and nothing else:

use mediadecode::{
  Received, Sent,
  adapter::VideoAdapter,
  decoder::VideoStreamDecoder,
  frame::VideoFrame,
  packet::VideoPacket,
};

type Frame<D> = VideoFrame<
  <<D as VideoStreamDecoder>::Adapter as VideoAdapter>::PixelFormat,
  <<D as VideoStreamDecoder>::Adapter as VideoAdapter>::FrameExtra,
  <D as VideoStreamDecoder>::Buffer,
>;

/// Feeds one packet and delivers every frame it made ready.
/// Answers `true` once the stream is over.
fn decode_one<D: VideoStreamDecoder>(
  decoder: &mut D,
  packet: &VideoPacket<
    <D::Adapter as VideoAdapter>::PacketExtra,
    D::Buffer,
  >,
  dst: &mut Frame<D>,
  mut on_frame: impl FnMut(&Frame<D>),
) -> Result<bool, D::Error> {
  // Offer until the decoder takes it. `MustDrain` promises nothing was
  // consumed, so the *same* packet is re-offered after a drain.
  loop {
    // `?` gives up on a real failure — and only on a real failure.
    match decoder.send_packet(packet)? {
      Sent::Accepted => break,
      Sent::MustDrain => {
        while let Received::Produced = decoder.receive_frame(dst)? {
          on_frame(dst);
        }
      }
    }
  }
  loop {
    match decoder.receive_frame(dst)? {
      Received::Produced => on_frame(dst),
      Received::NeedsInput => return Ok(false),
      Received::Ended => return Ok(true),
    }
  }
}

The compiler is what makes this correct: a consumer that forgets end-of-stream does not compile, a receive-side failure can no longer be mistaken for a drained decoder, and back pressure no longer has to be guessed at by offering every packet twice.

For an end-to-end example using the FFmpeg adapter, see mediadecode-ffmpeg.

Build requirements

  • Rust ≥ 1.95, edition 2024.
  • No system dependencies — mediadecode is FFmpeg-free and builds anywhere Rust does, including no_std targets with optional alloc.

License

mediadecode 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.