ff-preview 0.15.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 transparent auto-substitution.

Project status (as of 2026-06-04): The library foundation is in place. Development continues through avio-editor-demo, a real-world video editing application built on avio, which surfaces bugs and drives API improvements. Pull requests, bug reports, and feature requests are welcome — see the main repository for full context.

Installation

[dependencies]
ff-preview = "0.15"

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

# Enable proxy generation
ff-preview = { version = "0.15", 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 TimelinePlayer, TimelineRunner

MSRV

Rust 1.93.0 (edition 2024).

License

MIT OR Apache-2.0