unbundle 4.0.6

Unbundle media files - extract still frames, audio tracks, and subtitles from video files
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
# unbundle


[![Crates.io](https://img.shields.io/crates/v/unbundle)](https://crates.io/crates/unbundle)
[![docs.rs](https://img.shields.io/docsrs/unbundle)](https://docs.rs/unbundle)
[![License: MIT](https://img.shields.io/crates/l/unbundle)](LICENSE)

A clean, ergonomic Rust library for extracting video frames, audio tracks, and subtitles from media files using FFmpeg.

```rust
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`]https://docs.rs/image/latest/image/enum.DynamicImage.html, 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

## Installation


Add to your `Cargo.toml`:

```toml
[dependencies]
unbundle = "4.0"
```

Or with additional features:

```toml
[dependencies]
unbundle = { version = "4.0", features = ["async", "rayon", "hardware"] }
```

### System Requirements


`unbundle` requires FFmpeg libraries (4.0+) installed on your system.

**Linux (Debian/Ubuntu):**

```bash
sudo apt-get install libavcodec-dev libavformat-dev libavutil-dev \
    libswscale-dev libswresample-dev libavdevice-dev pkg-config
```

**macOS:**

```bash
brew install ffmpeg pkg-config
```

**Windows:**

Download FFmpeg development builds from <https://ffmpeg.org/download.html> or use vcpkg:

```powershell
vcpkg install ffmpeg:x64-windows
```

Set `FFMPEG_DIR` environment variable if FFmpeg is not in your PATH.

## Quick Start


### Extract Video Frames


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


```rust
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])
)?;
```

### Streaming Frame Iteration


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


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


```rust
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)?;
```

### Progress & Cancellation


```rust
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
- **Progress & cancellation** — cooperative progress callbacks and `CancellationToken` for long-running operations
- **Streaming iteration** — lazy `FrameIterator` (pull-based) and `for_each_frame` (push-based) without buffering entire frame sets
- **Audio sample iteration** — lazy `AudioIterator` yields 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 `MediaProbe` for 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
- **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 |

```toml
[dependencies]
unbundle = { version = "3.0", features = ["full"] }
```

#### Feature Usage Guide


- **Use `async`** when integrating with async web servers or when processing multiple videos concurrently
- **Use `rayon`** for CPU-intensive batch frame extraction (e.g., generating thousands of thumbnails)
- **Use `hardware`** when processing high-resolution video (4K+) or when CPU is a bottleneck
- **Use `scene`** for video analysis, automatic chapter detection, or intelligent thumbnail selection
- **Use `gif`** for creating preview animations or social media content
- **Use `waveform` and `loudness`** for audio visualization or normalization workflows
- **Use `transcode`** for audio format conversion in media pipelines
- **Use `encode`** for creating time-lapses, slideshows, or re-encoding frame sequences

## Examples


The [`examples/`](https://github.com/skanderjeddi/unbundle/tree/main/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 |
| `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:

```bash
cargo run --example metadata -- path/to/video.mp4
```

## Advanced Usage


### Container Remuxing


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


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


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


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


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


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


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


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


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


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


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


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


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


```rust
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)
    .fps(24)
    .codec(VideoCodec::H264);
VideoEncoder::new(config).write("output.mp4", &frames)?;
```

### Keyframe & Group of Pictures Analysis


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


```rust
use unbundle::MediaFile;

let mut unbundler = MediaFile::open("input.mp4")?;
let vfr = unbundler.video().analyze_variable_framerate()?;
println!("VFR: {}, mean FPS: {:.2}", vfr.is_vfr, vfr.mean_fps);
```

### Packet Inspection


```rust
use unbundle::MediaFile;

let mut unbundler = MediaFile::open("input.mp4")?;
for pkt in unbundler.packet_iter()? {
    let pkt = pkt?;
    println!("stream={} pts={:?} size={} key={}",
        pkt.stream_index, pkt.pts, pkt.size, pkt.is_keyframe);
}
```

## API Documentation


Complete API documentation is available at [docs.rs/unbundle](https://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::Specific` sorts requested frame numbers and processes them in order to minimize seeks.
- **Memory-efficient streaming**`for_each_frame` and `FrameIterator` process frames one at a time without buffering entire frame sets.
- **Parallel extraction**`frames_parallel()` (feature `rayon`) 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 `image` buffers.
- **Zero-copy audio** — Uses `avio_open_dyn_buf` for in-memory audio encoding without temporary files.

## Testing


Generate test fixtures first:

```bash
# Linux / macOS

bash tests/fixtures/generate_fixtures.sh

# Windows

tests\fixtures\generate_fixtures.bat
```

Then run tests:

```bash
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:

```bash
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_PATH` to point to FFmpeg's `.pc` files
- On Windows, set `FFMPEG_DIR` environment 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 -codecs` to 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 -hwaccels` to 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()` or `for_each_frame()`
- Process frames in smaller batches
- Use `AudioIterator` for large audio files instead of loading entire tracks

### Slow Frame Extraction


**Problem:** Frame extraction is slower than expected

**Solution:**
- Use `frames_parallel()` (feature `rayon`) 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::Interval` instead 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](https://github.com/skanderjeddi/unbundle) for:
- Bug reports and feature requests
- Pull requests
- Discussions and questions

## License


MIT — see [LICENSE](LICENSE) file for details.