unbundle 5.2.0

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

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

use std::path::Path;

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

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

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

#[tokio::test]
async fn stream_first_five_frames() {
    if skip_unless(SAMPLE_VIDEO) {
        return;
    }

    let mut unbundler = MediaFile::open(SAMPLE_VIDEO).unwrap();
    let mut stream: FrameStream = unbundler
        .video()
        .frame_stream(
            FrameRange::Specific(vec![0, 1, 2, 3, 4]),
            ExtractOptions::new(),
        )
        .unwrap();

    let mut count = 0u64;
    while let Some(result) = stream.next().await {
        let (frame_number, image): (u64, DynamicImage) = result.unwrap();
        assert!(image.width() > 0);
        assert!(image.height() > 0);
        assert!(frame_number <= 4, "unexpected frame number {frame_number}");
        count += 1;
    }

    assert_eq!(count, 5, "expected 5 frames from stream");
}

#[tokio::test]
async fn stream_non_consecutive_frames() {
    if skip_unless(SAMPLE_VIDEO) {
        return;
    }

    let mut unbundler = MediaFile::open(SAMPLE_VIDEO).unwrap();
    let mut stream = unbundler
        .video()
        .frame_stream(FrameRange::Specific(vec![0, 30, 60]), ExtractOptions::new())
        .unwrap();

    let mut frames = Vec::new();
    while let Some(result) = stream.next().await {
        let (_, image): (u64, DynamicImage) = result.unwrap();
        frames.push(image);
    }

    assert_eq!(frames.len(), 3);
}

#[tokio::test]
async fn stream_range() {
    if skip_unless(SAMPLE_VIDEO) {
        return;
    }

    let mut unbundler = MediaFile::open(SAMPLE_VIDEO).unwrap();
    let mut stream = unbundler
        .video()
        .frame_stream(FrameRange::Range(10, 14), ExtractOptions::new())
        .unwrap();

    let mut count = 0u64;
    while let Some(result) = stream.next().await {
        let _ = result.unwrap();
        count += 1;
    }

    assert_eq!(count, 5, "Range(10, 14) should yield 5 frames");
}

#[tokio::test]
async fn stream_no_video_stream_error() {
    if skip_unless(AUDIO_ONLY) {
        return;
    }

    let mut unbundler = MediaFile::open(AUDIO_ONLY).unwrap();
    let result = unbundler
        .video()
        .frame_stream(FrameRange::Range(0, 0), ExtractOptions::new());

    assert!(result.is_err(), "should error on audio-only file");
}

#[tokio::test]
async fn audio_future_extracts_wav() {
    if skip_unless(SAMPLE_VIDEO) {
        return;
    }

    let mut unbundler = MediaFile::open(SAMPLE_VIDEO).unwrap();
    let audio_bytes = unbundler
        .audio()
        .extract_async(unbundle::AudioFormat::Wav, ExtractOptions::new())
        .unwrap()
        .await
        .unwrap();

    assert!(!audio_bytes.is_empty(), "expected non-empty WAV data");
    assert_eq!(
        &audio_bytes[..4],
        b"RIFF",
        "WAV output should start with RIFF header",
    );
}

#[tokio::test]
async fn stream_from_open_url_source_input() {
    if skip_unless(SAMPLE_VIDEO) {
        return;
    }

    let mut unbundler = MediaFile::open_url(SAMPLE_VIDEO).unwrap();
    let mut stream = unbundler
        .video()
        .frame_stream(FrameRange::Range(0, 2), ExtractOptions::new())
        .unwrap();

    let mut count = 0u64;
    while let Some(result) = stream.next().await {
        let (_frame_number, image): (u64, DynamicImage) = result.unwrap();
        assert!(image.width() > 0);
        assert!(image.height() > 0);
        count += 1;
    }

    assert_eq!(count, 3, "expected three frames from URL-opened source");
}

#[tokio::test]
async fn audio_future_from_open_url_source_input() {
    if skip_unless(SAMPLE_VIDEO) {
        return;
    }

    let mut unbundler = MediaFile::open_url(SAMPLE_VIDEO).unwrap();
    let audio_bytes = unbundler
        .audio()
        .extract_async(unbundle::AudioFormat::Wav, ExtractOptions::new())
        .unwrap()
        .await
        .unwrap();

    assert!(!audio_bytes.is_empty(), "expected non-empty WAV data");
    assert_eq!(&audio_bytes[..4], b"RIFF");
}