unbundle 5.2.0

Unbundle media files - extract still frames, audio tracks, and subtitles from video files
Documentation
//! Segmented extraction integration tests.
//!
//! These tests require the fixture files generated by
//! `tests/fixtures/generate_fixtures.sh` (or `.bat` on Windows).

use std::path::Path;
use std::time::Duration;

use unbundle::{FrameRange, MediaFile, UnbundleError};

fn sample_video_path() -> &'static str {
    "tests/fixtures/sample_video.mp4"
}

#[test]
fn segments_extracts_from_multiple_ranges() {
    let path = sample_video_path();
    if !Path::new(path).exists() {
        eprintln!("Skipping: fixture '{path}' not found.");
        return;
    }

    let mut unbundler = MediaFile::open(path).expect("Failed to open");
    let frames = unbundler
        .video()
        .frames(FrameRange::Segments(vec![
            (Duration::from_secs(0), Duration::from_millis(500)),
            (Duration::from_secs(2), Duration::from_millis(2500)),
        ]))
        .expect("Failed to extract segments");

    // Both segments should produce frames.
    assert!(!frames.is_empty(), "Expected frames from segments");
}

#[test]
fn segments_invalid_range_returns_error() {
    let path = sample_video_path();
    if !Path::new(path).exists() {
        eprintln!("Skipping: fixture '{path}' not found.");
        return;
    }

    let mut unbundler = MediaFile::open(path).expect("Failed to open");
    let result = unbundler.video().frames(FrameRange::Segments(vec![
        // Invalid: start > end
        (Duration::from_secs(5), Duration::from_secs(2)),
    ]));
    assert!(result.is_err(), "Should error on invalid segment");
    match result.unwrap_err() {
        UnbundleError::InvalidRange { .. } => {}
        other => panic!("Expected InvalidRange, got: {other:?}"),
    }
}

#[test]
fn segments_empty_produces_no_frames() {
    let path = sample_video_path();
    if !Path::new(path).exists() {
        eprintln!("Skipping: fixture '{path}' not found.");
        return;
    }

    let mut unbundler = MediaFile::open(path).expect("Failed to open");
    let frames = unbundler
        .video()
        .frames(FrameRange::Segments(vec![]))
        .expect("Empty segments should succeed");

    assert!(frames.is_empty(), "Expected 0 frames from empty segments");
}

#[test]
fn segments_combined_frame_count() {
    let path = sample_video_path();
    if !Path::new(path).exists() {
        eprintln!("Skipping: fixture '{path}' not found.");
        return;
    }

    let mut unbundler = MediaFile::open(path).expect("Failed to open");
    let fps = unbundler
        .metadata()
        .video
        .as_ref()
        .unwrap()
        .frames_per_second;

    // Each 500ms segment at 30fps should produce ~15 frames.
    let seg1_frames = unbundler
        .video()
        .frames(FrameRange::TimeRange(
            Duration::from_secs(0),
            Duration::from_millis(500),
        ))
        .expect("Failed TimeRange")
        .len();

    let seg2_frames = unbundler
        .video()
        .frames(FrameRange::TimeRange(
            Duration::from_secs(2),
            Duration::from_millis(2500),
        ))
        .expect("Failed TimeRange")
        .len();

    let combined_frames = unbundler
        .video()
        .frames(FrameRange::Segments(vec![
            (Duration::from_secs(0), Duration::from_millis(500)),
            (Duration::from_secs(2), Duration::from_millis(2500)),
        ]))
        .expect("Failed Segments")
        .len();

    // Segments should produce approximately the sum of individual ranges
    // (may be slightly less due to dedup of overlapping frame numbers).
    assert!(
        combined_frames >= seg1_frames.min(seg2_frames),
        "Combined ({combined_frames}) should be >= min of individual ({seg1_frames}, {seg2_frames}), fps={fps}"
    );
}