unbundle 5.2.0

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

use std::path::Path;

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

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

#[test]
fn frame_and_metadata_returns_image_and_metadata() {
    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 (image, info) = unbundler
        .video()
        .frame_and_metadata(0)
        .expect("Failed to extract frame with info");

    assert!(image.width() > 0);
    assert_eq!(info.frame_number, 0);
}

#[test]
fn frame_and_metadata_first_frame_is_keyframe() {
    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 (_image, info) = unbundler
        .video()
        .frame_and_metadata(0)
        .expect("Failed to extract frame 0");

    assert!(info.is_keyframe, "First frame should be a keyframe");
    assert_eq!(
        info.frame_type,
        FrameType::I,
        "First frame should be an I-frame"
    );
}

#[test]
fn frame_and_metadata_has_timestamp() {
    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 (_image, info) = unbundler
        .video()
        .frame_and_metadata(30)
        .expect("Failed to extract frame 30");

    // Frame 30 at 30 fps should be approximately 1 second.
    let timestamp_secs = info.timestamp.as_secs_f64();
    assert!(
        (timestamp_secs - 1.0).abs() < 0.1,
        "Expected ~1.0s timestamp, got {timestamp_secs}"
    );
}

#[test]
fn frame_and_metadata_has_pts() {
    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 (_image, info) = unbundler
        .video()
        .frame_and_metadata(0)
        .expect("Failed to extract frame 0");

    // The first frame should have a PTS.
    assert!(info.pts.is_some(), "Expected PTS to be present for frame 0");
}

#[test]
fn frames_and_metadata_returns_correct_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 results = unbundler
        .video()
        .frames_and_metadata(FrameRange::Range(0, 4))
        .expect("Failed to extract frames with info");

    assert_eq!(results.len(), 5, "Expected 5 frames (0..=4)");
    for (index, (_image, info)) in results.iter().enumerate() {
        assert_eq!(
            info.frame_number, index as u64,
            "Frame number should match index"
        );
    }
}

#[test]
fn frame_type_debug_display() {
    // FrameType should derive Debug
    let frame_type = FrameType::I;
    let debug = format!("{frame_type:?}");
    assert_eq!(debug, "I");
}

#[test]
fn frame_and_metadata_out_of_range() {
    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 frame_count = unbundler.metadata().video.as_ref().unwrap().frame_count;
    let result = unbundler.video().frame_and_metadata(frame_count + 100);
    assert!(result.is_err(), "Should error on out-of-range frame");
}