fix(media): accept portrait video resolutions (#6058)

## Summary
- apply the existing 2160×3840 video resolution envelope independent of
orientation
- accept portrait recordings whose short/long edges fit that envelope
- retain rejection coverage for either edge exceeding its limit
- make the relay error describe the orientation-independent limits

## Why
The iOS simulator produces portrait H.264 recordings such as 1206×2622.
The relay previously checked `width <= 3840 && height <= 2160`, so the
equivalent portrait dimensions were rejected while landscape dimensions
were accepted.

## Validation
- `bin/just ci`
- `cargo test -p buzz-media` (120 passed; one MinIO integration test
ignored by its existing live-service guard)
- `cargo clippy -p buzz-media --all-targets -- -D warnings`
- exact blocked artifact
`/Users/judeedwards/.buzz/.scratch/pr5874-mongo-evidence/native/head-20260816T050317Z/video-bitexact.mp4`
validated through `validate_video_file`: 1206×2622 accepted

Signed-off-by: Carl <5f365698229751c0461f57bb03a4e93134e6e936bd7039ebe7b737282a43c754@buzz.block.builderlab.xyz>
Co-authored-by: Carl <5f365698229751c0461f57bb03a4e93134e6e936bd7039ebe7b737282a43c754@buzz.block.builderlab.xyz>
This commit is contained in:
Jude Edwards
2026-08-19 11:01:47 -07:00
committed by GitHub
parent fe7c6808e7
commit 196d62f97c
2 changed files with 48 additions and 5 deletions

View File

@@ -72,8 +72,10 @@ pub enum MediaError {
/// Video duration exceeds the 600-second limit.
#[error("video too long: duration exceeds 600 seconds")]
DurationTooLong,
/// Video resolution exceeds 3840×2160.
#[error("video resolution too high: maximum is 3840x2160")]
/// Video resolution exceeds the 2160 short-edge / 3840 long-edge envelope.
#[error(
"video resolution too high: maximum is 2160 on the short edge and 3840 on the long edge"
)]
ResolutionTooHigh,
/// MP4 moov atom appears after mdat — not fast-start.
#[error("moov atom not at front of file (not fast-start)")]

View File

@@ -294,7 +294,7 @@ pub fn validate_content(bytes: &[u8], config: &MediaConfig) -> Result<String, Me
/// - Exactly one video track using `avc1` (H.264 only — rejects HEVC, VP9, AV1)
/// - At most one audio track, using `mp4a` (AAC)
/// - Duration ≤ 600 seconds (from mvhd timescale, not edit lists)
/// - Resolution ≤ 3840×2160
/// - Resolution: short edge ≤ 2160 and long edge ≤ 3840 (portrait or landscape)
/// - moov atom precedes mdat (fast-start / web-optimised)
///
/// Returns [`VideoMeta`] on success.
@@ -370,10 +370,14 @@ pub fn validate_video_file(path: &Path, config: &MediaConfig) -> Result<VideoMet
return Err(MediaError::DurationTooLong);
}
// Resolution check.
// Resolution check. Apply the 2160×3840 envelope independent
// of orientation so equivalent portrait and landscape videos
// receive the same treatment.
let width = track.width() as u32;
let height = track.height() as u32;
if width > 3840 || height > 2160 {
let short_edge = width.min(height);
let long_edge = width.max(height);
if short_edge > 2160 || long_edge > 3840 {
return Err(MediaError::ResolutionTooHigh);
}
@@ -2547,6 +2551,43 @@ mod tests {
);
}
#[test]
fn test_validate_video_accepts_portrait_resolution() {
let mp4_bytes = build_mp4_bytes(true, b"avc1", 1_000, 2160, 3840, false);
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), &mp4_bytes).unwrap();
let meta = validate_video_file(tmp.path(), &test_config())
.expect("portrait video within the 2160x3840 envelope should be accepted");
assert_eq!((meta.width, meta.height), (2160, 3840));
}
#[test]
fn test_validate_video_rejects_resolution_above_short_edge_limit() {
let mp4_bytes = build_mp4_bytes(true, b"avc1", 1_000, 2161, 3840, false);
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), &mp4_bytes).unwrap();
let result = validate_video_file(tmp.path(), &test_config());
assert!(
matches!(result, Err(MediaError::ResolutionTooHigh)),
"expected ResolutionTooHigh, got {result:?}"
);
}
#[test]
fn test_validate_video_rejects_resolution_above_long_edge_limit() {
let mp4_bytes = build_mp4_bytes(true, b"avc1", 1_000, 2160, 3841, false);
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), &mp4_bytes).unwrap();
let result = validate_video_file(tmp.path(), &test_config());
assert!(
matches!(result, Err(MediaError::ResolutionTooHigh)),
"expected ResolutionTooHigh, got {result:?}"
);
}
#[test]
fn test_validate_video_resolution_too_high() {
let config = test_config();