From b5b77f446e3f86fa3cb5d24eb996820b59416511 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 11:41:27 +0000 Subject: [PATCH] #1417: Reload Footage re-reads a file's markers; index marker times when merging them Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U --- crates/engine/src/cmd/markers.rs | 5 ++- crates/engine/src/footage/footage_reload.rs | 2 + crates/host/tests/it/wav_markers.rs | 44 ++++++++++++++++++--- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/cmd/markers.rs b/crates/engine/src/cmd/markers.rs index 35ab7123f..3530eaa4b 100644 --- a/crates/engine/src/cmd/markers.rs +++ b/crates/engine/src/cmd/markers.rs @@ -328,8 +328,11 @@ fn update_from_source(s: &mut Session, p: &Value) -> Result { if l.markers_locked { continue; } + // One set of the times already on the layer: a file with thousands of markers must not + // scan the layer's list for each of them. + let mut times: std::collections::HashSet<_> = l.markers.iter().map(|x| x.time).collect(); for m in ms { - if !l.markers.iter().any(|x| x.time == m.time) { + if times.insert(m.time) { l.markers.push(m.clone()); n += 1; } diff --git a/crates/engine/src/footage/footage_reload.rs b/crates/engine/src/footage/footage_reload.rs index 75fd0f936..267985e37 100644 --- a/crates/engine/src/footage/footage_reload.rs +++ b/crates/engine/src/footage/footage_reload.rs @@ -81,6 +81,8 @@ fn refreshed(s: &Session, f: &Footage, probed: Footage) -> Footage { nf.codec = probed.codec; nf.has_video = probed.has_video; nf.has_audio = probed.has_audio; + // The markers the file carries now (a WAV's cue points), for Update Markers From Source. + nf.markers = probed.markers; if matches!(f.kind, FootageKind::Video | FootageKind::Audio) { nf.duration = probed.duration; // Interpret Footage ▸ Conform to frame rate stays; the file's own rate is refreshed. diff --git a/crates/host/tests/it/wav_markers.rs b/crates/host/tests/it/wav_markers.rs index 6f88b9c9c..98d2685dc 100644 --- a/crates/host/tests/it/wav_markers.rs +++ b/crates/host/tests/it/wav_markers.rs @@ -12,8 +12,9 @@ fn names(s: &mut Session, layer: u64) -> Vec { list.as_array().unwrap().iter().map(|m| m["comment"].as_str().unwrap().to_string()).collect() } -/// A one-second mono 8 kHz WAV with two named cue points (a `cue ` chunk and a `LIST` `adtl`). -fn marker_wav() -> Vec { +/// A one-second mono 8 kHz WAV with the named cue points `(id, sample, name)` (a `cue ` chunk and a +/// `LIST` `adtl`). +fn marker_wav(points: &[(u32, u32, &str)]) -> Vec { fn chunk(id: &[u8; 4], body: &[u8]) -> Vec { let mut v = id.to_vec(); v.extend((body.len() as u32).to_le_bytes()); @@ -30,10 +31,9 @@ fn marker_wav() -> Vec { fmt.extend(16000u32.to_le_bytes()); fmt.extend(2u16.to_le_bytes()); fmt.extend(16u16.to_le_bytes()); - let points = [(1u32, 0u32, "Intro"), (2, 4000, "Middle")]; let mut cue = (points.len() as u32).to_le_bytes().to_vec(); let mut adtl = b"adtl".to_vec(); - for (id, at, name) in points { + for &(id, at, name) in points { for v in [id, 0] { cue.extend(v.to_le_bytes()); } @@ -66,7 +66,7 @@ fn a_wav_layer_starts_with_the_markers_in_its_file() { let dir = std::env::temp_dir().join(format!("effectcraft-host-wav-markers-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("cues.wav"); - std::fs::write(&path, marker_wav()).unwrap(); + std::fs::write(&path, marker_wav(&[(1, 0, "Intro"), (2, 4000, "Middle")])).unwrap(); let r = s.execute_checked("file.import", json!({"paths": [path.to_string_lossy()], "addToComp": true})).unwrap(); assert_eq!(r["errors"], json!([]), "{r}"); let layer = s.active_comp().unwrap().layers.iter().find(|l| matches!(l.source, LayerSource::Footage { .. })).unwrap().id.0; @@ -90,3 +90,37 @@ fn a_wav_layer_starts_with_the_markers_in_its_file() { assert_eq!(s.execute_checked("layer.updateMarkersFromSource", json!({})).unwrap()["added"], 0); let _ = std::fs::remove_dir_all(&dir); } + +/// Reload Footage re-reads the markers the file carries: a cue point added to the file after the +/// import is available to Update Markers From Source (and a layer already made keeps its own). +#[test] +fn reloading_a_wav_picks_up_cue_points_added_to_the_file() { + let mut s = effectcraft_host::session(); + s.execute_checked("comp.new", json!({"name": "C", "width": 64, "height": 48, "frameRate": 30, "duration": 2})).unwrap(); + let dir = std::env::temp_dir().join(format!("effectcraft-host-wav-reload-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("cues.wav"); + std::fs::write(&path, marker_wav(&[(1, 0, "Intro")])).unwrap(); + let r = s.execute_checked("file.import", json!({"paths": [path.to_string_lossy()], "addToComp": true})).unwrap(); + assert_eq!(r["errors"], json!([]), "{r}"); + let (layer, item) = s + .active_comp() + .unwrap() + .layers + .iter() + .find_map(|l| match l.source { + LayerSource::Footage { item } => Some((l.id.0, item.0)), + _ => None, + }) + .unwrap(); + assert_eq!(names(&mut s, layer), ["Intro"]); + // The file gains a cue point; the layer does not change until asked. + std::fs::write(&path, marker_wav(&[(1, 0, "Intro"), (2, 4000, "Added later")])).unwrap(); + let r = s.execute_checked("file.reloadFootage", json!({"items": [item]})).unwrap(); + assert_eq!(r["reloaded"], 1, "{r}"); + assert_eq!(names(&mut s, layer), ["Intro"]); + let r = s.execute_checked("layer.updateMarkersFromSource", json!({"layers": [layer]})).unwrap(); + assert_eq!(r["added"], 1); + assert_eq!(names(&mut s, layer), ["Intro", "Added later"]); + let _ = std::fs::remove_dir_all(&dir); +}