ff-preview 0.16.0

Real-time video/audio preview and proxy workflow
Documentation

ff-preview

Real-time video preview and proxy workflow for Rust. Provides frame-accurate seek, audio-master A/V sync, a FrameSink trait for custom renderers, RGBA pixel delivery, and proxy generation with auto-substitution.

ff-preview adds a real-time, A/V-synchronised playback and seek loop on top of the decode primitives, converting frames to RGBA via libswscale for display. Decoding is delegated to ff-decode; this crate owns the playback clock, frame-accurate seek, and a FrameSink trait for custom renderers. Errors are typed and chain their source (PreviewError), so a failure reads as an actionable message rather than a raw FFmpeg return code.

It is an independent crate: use it on its own, or combine it with the other ff-* crates to assemble whatever media application, or editing model, you need. The ff-* crates are purified, model-free primitives, so none imposes an editing model on you; avio is one editing engine built on top of them. Each crate is versioned independently; see crates.io for current versions.

Installation

[dependencies]
ff-preview = "0.16"

# Enable async support
ff-preview = { version = "0.16", features = ["tokio"] }

# Enable proxy generation
ff-preview = { version = "0.16", features = ["proxy"] }

Quick Start

Playback with a custom RGBA sink

PreviewPlayer::open probes the file and prepares the pipeline. Call split() to obtain an exclusive PlayerRunner (owns the decode pipeline; register the sink and drive it with run()) and a cloneable PlayerHandle (non-blocking play / pause / seek / stop controls).

use std::thread;
use ff_preview::{PreviewPlayer, RgbaSink};

fn main() -> Result<(), ff_preview::PreviewError> {
    let (mut runner, handle) = PreviewPlayer::open("video.mp4")?.split();

    let sink = RgbaSink::new();
    let frames = sink.frame_handle(); // Arc<Mutex<Option<RgbaFrame>>> for the render thread
    runner.set_sink(Box::new(sink));

    thread::spawn(move || {
        let _ = runner.run();
    });

    handle.play();

    // In the render loop (any thread):
    if let Some(frame) = frames.lock().unwrap().as_ref() {
        // upload_to_gpu(&frame.data, frame.width, frame.height);
        let _ = (&frame.data, frame.width, frame.height, frame.pts);
    }

    Ok(())
}

Frame-accurate seek

use std::path::Path;
use std::time::Duration;
use ff_preview::{DecodeBuffer, FrameResult};

let mut buf = DecodeBuffer::open(Path::new("video.mp4")).build()?;
buf.seek(Duration::from_secs(30))?;

loop {
    match buf.pop_frame() {
        FrameResult::Frame(f) => {
            println!("pts: {:?}", f.timestamp().as_duration());
            break;
        }
        FrameResult::Seeking(_) => std::thread::sleep(Duration::from_millis(5)),
        FrameResult::Eof => break,
    }
}

Proxy generation

use std::path::Path;
use ff_preview::{ProxyGenerator, ProxyResolution};

let proxy_path = ProxyGenerator::new(Path::new("original_1080p.mp4"))?
    .resolution(ProxyResolution::Quarter)
    .output_dir(Path::new("/tmp"))
    .generate()?;

println!("proxy at {}", proxy_path.display());

Feature Flags

Feature What it enables
(default) PreviewPlayer, PlayerRunner, PlayerHandle, DecodeBuffer, PlaybackClock, FrameSink, RgbaSink, RgbaFrame, seek
tokio AsyncPreviewPlayer
proxy ProxyGenerator, ProxyJob, ProxyResolution
timeline Scene, ScenePlayer, SceneRunner

MSRV

Rust 1.93.0 (edition 2024).

License

MIT OR Apache-2.0