use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use serde::Serialize;
use crate::error::{Error, Result};
const XMP_UUID: [u8; 16] = [0xbe, 0x7a, 0xcf, 0xcb, 0x97, 0xa9, 0x42, 0xe8, 0x9c, 0x71, 0x99, 0x94, 0x91, 0xe3, 0xaf, 0xac];
const MAX_BOXES: usize = 100_000;
#[derive(Debug, Default, Serialize, PartialEq)]
pub struct VideoMeta {
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_seconds: Option<u64>,
pub patches: Vec<u64>,
}
struct Walker {
file: File,
meta: VideoMeta,
boxes: usize,
}
impl Walker {
fn read_at(&mut self, pos: u64, buf: &mut [u8]) -> bool {
self.file.seek(SeekFrom::Start(pos)).is_ok() && self.file.read_exact(buf).is_ok()
}
fn u32_at(&mut self, pos: u64) -> Option<u32> {
let mut b = [0u8; 4];
self.read_at(pos, &mut b).then(|| u32::from_be_bytes(b))
}
fn u64_at(&mut self, pos: u64) -> Option<u64> {
let mut b = [0u8; 8];
self.read_at(pos, &mut b).then(|| u64::from_be_bytes(b))
}
fn walk(&mut self, start: u64, end: u64, depth: u32, parent: &[u8; 4]) {
let mut i = start;
while i + 8 <= end {
self.boxes += 1;
if self.boxes > MAX_BOXES {
return;
}
let mut head = [0u8; 8];
if !self.read_at(i, &mut head) {
return;
}
let mut size = u32::from_be_bytes([head[0], head[1], head[2], head[3]]) as u64;
let kind: [u8; 4] = [head[4], head[5], head[6], head[7]];
let mut header = 8u64;
if size == 1 {
let Some(large) = self.u64_at(i + 8) else { return };
size = large;
header = 16;
} else if size == 0 {
size = end - i;
}
if size < header || i.checked_add(size).is_none_or(|e| e > end) {
return;
}
if depth == 0 && &kind == b"uuid" {
let mut id = [0u8; 16];
if self.read_at(i + header, &mut id) && id == XMP_UUID {
self.meta.patches.push(i + 4);
}
} else if (parent == b"moov" || parent == b"trak") && (&kind == b"udta" || &kind == b"meta") {
self.meta.patches.push(i + 4);
} else if &kind == b"mvhd" {
self.mvhd(i + header);
} else if &kind == b"tkhd" && self.meta.width.is_none() {
self.tkhd(i + header);
}
if &kind == b"moov" || &kind == b"trak" {
self.walk(i + header, i + size, depth + 1, &kind);
}
i += size;
}
}
fn mvhd(&mut self, body: u64) {
let mut version = [0u8; 1];
if !self.read_at(body, &mut version) {
return;
}
let (timescale, duration) = if version[0] == 1 {
(self.u32_at(body + 20), self.u64_at(body + 24))
} else {
(self.u32_at(body + 12), self.u32_at(body + 16).map(u64::from))
};
if let (Some(timescale), Some(duration)) = (timescale, duration) {
if timescale > 0 {
self.meta.duration_seconds = Some((duration as f64 / timescale as f64).round().max(0.0) as u64);
}
}
}
fn tkhd(&mut self, body: u64) {
let mut version = [0u8; 1];
if !self.read_at(body, &mut version) {
return;
}
let off = body + if version[0] == 1 { 88 } else { 76 };
if let (Some(w), Some(h)) = (self.u32_at(off), self.u32_at(off + 4)) {
let (w, h) = (w >> 16, h >> 16);
if w > 0 && h > 0 {
self.meta.width = Some(w);
self.meta.height = Some(h);
}
}
}
}
pub fn prepare_video(real: &Path) -> Result<VideoMeta> {
let file = File::open(real).map_err(|e| Error::from_io_read(&e))?;
let size = file.metadata().map_err(|e| Error::from_io_read(&e))?.len();
let mut walker = Walker { file, meta: VideoMeta::default(), boxes: 0 };
walker.walk(0, size, 0, b" ");
Ok(walker.meta)
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
pub fn mp4_box(kind: &[u8; 4], body: &[u8]) -> Vec<u8> {
let mut out = ((body.len() + 8) as u32).to_be_bytes().to_vec();
out.extend_from_slice(kind);
out.extend_from_slice(body);
out
}
pub fn synthetic_mp4() -> (Vec<u8>, Vec<u64>) {
let ftyp = mp4_box(b"ftyp", b"isom\0\0\0\x01isommp41");
let mut xmp_body = XMP_UUID.to_vec();
xmp_body.extend_from_slice(b"<x:xmpmeta GPS/>");
let xmp = mp4_box(b"uuid", &xmp_body);
let other_uuid = mp4_box(b"uuid", &[7u8; 20]);
let mut mvhd = vec![0u8; 100];
mvhd[12..16].copy_from_slice(&1000u32.to_be_bytes());
mvhd[16..20].copy_from_slice(&5400u32.to_be_bytes());
let mvhd = mp4_box(b"mvhd", &mvhd);
let mut tkhd = vec![0u8; 84];
tkhd[76..80].copy_from_slice(&(640u32 << 16).to_be_bytes());
tkhd[80..84].copy_from_slice(&(360u32 << 16).to_be_bytes());
let tkhd = mp4_box(b"tkhd", &tkhd);
let trak_udta = mp4_box(b"udta", b"\xa9xyz+39.9-116.4/");
let mut trak_body = tkhd.clone();
trak_body.extend_from_slice(&trak_udta);
let trak = mp4_box(b"trak", &trak_body);
let moov_udta = mp4_box(b"udta", b"location");
let moov_meta = mp4_box(b"meta", b"\0\0\0\0hdlr");
let mut moov_body = mvhd.clone();
moov_body.extend_from_slice(&trak);
moov_body.extend_from_slice(&moov_udta);
moov_body.extend_from_slice(&moov_meta);
let moov = mp4_box(b"moov", &moov_body);
let mdat = mp4_box(b"mdat", &[0x55u8; 3000]);
let mut file = Vec::new();
file.extend_from_slice(&ftyp);
let xmp_at = file.len() as u64;
file.extend_from_slice(&xmp);
file.extend_from_slice(&other_uuid);
let moov_at = file.len() as u64;
let trak_at = moov_at + 8 + mvhd.len() as u64;
let trak_udta_at = trak_at + 8 + tkhd.len() as u64;
let moov_udta_at = trak_at + trak.len() as u64;
let moov_meta_at = moov_udta_at + moov_udta.len() as u64;
file.extend_from_slice(&moov);
file.extend_from_slice(&mdat);
(file, vec![xmp_at + 4, trak_udta_at + 4, moov_udta_at + 4, moov_meta_at + 4])
}
#[test]
fn walks_boxes_like_scrub_video() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("a.mp4");
let (bytes, expected) = synthetic_mp4();
std::fs::write(&path, &bytes).unwrap();
let meta = prepare_video(&path).unwrap();
assert_eq!((meta.width, meta.height, meta.duration_seconds), (Some(640), Some(360), Some(5)));
let mut patches = meta.patches.clone();
patches.sort();
let mut expected = expected;
expected.sort();
assert_eq!(patches, expected);
for p in &patches {
let kind = &bytes[*p as usize..*p as usize + 4];
assert!(kind == b"udta" || kind == b"meta" || kind == b"uuid");
}
assert_eq!(std::fs::read(&path).unwrap(), bytes);
}
#[test]
fn broken_box_stops_walk() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("b.mp4");
let mut bytes = mp4_box(b"ftyp", b"isom");
bytes.extend_from_slice(&1000u32.to_be_bytes());
bytes.extend_from_slice(b"moov");
std::fs::write(&path, &bytes).unwrap();
assert_eq!(prepare_video(&path).unwrap(), VideoMeta::default());
let mut large = 1u32.to_be_bytes().to_vec();
large.extend_from_slice(b"mdat");
large.extend_from_slice(&24u64.to_be_bytes());
large.extend_from_slice(&[0u8; 8]);
large.extend_from_slice(&mp4_box(b"udta", b"x"));
std::fs::write(&path, &large).unwrap();
assert!(prepare_video(&path).unwrap().patches.is_empty());
}
}