unbundle 5.2.0

Unbundle media files - extract still frames, audio tracks, and subtitles from video files
Documentation
//! Parallel frame extraction integration tests (feature = "rayon").
//!
//! These tests require the `rayon` feature and test fixtures
//! generated by `tests/fixtures/generate_fixtures.sh`.

#![cfg(feature = "rayon")]

use std::path::Path;

use image::DynamicImage;
use unbundle::{ExtractOptions, FrameRange, MediaFile};

const SAMPLE_VIDEO: &str = "tests/fixtures/sample_video.mp4";

fn skip_unless(path: &str) -> bool {
    if !Path::new(path).exists() {
        eprintln!("Skipping: fixture {path} not found");
        return true;
    }
    false
}

#[test]
fn parallel_basic_consecutive() {
    if skip_unless(SAMPLE_VIDEO) {
        return;
    }

    let mut unbundler = MediaFile::open(SAMPLE_VIDEO).unwrap();
    let config = ExtractOptions::new();
    let frames = unbundler
        .video()
        .frames_parallel(FrameRange::Range(0, 9), &config)
        .unwrap();

    assert_eq!(frames.len(), 10);

    for frame in &frames {
        let frame: &DynamicImage = frame;
        assert!(frame.width() > 0);
        assert!(frame.height() > 0);
    }
}

#[test]
fn parallel_with_large_gaps() {
    if skip_unless(SAMPLE_VIDEO) {
        return;
    }

    let mut unbundler = MediaFile::open(SAMPLE_VIDEO).unwrap();
    let config = ExtractOptions::new();
    let frames = unbundler
        .video()
        .frames_parallel(FrameRange::Specific(vec![0, 1, 50, 51, 100, 101]), &config)
        .unwrap();

    assert_eq!(frames.len(), 6, "should extract all 6 requested frames");
}

#[test]
fn parallel_matches_sequential() {
    if skip_unless(SAMPLE_VIDEO) {
        return;
    }

    let target = vec![10u64, 11, 12];

    let mut unbundler = MediaFile::open(SAMPLE_VIDEO).unwrap();
    let parallel = unbundler
        .video()
        .frames_parallel(FrameRange::Specific(target.clone()), &ExtractOptions::new())
        .unwrap();

    let mut unbundler2 = MediaFile::open(SAMPLE_VIDEO).unwrap();
    let sequential = unbundler2
        .video()
        .frames(FrameRange::Specific(target))
        .unwrap();

    assert_eq!(parallel.len(), sequential.len());

    // Pixel-level comparison of the first frame.
    let p_img = parallel[0].to_rgb8();
    let s_img = sequential[0].to_rgb8();
    assert_eq!(p_img.dimensions(), s_img.dimensions());
    assert_eq!(
        p_img.as_raw(),
        s_img.as_raw(),
        "parallel and sequential output should be pixel-identical",
    );
}

#[test]
fn parallel_interval() {
    if skip_unless(SAMPLE_VIDEO) {
        return;
    }

    let mut unbundler = MediaFile::open(SAMPLE_VIDEO).unwrap();
    let config = ExtractOptions::new();
    let frames = unbundler
        .video()
        .frames_parallel(FrameRange::Interval(30), &config)
        .unwrap();

    // With a 30-fps, 5-second fixture we expect ~5 frames at interval=30.
    assert!(
        !frames.is_empty(),
        "interval extraction should return at least one frame",
    );
}

#[test]
fn parallel_from_open_url_source_input() {
    if skip_unless(SAMPLE_VIDEO) {
        return;
    }

    let mut unbundler = MediaFile::open_url(SAMPLE_VIDEO).unwrap();
    let frames = unbundler
        .video()
        .frames_parallel(
            FrameRange::Specific(vec![0, 15, 30]),
            &ExtractOptions::new(),
        )
        .unwrap();

    assert_eq!(frames.len(), 3);
    for frame in &frames {
        assert!(frame.width() > 0);
        assert!(frame.height() > 0);
    }
}