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**]https://github.com/itsakeyfut/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]https://github.com/itsakeyfut/avio for full context.

## Installation

```toml
[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).

```rust
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

```rust
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

```rust
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