Merge pull request #1051 from marcoarnulfo/fix/interpret-loop-video-duration

This commit is contained in:
Brandon Thomas 2026-10-13 04:28:37 -04:00
commit 15d2030d73
4 changed files with 26 additions and 5 deletions

View File

@ -775,9 +775,9 @@ fn interpret(s: &mut Session, p: &Value) -> Result<Value> {
s.edit("Interpret Footage", None, |proj, _| {
for (i, g) in items.iter().zip(&guesses) {
let Some(ItemKind::Footage(f)) = proj.item_mut(*i).map(|x| &mut x.kind) else { continue };
let old = f.duration;
let old = f.looped_duration();
it.apply(f, *g);
let new = f.duration;
let new = f.looped_duration();
if new == old {
continue;
}

View File

@ -283,7 +283,7 @@ fn add_item(s: &mut Session, p: &Value) -> Result<Value> {
&serde_json::json!({"comp": cid.0, "item": item.0, "time": f_p(p, "time"), "index": p.get("index"), "position": p.get("position")}),
);
}
ItemKind::Footage(f) => (LayerSource::Footage { item }, (f.width, f.height), Some(f.duration)),
ItemKind::Footage(f) => (LayerSource::Footage { item }, (f.width, f.height), Some(f.looped_duration())),
ItemKind::Solid(so) => (LayerSource::Solid { item }, (so.width, so.height), None),
ItemKind::Folder => return Err(bad("layer.addItem", "folders can't be layers")),
};
@ -998,7 +998,7 @@ fn slip(s: &mut Session, p: &Value) -> Result<Value> {
}
let dur = match &l.source {
LayerSource::Footage { item } => match s.project.item(*item).map(|i| &i.kind) {
Some(ItemKind::Footage(f)) if f.kind != effectcraft_project::FootageKind::Still => Some(Tick(f.duration.0 * f.loop_count.max(1) as i64)),
Some(ItemKind::Footage(f)) if f.kind != effectcraft_project::FootageKind::Still => Some(f.looped_duration()),
_ => None,
},
LayerSource::Comp { item } => s.project.comp(*item).map(|c| c.duration),

View File

@ -1515,3 +1515,19 @@ fn interpret_footage_loop_from_form_float() {
_ => panic!(),
}
}
/// Looped video footage lasts its loops: a 2 s clip looped 3 times is a 6 s layer, and a layer
/// that ran to the clip's end is re-timed when the loop count changes (#1045).
#[test]
fn interpret_footage_loop_extends_video_layers() {
let mut s = comp();
let p = s.execute("file.importPlaceholder", json!({"name": "Clip", "width": 320, "height": 240, "duration": 2})).unwrap()["item"].as_u64().unwrap();
let before = s.execute("layer.addItem", json!({"item": p})).unwrap()["layer"].as_u64().unwrap();
s.state.project_selection = vec![effectcraft_project::ItemId(p)];
s.execute("file.interpretFootage", json!({"loop": 3.0})).unwrap();
let after = s.execute("layer.addItem", json!({"item": p})).unwrap()["layer"].as_u64().unwrap();
let out = |s: &Session, l: u64| s.active_comp().unwrap().layer(effectcraft_project::LayerId(l)).unwrap().out_point.seconds();
assert_eq!(out(&s, after), 6.0, "a new layer plays every loop");
assert_eq!(out(&s, before), 6.0, "a layer that ran to the clip's end still does");
assert_eq!(s.project.item(effectcraft_project::ItemId(p)).unwrap().duration(), Some(Tick::from_seconds_f64(6.0)));
}

View File

@ -1175,6 +1175,11 @@ impl Footage {
self.linear_light && !self.preserve_rgb
}
/// How long the footage plays: its duration times Interpret Footage ▸ Loop.
pub fn looped_duration(&self) -> Tick {
Tick(self.duration.0.saturating_mul(self.loop_count.max(1) as i64))
}
/// Source time as the footage's timecode: its frame `frame` from Start Timecode
/// (`0:00:41:20`), as the Footage panel shows it.
pub fn timecode(&self, frame: i64) -> String {
@ -1298,7 +1303,7 @@ impl Item {
pub fn duration(&self) -> Option<Tick> {
match &self.kind {
ItemKind::Comp(c) => Some(c.duration),
ItemKind::Footage(f) if f.kind != FootageKind::Still => Some(f.duration),
ItemKind::Footage(f) if f.kind != FootageKind::Still => Some(f.looped_duration()),
_ => None,
}
}