diff --git a/crates/engine/src/media_browser.rs b/crates/engine/src/media_browser.rs index 5edd67ca..12c002d2 100644 --- a/crates/engine/src/media_browser.rs +++ b/crates/engine/src/media_browser.rs @@ -312,7 +312,9 @@ pub fn footage_metadata(f: &Footage) -> Value { "pixelAspect": f.pixel_aspect, "frameRate": f.frame_rate.as_f64(), "duration": f.duration.seconds(), - "frames": if f.has_video && f.kind != FootageKind::Still { Some(f.frame_rate.frame_at(f.duration)) } else { None }, + // Rounded like the media pool's frame count: Matroska stores the duration in whole + // milliseconds, a hair short of the last frame at 30 fps (#1429). + "frames": if f.has_video && f.kind != FootageKind::Still { Some(f.frame_rate.frame_at(f.frame_rate.snap_nearest(f.duration))) } else { None }, "hasVideo": f.has_video, "hasAudio": f.has_audio, "alpha": format!("{:?}", f.alpha), diff --git a/crates/host/tests/webm_metadata.rs b/crates/host/tests/webm_metadata.rs new file mode 100644 index 00000000..2e3fed9d --- /dev/null +++ b/crates/host/tests/webm_metadata.rs @@ -0,0 +1,28 @@ +//! A WebM rendered by EffectCraft and imported back reports its real frame count (#1429). Matroska +//! stores the duration in whole milliseconds, so at 30 fps a 1-frame file lasts 33 ms, just short +//! of one 33.33 ms frame. + +use serde_json::json; + +fn tmp(name: &str) -> String { + let d = std::env::temp_dir().join(format!("effectcraft-host-webm-meta-{}", std::process::id())); + std::fs::create_dir_all(&d).unwrap(); + d.join(name).to_string_lossy().to_string() +} + +#[test] +fn a_webm_at_30_fps_reports_every_frame() { + for n in [1u32, 4] { + let mut s = effectcraft_host::session(); + let out = tmp(&format!("frames-{n}.webm")); + s.execute("comp.new", json!({"name": "Main", "width": 64, "height": 36, "frameRate": 30, "duration": 1})).unwrap(); + s.execute("layer.newSolid", json!({"name": "BG", "color": "#142850"})).unwrap(); + let span = json!({"comp": "Main", "format": "webm", "output": out, "timeSpan": "custom", "start": 0, "duration": n as f64 / 30.0}); + s.execute("renderQueue.add", span).unwrap(); + s.execute("renderQueue.render", json!({"wait": true})).unwrap(); + let r = s.execute("file.import", json!({"paths": [out.clone()]})).unwrap(); + let m = s.execute("item.metadata", json!({"item": r["items"][0]})).unwrap(); + let _ = std::fs::remove_file(&out); + assert_eq!(m["item"]["frames"], json!(n), "{n} rendered frames: {m}"); + } +}