#![cfg(feature = "scene")]
use std::path::Path;
use unbundle::{MediaFile, SceneDetectionMode, SceneDetectionOptions};
const SAMPLE_VIDEO: &str = "tests/fixtures/sample_video.mp4";
const SAMPLE_MKV: &str = "tests/fixtures/sample_video.mkv";
const AUDIO_ONLY: &str = "tests/fixtures/audio_only.wav";
fn skip_unless(path: &str) -> bool {
if !Path::new(path).exists() {
eprintln!("Skipping: fixture {path} not found");
return true;
}
false
}
#[test]
fn detect_scenes_returns_results() {
if skip_unless(SAMPLE_VIDEO) {
return;
}
let mut unbundler = MediaFile::open(SAMPLE_VIDEO).unwrap();
let scenes = unbundler.video().detect_scenes(None).unwrap();
let _ = scenes;
}
#[test]
fn detect_scenes_with_default_config() {
if skip_unless(SAMPLE_VIDEO) {
return;
}
let mut unbundler = MediaFile::open(SAMPLE_VIDEO).unwrap();
let config = SceneDetectionOptions::default();
assert!(
(config.threshold - 10.0).abs() < f64::EPSILON,
"default threshold should be 10.0",
);
assert_eq!(config.mode, SceneDetectionMode::Auto);
assert!(config.max_duration.is_none());
assert!(config.max_scene_changes.is_none());
let scenes = unbundler.video().detect_scenes(Some(config)).unwrap();
let _ = scenes;
}
#[test]
fn detect_scenes_with_low_threshold() {
if skip_unless(SAMPLE_VIDEO) {
return;
}
let mut unbundler = MediaFile::open(SAMPLE_VIDEO).unwrap();
let low = SceneDetectionOptions {
threshold: 1.0,
..SceneDetectionOptions::default()
};
let scenes_low = unbundler.video().detect_scenes(Some(low)).unwrap();
let mut unbundler2 = MediaFile::open(SAMPLE_VIDEO).unwrap();
let high = SceneDetectionOptions {
threshold: 99.0,
..SceneDetectionOptions::default()
};
let scenes_high = unbundler2.video().detect_scenes(Some(high)).unwrap();
assert!(
scenes_low.len() >= scenes_high.len(),
"lower threshold ({}) should find >= scenes than higher ({})",
scenes_low.len(),
scenes_high.len(),
);
}
#[test]
fn detect_scenes_scene_change_fields() {
if skip_unless(SAMPLE_VIDEO) {
return;
}
let mut unbundler = MediaFile::open(SAMPLE_VIDEO).unwrap();
let scenes = unbundler
.video()
.detect_scenes(Some(SceneDetectionOptions {
threshold: 1.0,
..SceneDetectionOptions::default()
}))
.unwrap();
for scene in &scenes {
assert!(scene.score > 0.0, "scene score should be positive");
assert!(
scene.timestamp.as_secs_f64() >= 0.0,
"timestamp should be non-negative",
);
}
}
#[test]
fn detect_scenes_no_video_stream_error() {
if skip_unless(AUDIO_ONLY) {
return;
}
let mut unbundler = MediaFile::open(AUDIO_ONLY).unwrap();
let result = unbundler.video().detect_scenes(None);
assert!(result.is_err(), "should error on audio-only file");
let err = format!("{}", result.unwrap_err());
assert!(
err.contains("video") || err.contains("Video"),
"error should mention video stream: {err}",
);
}
#[test]
fn detect_scenes_with_cancellation() {
if skip_unless(SAMPLE_VIDEO) {
return;
}
use unbundle::{CancellationToken, ExtractOptions};
let mut unbundler = MediaFile::open(SAMPLE_VIDEO).unwrap();
let token = CancellationToken::new();
token.cancel();
let config = ExtractOptions::new().with_cancellation(token);
let result = unbundler.video().detect_scenes_with_options(None, &config);
assert!(result.is_err(), "cancelled detection should error");
let err = format!("{}", result.unwrap_err());
assert!(
err.contains("ancelled") || err.contains("cancel"),
"error should indicate cancellation: {err}",
);
}
#[test]
fn detect_scenes_mkv_format() {
if skip_unless(SAMPLE_MKV) {
return;
}
let mut unbundler = MediaFile::open(SAMPLE_MKV).unwrap();
let scenes = unbundler.video().detect_scenes(None).unwrap();
let _ = scenes;
}
#[test]
fn scene_config_debug() {
let config = SceneDetectionOptions {
threshold: 42.0,
..SceneDetectionOptions::default()
};
let debug = format!("{:?}", config);
assert!(debug.contains("42"), "Debug should show threshold: {debug}");
}
#[test]
fn detect_scenes_with_max_scene_changes_limit() {
if skip_unless(SAMPLE_VIDEO) {
return;
}
let mut unbundler = MediaFile::open(SAMPLE_VIDEO).unwrap();
let config = SceneDetectionOptions::new()
.threshold(1.0)
.max_scene_changes(1);
let scenes = unbundler.video().detect_scenes(Some(config)).unwrap();
assert!(scenes.len() <= 1, "should stop at configured scene limit");
}
#[test]
fn detect_scenes_with_max_duration_limit() {
if skip_unless(SAMPLE_VIDEO) {
return;
}
let mut unbundler = MediaFile::open(SAMPLE_VIDEO).unwrap();
let config = SceneDetectionOptions::new().max_duration(std::time::Duration::from_secs(1));
let scenes = unbundler.video().detect_scenes(Some(config)).unwrap();
for scene in scenes {
assert!(
scene.timestamp <= std::time::Duration::from_secs(1),
"scene timestamp should be within configured max duration",
);
}
}
#[test]
fn detect_scenes_keyframe_mode_runs() {
if skip_unless(SAMPLE_VIDEO) {
return;
}
let mut unbundler = MediaFile::open(SAMPLE_VIDEO).unwrap();
let config = SceneDetectionOptions::new()
.mode(SceneDetectionMode::Keyframes)
.max_scene_changes(10);
let scenes = unbundler.video().detect_scenes(Some(config)).unwrap();
assert!(scenes.len() <= 10);
}
#[test]
fn scene_config_with_aliases_builds() {
let config = SceneDetectionOptions::new()
.with_threshold(5.0)
.with_mode(SceneDetectionMode::Full)
.with_max_duration(std::time::Duration::from_secs(2))
.with_max_scene_changes(3);
assert!((config.threshold - 5.0).abs() < f64::EPSILON);
assert_eq!(config.mode, SceneDetectionMode::Full);
assert_eq!(config.max_duration, Some(std::time::Duration::from_secs(2)));
assert_eq!(config.max_scene_changes, Some(3));
}
#[test]
fn scene_change_debug_and_clone() {
use unbundle::SceneChange;
let sc = SceneChange {
timestamp: std::time::Duration::from_millis(1500),
frame_number: 45,
score: 85.3,
};
let cloned = sc.clone();
assert_eq!(cloned.frame_number, 45);
assert!((cloned.score - 85.3).abs() < f64::EPSILON);
let debug = format!("{:?}", sc);
assert!(
debug.contains("45"),
"Debug should show frame number: {debug}"
);
}