diff --git a/crates/codecs/src/mp4.rs b/crates/codecs/src/mp4.rs index 8b8a249f..3be17c58 100644 --- a/crates/codecs/src/mp4.rs +++ b/crates/codecs/src/mp4.rs @@ -552,4 +552,21 @@ mod tests { let s = Mp4Source::open("180.mov", rotated_mov([-ONE, 0, 0, 0, -ONE, 0, 64 * ONE, 32 * ONE, W])).expect("open"); assert_eq!(s.info().video.as_ref().map(|v| (v.width, v.height)), Some((64, 32))); } + + #[test] + fn mirrored_or_scaled_matrices_are_not_mistaken_for_rotations() { + const ONE: i32 = 0x10000; + const W: i32 = 0x4000_0000; + // horizontal mirror: not a pure rotation, so the frame is left as stored + let s = Mp4Source::open("mirror.mov", rotated_mov([-ONE, 0, 0, 0, ONE, 0, 64 * ONE, 0, W])).expect("open"); + let (w, h, top, bottom) = halves(&s); + assert_eq!((w, h), (64, 32)); + assert!(top.abs_diff(bottom) < 4); + // a 2× scaled identity is still upright (only the signs decide the rotation) + let s = Mp4Source::open("scaled.mov", rotated_mov([2 * ONE, 0, 0, 0, 2 * ONE, 0, 0, 0, W])).expect("open"); + assert_eq!(s.info().video.as_ref().map(|v| (v.width, v.height)), Some((64, 32))); + // a scaled quarter turn is still a quarter turn + let s = Mp4Source::open("cw2.mov", rotated_mov([0, 2 * ONE, 0, -2 * ONE, 0, 0, 64 * ONE, 0, W])).expect("open"); + assert_eq!(s.info().video.as_ref().map(|v| (v.width, v.height)), Some((32, 64))); + } } diff --git a/crates/matroska/src/track.rs b/crates/matroska/src/track.rs index 2ad1d52b..af8af356 100644 --- a/crates/matroska/src/track.rs +++ b/crates/matroska/src/track.rs @@ -318,3 +318,30 @@ impl Track { } } } + +#[cfg(test)] +mod rotation_tests { + use super::{Projection, VideoInfo}; + + fn with_roll(projection_type: u64, yaw: f64, pitch: f64, roll: f64) -> VideoInfo { + VideoInfo { projection: Some(Projection { projection_type, yaw, pitch, roll }), ..Default::default() } + } + + #[test] + fn display_rotation_maps_rectangular_roll_to_clockwise_quarter_turns() { + assert_eq!(VideoInfo::default().display_rotation(), Some(0), "no Projection: as stored"); + // ProjectionPoseRoll is counter-clockwise: -90 is a quarter turn clockwise + for (roll, turns) in [(0.0, 0), (-90.0, 1), (180.0, 2), (-180.0, 2), (90.0, 3), (-270.0, 3), (270.0, 1), (360.0, 0)] { + assert_eq!(with_roll(0, 0.0, 0.0, roll).display_rotation(), Some(turns), "roll {roll}"); + } + } + + #[test] + fn display_rotation_leaves_non_rectangular_flipped_and_odd_angles_alone() { + assert_eq!(with_roll(1, 0.0, 0.0, -90.0).display_rotation(), None, "equirectangular"); + assert_eq!(with_roll(0, 180.0, 0.0, 0.0).display_rotation(), None, "yaw flip"); + assert_eq!(with_roll(0, 0.0, 180.0, 0.0).display_rotation(), None, "pitch flip"); + assert_eq!(with_roll(0, 0.0, 0.0, 45.0).display_rotation(), None, "not a quarter turn"); + assert_eq!(with_roll(0, 0.0, 0.0, f64::NAN).display_rotation(), None, "malformed angle"); + } +}