27 stable releases (3 major)
Uses new Rust 2024
| 5.1.0 | Feb 14, 2026 |
|---|---|
| 5.0.1 | Feb 14, 2026 |
| 4.3.8 | Feb 14, 2026 |
| 3.0.0 | Feb 12, 2026 |
| 2.0.1 | Feb 12, 2026 |
#39 in Video
535KB
9K
SLoC
unbundle
A clean, ergonomic Rust library for extracting video frames, audio tracks, and subtitles from media files using FFmpeg.
New: unbundle-cli MVP
Install:
cargo install unbundle
Commands:
# Probe metadata
unbundle metadata input.mp4
# Extract frames
unbundle extract-frames input.mp4 --out frames --every 10 --start 0 --end 300
# Extract full audio
unbundle extract-audio input.mp4 --format mp3 --out audio.mp3
# Extract subtitle file
unbundle extract-subs input.mkv --format srt --out subs.srt
# Validate media structure
unbundle validate input.mp4
Quick Comparison
| Tool | Best for | Level | Notes |
|---|---|---|---|
unbundle |
Rust-native media extraction APIs | High-level | Strong typing, iterators, options, errors |
unbundle-cli |
Simple extraction workflows | High-level | Friendly commands for common tasks |
ffmpeg-next |
Building custom low-level media flows | Mid/Low-level | Flexible, but more boilerplate |
ffmpeg CLI |
One-off shell pipelines | CLI-level | Powerful, but less ergonomic in Rust code |
CI is passing on Linux, macOS, and Windows (see the CI badge above). Contributing guide: CONTRIBUTING.md.
Project metadata
- Homepage: https://docs.rs/unbundle
- Repository: https://github.com/skanderjeddi/unbundle
- Changelog: https://github.com/skanderjeddi/unbundle/releases
- Topics: rust, ffmpeg, video, audio, subtitles, media-processing
use unbundle::MediaFile;
let mut unbundler = MediaFile::open("video.mp4")?;
// Extract a frame at 30 seconds
let frame = unbundler.video().frame_at(Duration::from_secs(30))?;
frame.save("frame_30s.png")?;
// Extract complete audio track
unbundler.audio().save("audio.wav", AudioFormat::Wav)?;
Why unbundle?
- Type-safe API — frames as
image::DynamicImage, audio as bytes or files, subtitles as structured events - Flexible extraction — by frame number, timestamp, range, interval, or custom frame lists
- Streaming support — lazy iterators and async streams avoid buffering entire frame sets
- Rich metadata — dimensions, frame rates, codecs, chapters, per-frame decode info
- Production-ready — progress callbacks, cancellation tokens, hardware acceleration, parallel processing
Use Cases
- Video thumbnails — contact sheets, smart frame selection, chapter previews
- Media processing — format conversion, audio extraction, subtitle manipulation
- Analysis tools — scene detection, keyframe analysis, Group of Pictures structure inspection
- Content indexing — frame extraction for search, waveform visualization
- Transcoding pipelines — lossless remuxing, audio re-encoding
Contact-sheet mini-guide
use unbundle::{MediaFile, ThumbnailHandle, ThumbnailOptions};
let mut unbundler = MediaFile::open("input.mp4")?;
let options = ThumbnailOptions::new(4, 3).with_thumbnail_width(320);
let sheet = ThumbnailHandle::grid(&mut unbundler, &options)?;
sheet.save("contact_sheet.png")?;
This creates a 4×3 contact sheet from evenly spaced frames across the video.
Installation
Add to your Cargo.toml:
[dependencies]
unbundle = "5.1.0"
Or with additional features:
[dependencies]
unbundle = { version = "5.1.0", features = ["async", "rayon", "hardware"] }
System Requirements
unbundle requires FFmpeg libraries (4.0+) installed on your system.
Linux (Debian/Ubuntu):
sudo apt-get install libavcodec-dev libavformat-dev libavutil-dev \
libswscale-dev libswresample-dev libavdevice-dev pkg-config
macOS:
brew install ffmpeg pkg-config
Windows:
Use vcpkg (recommended for headers + libs):
# Install vcpkg if needed
git clone https://github.com/microsoft/vcpkg.git C:\vcpkg
C:\vcpkg\bootstrap-vcpkg.bat
# Install FFmpeg for MSVC x64
vcpkg install ffmpeg:x64-windows
# Configure environment for build scripts
setx VCPKG_ROOT "C:\vcpkg"
setx VCPKGRS_DYNAMIC "1"
setx FFMPEG_DIR "C:\vcpkg\installed\x64-windows"
setx PATH "%PATH%;C:\vcpkg\installed\x64-windows\bin"
Then restart your terminal and run cargo build.
For the current shell only (without setx):
$env:VCPKG_ROOT = "C:\vcpkg"
$env:VCPKGRS_DYNAMIC = "1"
$env:FFMPEG_DIR = "C:\vcpkg\installed\x64-windows"
$env:PATH = "C:\vcpkg\installed\x64-windows\bin;" + $env:PATH
The crate includes a Windows build helper (build.rs) that emits guidance when
FFMPEG_DIR is missing and a vcpkg install is detected.
Quick Start
Extract Video Frames
use std::time::Duration;
use unbundle::MediaFile;
let mut unbundler = MediaFile::open("input.mp4")?;
// Extract the first frame
let frame = unbundler.video().frame(0)?;
frame.save("first_frame.png")?;
// Extract a frame at 30 seconds
let frame = unbundler.video().frame_at(Duration::from_secs(30))?;
frame.save("frame_30s.png")?;
Extract Multiple Frames
use std::time::Duration;
use unbundle::{FrameRange, MediaFile};
let mut unbundler = MediaFile::open("input.mp4")?;
// Every 30th frame
let frames = unbundler.video().frames(FrameRange::Interval(30))?;
// Frames between two timestamps
let frames = unbundler.video().frames(
FrameRange::TimeRange(Duration::from_secs(10), Duration::from_secs(20))
)?;
// Specific frame numbers
let frames = unbundler.video().frames(
FrameRange::Specific(vec![0, 50, 100, 150])
)?;
Open from URLs or Streams
use unbundle::MediaFile;
// Works with paths, file:// URLs, and network URLs supported by your FFmpeg build.
let mut unbundler = MediaFile::open_url("https://example.com/video.mp4")?;
let metadata = unbundler.metadata();
println!("Format: {}", metadata.format);
open_url() accepts any FFmpeg input string, including http://, https://, rtsp://, and local path-like sources.
Apply FFmpeg Filters to Frames
use unbundle::MediaFile;
let mut unbundler = MediaFile::open("input.mp4")?;
// Chain multiple filters incrementally.
let frame = unbundler
.video()
.filter("scale=320:240")
.filter("eq=contrast=1.1")
.frame(0)?;
frame.save("filtered.png")?;
Streaming Frame Iteration
use unbundle::{FrameRange, MediaFile};
let mut unbundler = MediaFile::open("input.mp4")?;
// Push-based: process each frame without buffering
unbundler.video().for_each_frame(
FrameRange::Range(0, 99),
|frame_number, image| {
image.save(format!("frame_{frame_number}.png"))?;
Ok(())
},
)?;
// Pull-based: lazy iterator with early exit
let iter = unbundler.video().frame_iter(FrameRange::Range(0, 99))?;
for result in iter {
let (frame_number, image) = result?;
image.save(format!("frame_{frame_number}.png"))?;
}
Extract Audio
use std::time::Duration;
use unbundle::{AudioFormat, MediaFile};
let mut unbundler = MediaFile::open("input.mp4")?;
// Save complete audio track to WAV
unbundler.audio().save("output.wav", AudioFormat::Wav)?;
// Extract a 30-second segment as MP3
unbundler.audio().save_range(
"segment.mp3",
Duration::from_secs(30),
Duration::from_secs(60),
AudioFormat::Mp3,
)?;
// Extract audio to memory
let audio_bytes = unbundler.audio().extract(AudioFormat::Wav)?;
// Multi-track: extract the second audio track
let audio_bytes = unbundler.audio_track(1)?.extract(AudioFormat::Wav)?;
Extract Subtitles
use unbundle::{MediaFile, SubtitleFormat};
let mut unbundler = MediaFile::open("input.mkv")?;
// Extract subtitle events with timing
let events = unbundler.subtitle().extract()?;
for event in &events {
println!("[{:?} → {:?}] {}", event.start_time, event.end_time, event.text);
}
// Save as SRT file
unbundler.subtitle().save("output.srt", SubtitleFormat::Srt)?;
// Multi-track: extract the second subtitle track
unbundler.subtitle_track(1)?.save("track2.vtt", SubtitleFormat::WebVtt)?;
Raw Stream Copy (No Re-encode)
use std::time::Duration;
use unbundle::MediaFile;
let mut unbundler = MediaFile::open("input.mp4")?;
// Copy video packets exactly as-is (codec preserved)
unbundler.video().stream_copy("video_copy.mp4")?;
// Copy a time segment without decoding/re-encoding
unbundler.video().stream_copy_range(
"segment.mp4",
Duration::from_secs(10),
Duration::from_secs(20),
)?;
// In-memory stream copy
let mkv_bytes = unbundler.video().stream_copy_to_memory("matroska")?;
// Audio stream copy (codec preserved)
unbundler.audio().stream_copy("audio_copy.aac")?;
let adts_bytes = unbundler.audio().stream_copy_to_memory("adts")?;
// Subtitle stream copy (codec preserved)
unbundler.subtitle().stream_copy("subs_copy.mkv")?;
let subtitle_bytes = unbundler.subtitle().stream_copy_to_memory("matroska")?;
Advanced Filter Graph Recipes
use unbundle::MediaFile;
let mut unbundler = MediaFile::open("input.mp4")?;
// Complex chain: resize -> crop -> mirror -> color adjust
let frame = unbundler.video().frame_with_filter(
42,
"scale=640:480,crop=320:240:10:20,hflip,transpose=1",
)?;
frame.save("filtered_advanced.png")?;
// Another chain: downscale + grayscale + mirror
let frame = unbundler
.video()
.frame_with_filter(42, "scale=iw/2:ih/2,format=gray,hflip")?;
frame.save("filtered_denoise_gray.png")?;
Progress & Cancellation
use std::sync::Arc;
use unbundle::{
CancellationToken, ExtractOptions, FrameRange,
MediaFile, ProgressCallback, ProgressInfo,
};
struct PrintProgress;
impl ProgressCallback for PrintProgress {
fn on_progress(&self, info: &ProgressInfo) {
println!("Frame {}/{}", info.current, info.total.unwrap_or(0));
}
}
let token = CancellationToken::new();
let config = ExtractOptions::new()
.with_progress(Arc::new(PrintProgress))
.with_cancellation(token.clone());
let mut unbundler = MediaFile::open("input.mp4")?;
let frames = unbundler.video().frames_with_options(
FrameRange::Range(0, 99),
&config,
)?;
Features
Core Capabilities
- Frame extraction — by frame number, timestamp, range, interval, or specific frame list
- Audio extraction — to WAV, MP3, FLAC, or AAC (file or in-memory)
- Subtitle extraction — decode text-based subtitles to SRT, WebVTT, or raw text
- Container remuxing — lossless format conversion (e.g. MKV → MP4) without re-encoding
- Rich metadata — video dimensions, frame rate, frame count, audio sample rate, channels, codec info, multi-track audio/subtitle metadata
- Configurable output — pixel format (RGB8, RGBA8, GRAY8), target resolution with aspect ratio preservation
- Custom FFmpeg filters — apply filter graphs during frame extraction (e.g. scale, crop, eq, hflip)
- Progress & cancellation — cooperative progress callbacks and
CancellationTokenfor long-running operations - Streaming iteration — lazy
FrameIterator(pull-based) andfor_each_frame(push-based) without buffering entire frame sets - Audio sample iteration — lazy
AudioIteratoryields mono f32 chunks for incremental audio processing - Validation — inspect media files for structural issues before extraction
- Chapter support — extract chapter metadata (titles, timestamps) from containers
- Frame metadata — per-frame decode info (PTS, keyframe flag, picture type) via
frame_and_metadata/frames_and_metadata - Segmented extraction — extract frames from multiple disjoint time ranges in a single call with
FrameRange::Segments - Stream probing — lightweight
MediaProbefor quick metadata inspection without keeping the demuxer open - Thumbnail helpers — single-frame thumbnails, contact-sheet grids, and variance-based "smart" thumbnail selection
- Keyframe & Group of Pictures analysis — scan video packets for keyframe positions and Group of Pictures structure without decoding
- VFR detection — detect variable frame rate streams and analyze PTS distributions
- Packet iteration — raw packet-level demuxer iteration for advanced inspection
- Raw stream copy — copy video/audio/subtitle packets directly to file or memory without re-encoding
- Efficient seeking — seeks to the nearest keyframe, then decodes forward
- Zero-copy in-memory audio — uses FFmpeg's dynamic buffer I/O
Optional Features
Enable additional functionality through Cargo features:
| Feature | Description |
|---|---|
async |
FrameStream (async frame iteration) and AudioFuture via Tokio |
rayon |
frames_parallel() distributes decoding across rayon threads |
hardware |
Hardware-accelerated decoding (CUDA, VAAPI, DXVA2, D3D11VA, VideoToolbox, QSV) |
scene |
Scene change detection via FFmpeg's scdet filter |
gif |
Animated GIF export from video frames |
waveform |
Audio waveform visualization data (min/max/RMS per bin) |
loudness |
Peak/RMS loudness analysis with dBFS conversion |
transcode |
Audio re-encoding between formats (e.g. AAC → MP3) |
encode |
Encode DynamicImage sequences into video files (H.264, H.265, MPEG-4) |
full |
Enables all of the above |
[dependencies]
unbundle = { version = "5.1.0", features = ["full"] }
Feature Usage Guide
- Use
asyncwhen integrating with async web servers or when processing multiple videos concurrently - Use
rayonfor CPU-intensive batch frame extraction (e.g., generating thousands of thumbnails) - Use
hardwarewhen processing high-resolution video (4K+) or when CPU is a bottleneck - Use
scenefor video analysis, automatic chapter detection, or intelligent thumbnail selection - Use
giffor creating preview animations or social media content - Use
waveformandloudnessfor audio visualization or normalization workflows - Use
transcodefor audio format conversion in media pipelines - Use
encodefor creating time-lapses, slideshows, or re-encoding frame sequences
Examples
The examples/ directory contains complete, runnable examples:
| Example | Description |
|---|---|
extract_frames |
Extract frames by number, timestamp, range, interval |
extract_audio |
Extract the complete audio track |
extract_audio_segment |
Extract a specific time range as MP3 |
open_url |
Open from URL/path-like source strings |
thumbnail |
Create a thumbnail grid from evenly-spaced frames |
metadata |
Display all media metadata |
video_iterator |
Lazy frame iteration with early exit |
pixel_formats |
Demonstrate RGB8/RGBA8/GRAY8 output |
progress |
Progress callbacks and cancellation |
subtitle |
Extract subtitles as SRT/WebVTT/raw text |
remux |
Lossless container format conversion |
validate |
Media file validation report |
async_extraction |
Async frame streaming and audio extraction (async) |
rayon |
Parallel frame extraction across threads (rayon) |
scene |
Scene change detection (scene) |
hardware_acceleration |
Hardware-accelerated decoding (hardware) |
gif_export |
Export video frames as animated GIF (gif) |
waveform |
Generate audio waveform data (waveform) |
loudness |
Analyze audio loudness levels (loudness) |
audio_iterator |
Lazy audio sample iteration |
video_encoder |
Encode image sequences into video files (encode) |
transcode |
Re-encode audio between formats (transcode) |
keyframe |
Group of Pictures/keyframe structure analysis |
variable_framerate |
Variable frame rate detection |
packet_iterator |
Raw packet-level demuxer inspection |
subtitle_search |
Search subtitle text content |
Run an example:
cargo run --example metadata -- path/to/video.mp4
Changelog
Release notes are tracked in CHANGELOG.md.
Advanced Usage
Container Remuxing
use unbundle::Remuxer;
// Convert MKV to MP4 without re-encoding
Remuxer::new("input.mkv", "output.mp4")?.run()?;
// Exclude subtitles during remux
Remuxer::new("input.mkv", "output.mp4")?
.exclude_subtitles()
.run()?;
Custom Output Format
use unbundle::{ExtractOptions, FrameRange, MediaFile, PixelFormat};
let config = ExtractOptions::new()
.with_pixel_format(PixelFormat::Rgba8)
.with_resolution(Some(1280), Some(720));
let mut unbundler = MediaFile::open("input.mp4")?;
let frames = unbundler.video().frames_with_options(
FrameRange::Interval(30),
&config,
)?;
Read Metadata
use unbundle::MediaFile;
let unbundler = MediaFile::open("input.mp4")?;
let metadata = unbundler.metadata();
println!("Duration: {:?}", metadata.duration);
println!("Format: {}", metadata.format);
if let Some(video) = &metadata.video {
println!("Video: {}x{}, {:.2} fps, {} frames",
video.width, video.height,
video.frames_per_second, video.frame_count);
println!("Codec: {}", video.codec);
}
if let Some(audio) = &metadata.audio {
println!("Audio: {} Hz, {} channels, codec: {}",
audio.sample_rate, audio.channels, audio.codec);
}
// List all audio and subtitle tracks
if let Some(tracks) = &metadata.audio_tracks {
println!("{} audio track(s)", tracks.len());
}
if let Some(tracks) = &metadata.subtitle_tracks {
println!("{} subtitle track(s)", tracks.len());
}
Validate Media Files
use unbundle::MediaFile;
let unbundler = MediaFile::open("input.mp4")?;
let report = unbundler.validate();
if report.is_valid() {
println!("File is valid");
} else {
println!("{report}");
}
Probe Media Files
use unbundle::MediaProbe;
// Quick metadata inspection without keeping the file open
let metadata = MediaProbe::probe("input.mp4")?;
println!("Duration: {:?}", metadata.duration);
// Probe multiple files at once
let results = MediaProbe::probe_many(&["video1.mp4", "video2.mkv"]);
Chapter Metadata
use unbundle::MediaFile;
let unbundler = MediaFile::open("input.mkv")?;
let metadata = unbundler.metadata();
if let Some(chapters) = &metadata.chapters {
for chapter in chapters {
println!("[{:?} → {:?}] {}",
chapter.start, chapter.end,
chapter.title.as_deref().unwrap_or("Untitled"));
}
}
Frame Metadata
use unbundle::MediaFile;
let mut unbundler = MediaFile::open("input.mp4")?;
// Get a frame with its decode metadata
let (image, info) = unbundler.video().frame_and_metadata(0)?;
println!("Frame {}: keyframe={}, type={:?}, pts={:?}",
info.frame_number, info.is_keyframe, info.frame_type, info.pts);
Thumbnail Generation
use std::time::Duration;
use unbundle::{MediaFile, ThumbnailHandle, ThumbnailOptions};
let mut unbundler = MediaFile::open("input.mp4")?;
// Single thumbnail at a timestamp
let thumb = ThumbnailHandle::at_timestamp(&mut unbundler, Duration::from_secs(5), 320)?;
// Contact-sheet grid
let config = ThumbnailOptions::new(4, 3); // 4 columns × 3 rows
let grid = ThumbnailHandle::grid(&mut unbundler, &config)?;
grid.save("contact_sheet.png")?;
// Smart thumbnail (picks frame with highest visual variance)
let smart = ThumbnailHandle::smart(&mut unbundler, 10, 320)?;
GIF Export
use std::time::Duration;
use unbundle::{FrameRange, GifOptions, MediaFile};
let mut unbundler = MediaFile::open("input.mp4")?;
let config = GifOptions::new().width(320).frame_delay(10);
unbundler.video().export_gif(
"output.gif",
FrameRange::TimeRange(Duration::from_secs(0), Duration::from_secs(5)),
&config,
)?;
// Or export to memory
let bytes = unbundler.video().export_gif_to_memory(
FrameRange::Interval(10),
&config,
)?;
Audio Waveform
use unbundle::{MediaFile, WaveformOptions};
let mut unbundler = MediaFile::open("input.mp4")?;
let waveform = unbundler.audio().generate_waveform(
&WaveformOptions::new().bins(1000),
)?;
for bin in &waveform.bins {
println!("min={:.3} max={:.3} rms={:.3}", bin.min, bin.max, bin.rms);
}
Loudness Analysis
use unbundle::MediaFile;
let mut unbundler = MediaFile::open("input.mp4")?;
let loudness = unbundler.audio().analyze_loudness()?;
println!("Peak: {:.1} dBFS, RMS: {:.1} dBFS", loudness.peak_dbfs, loudness.rms_dbfs);
Audio Sample Iteration
use unbundle::MediaFile;
let mut unbundler = MediaFile::open("input.mp4")?;
let iter = unbundler.audio().sample_iter()?;
let mut total_samples = 0u64;
for chunk in iter {
let chunk = chunk?;
total_samples += chunk.samples.len() as u64;
}
println!("Total mono samples: {total_samples}");
Audio Transcoding
use unbundle::{AudioFormat, MediaFile, Transcoder};
let mut unbundler = MediaFile::open("input.mp4")?;
// Re-encode audio from the source format to MP3
Transcoder::new(&mut unbundler)
.format(AudioFormat::Mp3)
.run("output.mp3")?;
Video Writing
use unbundle::{MediaFile, FrameRange, VideoEncoder, VideoEncoderOptions, VideoCodec};
let mut unbundler = MediaFile::open("input.mp4")?;
let frames = unbundler.video().frames(FrameRange::Interval(30))?;
let config = VideoEncoderOptions::default()
.resolution(1920, 1080)
.frames_per_second(24)
.codec(VideoCodec::H264);
VideoEncoder::new(config).write("output.mp4", &frames)?;
Keyframe & Group of Pictures Analysis
use unbundle::MediaFile;
let mut unbundler = MediaFile::open("input.mp4")?;
let group_of_pictures = unbundler.video().analyze_group_of_pictures()?;
println!(
"Keyframes: {}, Average Group of Pictures size: {:.1}",
group_of_pictures.keyframes.len(),
group_of_pictures.average_group_of_pictures_size
);
VFR Detection
use unbundle::MediaFile;
let mut unbundler = MediaFile::open("input.mp4")?;
let analysis = unbundler.video().analyze_variable_framerate()?;
println!("VFR: {}, mean FPS: {:.2}", analysis.is_variable_frame_rate, analysis.mean_frames_per_second);
Packet Inspection
use unbundle::MediaFile;
let mut unbundler = MediaFile::open("input.mp4")?;
for packet in unbundler.packet_iter()? {
let packet = packet?;
println!("stream={} pts={:?} size={} key={}",
packet.stream_index, packet.pts, packet.size, packet.is_keyframe);
}
API Documentation
Complete API documentation is available at docs.rs/unbundle.
Essential Types
| Type | Description |
|---|---|
MediaFile |
Main entry point — opens a media file and provides access to media handles |
VideoHandle |
Extracts video frames as DynamicImage |
AudioHandle |
Extracts audio tracks as bytes or files |
SubtitleHandle |
Extracts text-based subtitle tracks |
FrameRange |
Specifies which frames to extract (range, interval, timestamps, etc.) |
ExtractOptions |
Configure threading, progress callbacks, cancellation, pixel format, resolution, hardware acceleration |
Stream & Iteration Types
| Type | Description |
|---|---|
FrameIterator |
Lazy, pull-based frame iterator |
AudioIterator |
Lazy pull-based audio sample iterator (mono f32) |
AudioChunk |
A chunk of decoded audio samples with timing |
PacketIterator |
Lazy raw-packet-level demuxer iterator |
FrameStream |
Async stream of decoded frames via Tokio (feature: async) |
AudioFuture |
Async audio extraction future (feature: async) |
Configuration Types
| Type | Description |
|---|---|
FrameOutputOptions |
Pixel format and resolution settings for frame output |
PixelFormat |
Output pixel format (RGB8, RGBA8, GRAY8) |
AudioFormat |
Output audio format (WAV, MP3, FLAC, AAC) |
SubtitleFormat |
Output subtitle format (SRT, WebVTT, Raw) |
ThumbnailOptions |
Grid thumbnail options (columns, rows, width) |
GifOptions |
Animated GIF export configuration (width, delay, repeat) (feature: gif) |
WaveformOptions |
Waveform generation settings (bin count, time range) (feature: waveform) |
SceneDetectionOptions |
Scene detection threshold configuration (feature: scene) |
VideoEncoderOptions |
Video encoder settings (FPS, resolution, codec, CRF) (feature: encode) |
HardwareAccelerationMode |
Hardware acceleration mode selection (feature: hardware) |
Metadata Types
| Type | Description |
|---|---|
MediaMetadata |
Container-level metadata (duration, format) |
VideoMetadata |
Video stream metadata (dimensions, frame rate, codec) |
AudioMetadata |
Audio stream metadata (sample rate, channels, codec) |
SubtitleMetadata |
Subtitle stream metadata (codec, language) |
ChapterMetadata |
Chapter information (title, start/end times) |
FrameMetadata |
Per-frame decode metadata (PTS, keyframe flag, picture type) |
FrameType |
Picture type enum (I, P, B, etc.) |
KeyFrameMetadata |
Keyframe position metadata (packet number, PTS, timestamp) |
GroupOfPicturesInfo |
Group of Pictures structure analysis result (keyframes, sizes, statistics) |
VariableFrameRateAnalysis |
Variable frame rate detection result (min/max/mean FPS) |
PacketInfo |
Per-packet metadata (stream index, PTS, DTS, size, keyframe) |
Utility Types
| Type | Description |
|---|---|
MediaProbe |
Lightweight stateless media file probing |
ThumbnailHandle |
Thumbnail generation helpers (single, grid, smart) |
Remuxer |
Lossless container format conversion |
ValidationReport |
Result of media file validation |
ProgressCallback |
Trait for receiving progress updates |
ProgressInfo |
Progress event data (current, total, percentage, ETA) |
CancellationToken |
Cooperative cancellation via Arc<AtomicBool> |
OperationType |
Identifies the operation being tracked |
UnbundleError |
Error type with rich context |
SubtitleEvent |
A single decoded subtitle event (text, start/end time) |
BitmapSubtitleEvent |
A bitmap subtitle event with image and timing |
Feature-Specific Types
| Type | Feature | Description |
|---|---|---|
Transcoder |
transcode |
Audio re-encoding builder (format, range, bitrate) |
VideoEncoder |
encode |
Encodes image sequences into video files |
VideoCodec |
encode |
Supported video codecs (H.264, H.265, MPEG-4) |
WaveformData |
waveform |
Generated waveform result with per-bin statistics |
WaveformBin |
waveform |
Single waveform bin (min, max, RMS amplitude) |
LoudnessInfo |
loudness |
Peak/RMS loudness with dBFS equivalents |
SceneChange |
scene |
Detected scene change with timestamp and score |
HardwareDeviceType |
hardware |
Supported hardware device types (CUDA, VAAPI, etc.) |
Performance
unbundle is designed for efficiency in both single-file and batch processing scenarios:
- Smart seeking — Uses FFmpeg's keyframe-based seeking. For sequential access (ranges, intervals), frames are decoded without redundant seeks.
- Lightweight decoders — Each extraction call creates a fresh decoder. FFmpeg decoder creation is fast relative to actual decoding work.
- Batch optimization —
FrameRange::Specificsorts requested frame numbers and processes them in order to minimize seeks. - Memory-efficient streaming —
for_each_frameandFrameIteratorprocess frames one at a time without buffering entire frame sets. - Parallel extraction —
frames_parallel()(featurerayon) splits frames across rayon threads, each with its own demuxer for true parallelism. - Hardware acceleration — When enabled (feature
hardware), attempts GPU-accelerated decoding with automatic fallback to software. - Correct stride handling — Properly handles FFmpeg's row padding when converting frames to
imagebuffers. - Zero-copy audio — Uses
avio_open_dyn_buffor in-memory audio encoding without temporary files.
Testing
Generate test fixtures first:
# Linux / macOS
bash tests/fixtures/generate_fixtures.sh
# Windows
tests\fixtures\generate_fixtures.bat
Then run tests:
cargo test --all-features
Test Coverage
The test suite includes comprehensive coverage:
| Test Module | Coverage |
|---|---|
video |
Single frames, ranges, intervals, timestamps, specific lists, pixel formats, resolution scaling |
audio |
WAV/MP3/FLAC/AAC extraction, ranges, file output, multi-track |
subtitle |
Subtitle decoding, SRT/WebVTT export, multi-track |
metadata |
Container metadata, video/audio/subtitle stream properties |
configuration |
ExtractOptions builder, pixel formats, resolution, cancellation |
progress |
ProgressCallback, ProgressInfo fields, CancellationToken |
error_handling |
Error variants, context, invalid inputs, missing streams |
video_iterator |
FrameIterator, lazy iteration, early exit |
conversion |
Remuxer, stream exclusion, lossless format conversion |
validation |
ValidationReport, warnings, errors, valid files |
chapters |
Chapter metadata extraction, titles, timestamps, ordering |
frame_metadata |
FrameMetadata, FrameType, keyframe detection, PTS values |
segmented_extraction |
FrameRange::Segments, multiple disjoint time ranges |
probing |
MediaProbe, probe/probe_many, error handling |
thumbnail |
ThumbnailHandle, grid, smart selection, aspect ratio |
audio_iterator |
AudioIterator, chunk iteration, sample rates |
keyframe |
GroupOfPicturesInfo, KeyFrameMetadata, Group of Pictures statistics |
variable_framerate |
VariableFrameRateAnalysis, constant vs variable frame rate |
packet_iterator |
PacketIterator, PacketInfo, stream filtering |
subtitle_search |
Subtitle search, case-insensitive matching |
metadata_extended |
Extended metadata: video tracks, colorspace, HDR |
Feature-specific tests (require corresponding features enabled):
| Test Module | Feature Required | Coverage |
|---|---|---|
scene |
scene |
Scene change detection, threshold configuration |
async_extraction |
async |
FrameStream, AudioFuture, async streaming |
rayon |
rayon |
frames_parallel, sequential parity, interval mode |
hardware_acceleration |
hardware |
Hardware device enumeration, Auto/Software modes |
gif_export |
gif |
GIF encoding, file and in-memory output |
waveform |
waveform |
WaveformOptions, bin statistics, time ranges |
loudness |
loudness |
Peak/RMS loudness, dBFS values |
video_encoder |
encode |
VideoEncoder, codec selection, frame encoding |
transcode |
transcode |
Transcoder, format conversion, time ranges |
Benchmarks
Run performance benchmarks:
cargo bench --all-features
Criterion benchmarks are located in benches/ and measure:
- Frame extraction throughput (single vs parallel)
- Seek performance (sequential vs random access)
- Audio extraction speed across formats
- Iterator overhead vs batch extraction
Troubleshooting
FFmpeg Linking Errors
Problem: error: linking with 'cc' failed or cannot find -lavcodec
Solution:
- Ensure FFmpeg development libraries are installed (see Installation)
- Set
PKG_CONFIG_PATHto point to FFmpeg's.pcfiles - On Windows, set
FFMPEG_DIRenvironment variable - Verify with:
pkg-config --libs --cflags libavcodec
Codec Not Supported
Problem: UnbundleError::UnsupportedAudioFormat
Solution:
- Check that your FFmpeg build includes the required codec
- Run
ffmpeg -codecsto list available codecs - Some codecs require FFmpeg to be built with specific flags (e.g.,
--enable-libx264)
Hardware Acceleration Fails
Problem: Hardware decoding falls back to software or fails entirely
Solution:
- Verify GPU drivers are up to date
- Check available hardware devices:
unbundle::hardware_acceleration::available_hardware_devices() - Use
ExtractOptions::with_hardware_acceleration(HardwareAccelerationMode::Auto)for automatic fallback - Not all codecs/formats support hardware acceleration
- Try
ffmpeg -hwaccelsto list available hardware acceleration methods
Out of Memory
Problem: High memory usage when extracting many frames
Solution:
- Use streaming iteration instead of batch extraction:
frame_iter()orfor_each_frame() - Process frames in smaller batches
- Use
AudioIteratorfor large audio files instead of loading entire tracks
Slow Frame Extraction
Problem: Frame extraction is slower than expected
Solution:
- Use
frames_parallel()(featurerayon) for CPU-bound workloads - Enable hardware acceleration (feature
hardware) for high-resolution video - Avoid extracting specific frames in random order — sorted access is much faster
- Consider using
FrameRange::Intervalinstead of many individual frame numbers
Permission Denied / File Not Found
Problem: Cannot open media file
Solution:
- Verify file path is correct and file exists
- Check file permissions (readable by current user)
- Ensure file is not locked by another process
- On Windows, use raw string literals for paths:
r"C:\path\to\video.mp4"
Contributing
Contributions are welcome! Please see the GitHub repository for:
- Bug reports and feature requests
- Pull requests
- Discussions and questions
License
MIT — see LICENSE file for details.
Dependencies
~13–19MB
~365K SLoC