ff-decode 0.1.3

Video and audio decoding - the Rust way
Documentation

ff-decode

Safe, high-level video and audio decoding — no unsafe code required.

This crate provides frame-by-frame video/audio decoding, efficient seeking, and thumbnail generation. All APIs are safe — FFmpeg internals are fully encapsulated so you never need to write unsafe code.

Features

  • Video Decoding: Frame-by-frame decoding with Iterator pattern
  • Audio Decoding: Sample-level audio extraction
  • Seeking: Fast keyframe and exact seeking without file re-open
  • Thumbnails: Efficient thumbnail generation for timelines
  • Hardware Acceleration: Optional NVDEC, QSV, AMF, VideoToolbox, VAAPI support
  • Frame Pooling: Memory reuse for reduced allocation overhead

Quick Start

Add to your Cargo.toml:

[dependencies]

ff-decode = "0.1"

ff-format = "0.1"

Usage Examples

Video Decoding

use ff_decode::{VideoDecoder, SeekMode};
use ff_format::PixelFormat;
use std::time::Duration;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Open a video file and create decoder
    let mut decoder = VideoDecoder::open("video.mp4")?
        .output_format(PixelFormat::Rgba)
        .build()?;

    // Get basic info
    println!("Duration: {:?}", decoder.duration());
    println!("Resolution: {}x{}", decoder.width(), decoder.height());
    println!("FPS: {:.2}", decoder.frame_rate());

    // Decode frames sequentially using iterator
    for frame in decoder.frames().take(100) {
        let frame = frame?;
        println!("Frame at {:?}", frame.timestamp().as_duration());
        // Access frame data with frame.planes()
    }

    // Seek to specific position
    decoder.seek(Duration::from_secs(30), SeekMode::Keyframe)?;

    // Decode one frame after seeking
    if let Some(frame) = decoder.decode_one()? {
        println!("Frame at {:?}", frame.timestamp().as_duration());
    }

    Ok(())
}

Audio Decoding

use ff_decode::AudioDecoder;
use ff_format::SampleFormat;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut decoder = AudioDecoder::open("audio.mp3")?
        .output_format(SampleFormat::F32)
        .output_sample_rate(48000)
        .build()?;

    // Get audio info
    println!("Sample rate: {} Hz", decoder.sample_rate());
    println!("Channels: {}", decoder.channels());
    println!("Duration: {:?}", decoder.duration());

    // Decode all audio frames
    for frame in decoder.frames().take(100) {
        let frame = frame?;
        println!("Audio frame with {} samples", frame.samples());
    }

    Ok(())
}

Hardware Acceleration

use ff_decode::{VideoDecoder, HardwareAccel};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Auto-detect GPU and use hardware acceleration
    let decoder = VideoDecoder::open("video.mp4")?
        .hardware_accel(HardwareAccel::Auto)
        .build()?;

    // Or specify a specific accelerator
    let nvdec_decoder = VideoDecoder::open("video.mp4")?
        .hardware_accel(HardwareAccel::Nvdec)  // NVIDIA NVDEC
        .build()?;

    Ok(())
}

Seeking

use ff_decode::{VideoDecoder, SeekMode};
use std::time::Duration;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut decoder = VideoDecoder::open("video.mp4")?.build()?;

    // Fast keyframe seek (may be slightly inaccurate)
    decoder.seek(Duration::from_secs(30), SeekMode::Keyframe)?;

    // Exact seek (slower but frame-accurate)
    decoder.seek(Duration::from_secs(30), SeekMode::Exact)?;

    // Backward seek (seeks to keyframe at or before target)
    decoder.seek(Duration::from_secs(30), SeekMode::Backward)?;

    Ok(())
}

Thumbnail Generation

use ff_decode::VideoDecoder;
use std::time::Duration;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut decoder = VideoDecoder::open("video.mp4")?.build()?;

    // Generate a single thumbnail at specific time
    let thumbnail = decoder.thumbnail_at(
        Duration::from_secs(5),
        320,  // width
        180,  // height
    )?;

    // Generate multiple thumbnails for timeline
    let thumbnails = decoder.thumbnails(
        10,    // count
        160,   // width
        90,    // height
    )?;

    println!("Generated {} thumbnails", thumbnails.len());

    Ok(())
}

Frame Pooling

use ff_decode::{VideoDecoder, SimpleFramePool};
use std::sync::Arc;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a frame pool for memory reuse (32 frames)
    let pool = SimpleFramePool::new(32);

    let mut decoder = VideoDecoder::open("video.mp4")?
        .frame_pool(pool)
        .build()?;

    // Frames are automatically acquired from and returned to the pool
    for frame in decoder.frames().take(100) {
        let frame = frame?;
        // Process frame...
    }

    Ok(())
}

Performance

Benchmarking

Run performance benchmarks with:

# Run all benchmarks

cargo bench -p ff-decode


# Run specific benchmark group

cargo bench -p ff-decode -- seek


# Quick benchmarks (faster, less precise)

cargo bench -p ff-decode -- --quick

Benchmark Results

Typical performance on modern hardware (actual results may vary):

Operation Time Notes
Keyframe seek ~12ms Fast seeking to nearest keyframe
Exact seek ~20ms Frame-accurate seeking
Single frame decode ~0.3ms H.264 1080p
Thumbnail (320x180) ~12ms Including seek and scale
Sequential decode (100 frames) ~33ms ~0.33ms per frame

Seek Performance

One of the key features of ff-decode is efficient seeking without file re-opening. The benchmark seek_repeated/scrubbing_5_positions demonstrates this - 5 seeks complete in ~100ms (20ms per seek), which would be much slower if the file was being re-opened each time.

Platform Support

Platform Status Hardware Acceleration
Windows ✅ Tested NVDEC, QSV, AMF
macOS ✅ Tested VideoToolbox
Linux ✅ Tested VAAPI, NVDEC, QSV

Error Handling

All operations return Result<T, DecodeError>:

use ff_decode::{VideoDecoder, DecodeError};

fn decode_video(path: &str) -> Result<(), DecodeError> {
    let mut decoder = VideoDecoder::open(path)?
        .build()?;

    match decoder.decode_one()? {
        Some(frame) => println!("Got frame: {}x{}", frame.width(), frame.height()),
        None => println!("End of stream"),
    }

    Ok(())
}

Error types include:

  • DecodeError::FileNotFound - File doesn't exist
  • DecodeError::NoVideoStream - No video stream in file
  • DecodeError::NoAudioStream - No audio stream in file
  • DecodeError::UnsupportedCodec - Codec not supported
  • DecodeError::DecodingFailed - Decoding error
  • DecodeError::SeekFailed - Seek operation failed
  • DecodeError::HwAccelUnavailable - Hardware acceleration unavailable
  • DecodeError::EndOfStream - End of stream reached

Module Structure

  • video - Video decoder for extracting video frames
  • audio - Audio decoder for extracting audio frames
  • error - Error types for decoding operations
  • pool - Frame pool trait for memory reuse

Architecture

This crate is part of the ff-* suite of crates that provide a safe Rust wrapper around FFmpeg:

  • ff-sys - Low-level FFI bindings (internal)
  • ff-format - Common types (VideoFrame, AudioFrame, etc.)
  • ff-probe - Metadata extraction
  • ff-decode - Decoding (this crate)
  • ff-encode - Encoding
  • ff-filter - Filters and effects

Performance Characteristics

Typical performance on modern hardware (see benchmarks with cargo bench -p ff-decode):

  • Frame decode: ~0.3ms for 1080p H.264 (software decoding)
  • Seek (keyframe): ~12ms per seek (no file re-open)
  • Seek (exact): ~20ms per seek (includes frame skipping)
  • Thumbnail generation: ~12ms per thumbnail (320x180)
  • Scrubbing (5 positions): ~100ms total (~20ms per position)

Hardware acceleration can significantly improve decode performance.

License

Licensed under either of:

  • MIT license
  • Apache License, Version 2.0

at your option.