diff --git a/crates/engine/src/analysis/frames_key.rs b/crates/engine/src/analysis/frames_key.rs index e093c8aed..1b141c53e 100644 --- a/crates/engine/src/analysis/frames_key.rs +++ b/crates/engine/src/analysis/frames_key.rs @@ -226,7 +226,9 @@ impl Text { premul_color, loop_count, codec, - missing, + // Whether the file is there is an observation (`footage.check`), not an edit: an + // offline file is the same footage, and its analyses stay valid (#3534). + missing: _, sequence, color_profile, layer, @@ -276,7 +278,6 @@ impl Text { self.floats("matte", premul_color); self.field("loop", loop_count); self.str("codec", codec); - self.field("missing", missing); self.field("frames", sequence.len()); for s in sequence { self.str("file", s); diff --git a/crates/engine/src/analysis/frames_key/legacy.rs b/crates/engine/src/analysis/frames_key/legacy.rs index 6147883ae..882c82d80 100644 --- a/crates/engine/src/analysis/frames_key/legacy.rs +++ b/crates/engine/src/analysis/frames_key/legacy.rs @@ -6,16 +6,19 @@ use effectcraft_project::{Footage, ItemId, ItemKind, Layer, LayerSource, Project use super::fnv; -/// The signatures a saved key of `layer` may have been made from. Before `Footage::placed_page` -/// was added in 0.23, the footage text ended without that field. +/// The signatures a saved key of `layer` may have been made from. The footage's availability is +/// tried both ways: those releases hashed it, though it isn't part of the footage (#3534). Before +/// `Footage::placed_page` was added in 0.23, the footage text ended without that field. pub(crate) fn signatures(p: &Project, comp: ItemId, layer: &Layer) -> Vec { let src = match &layer.source { LayerSource::Footage { item } | LayerSource::Solid { item } | LayerSource::Comp { item } => match p.item(*item).map(|i| &i.kind) { Some(ItemKind::Footage(f)) => { - let mut out = Vec::with_capacity(2); - out.push(signature_with_source(p, comp, layer, &footage_text(f, f.missing, true))); - if matches!(layer.source, LayerSource::Footage { .. }) && !f.placed_page { - out.push(signature_with_source(p, comp, layer, &footage_text(f, f.missing, false))); + let mut out = Vec::with_capacity(4); + for missing in [false, true] { + out.push(signature_with_source(p, comp, layer, &footage_text(f, missing, true))); + if matches!(layer.source, LayerSource::Footage { .. }) && !f.placed_page { + out.push(signature_with_source(p, comp, layer, &footage_text(f, missing, false))); + } } return out; } diff --git a/crates/engine/src/tests/analysis_signature.rs b/crates/engine/src/tests/analysis_signature.rs index 98612bee4..58a930998 100644 --- a/crates/engine/src/tests/analysis_signature.rs +++ b/crates/engine/src/tests/analysis_signature.rs @@ -100,7 +100,7 @@ fn analyses_saved_by_0_24_survive_edits_that_keep_their_frames() { fn signature_text_is_pinned() { let s = open(ANALYSED_0_24); let layer = s.project.comp(COMP).unwrap().layer(CLIP).unwrap(); - let key = r#"frames/1;footage=1;footage;path="/tmp/effectcraft-3456-shake.mp4";kind=video;w=160;h=120;par=1.0;rate=12/1;nativeRate=none;duration=254016000000;video=true;audio=false;alpha=ignore;matte=[0.0, 0.0, 0.0];loop=1;codec="H.264 (High)";missing=false;frames=0;profile{rec709;};layer=none;fields=off;invertAlpha=false;linearLight=false;preserveRgb=false;cineon;log=false;points=[0.0, 1023.0, 95.0, 685.0, 1.7, 20.0];units=10bit;data=none;page=0;alphabetical=false;startTimecode=none;fileTimecode=none;placedPage=false;in=0;out=254016000000;start=0;stretch=100.0;timeRemap{prop="timeRemap";scalar=0.0;spatial=false;key=0;scalar=0.2;interp=linear/linear;continuous=false;autoBezier=false;spatialIn=[0.0, 0.0, 0.0];spatialOut=[0.0, 0.0, 0.0];spatialAuto=true;spatialContinuous=false;roving=false;muted=false;key=254016000000;scalar=1.0;interp=linear/linear;continuous=false;autoBezier=false;spatialIn=[0.0, 0.0, 0.0];spatialOut=[0.0, 0.0, 0.0];spatialAuto=true;spatialContinuous=false;roving=false;muted=false;expr=none;};compRate{rate=12/1;};"#; + let key = r#"frames/1;footage=1;footage;path="/tmp/effectcraft-3456-shake.mp4";kind=video;w=160;h=120;par=1.0;rate=12/1;nativeRate=none;duration=254016000000;video=true;audio=false;alpha=ignore;matte=[0.0, 0.0, 0.0];loop=1;codec="H.264 (High)";frames=0;profile{rec709;};layer=none;fields=off;invertAlpha=false;linearLight=false;preserveRgb=false;cineon;log=false;points=[0.0, 1023.0, 95.0, 685.0, 1.7, 20.0];units=10bit;data=none;page=0;alphabetical=false;startTimecode=none;fileTimecode=none;placedPage=false;in=0;out=254016000000;start=0;stretch=100.0;timeRemap{prop="timeRemap";scalar=0.0;spatial=false;key=0;scalar=0.2;interp=linear/linear;continuous=false;autoBezier=false;spatialIn=[0.0, 0.0, 0.0];spatialOut=[0.0, 0.0, 0.0];spatialAuto=true;spatialContinuous=false;roving=false;muted=false;key=254016000000;scalar=1.0;interp=linear/linear;continuous=false;autoBezier=false;spatialIn=[0.0, 0.0, 0.0];spatialOut=[0.0, 0.0, 0.0];spatialAuto=true;spatialContinuous=false;roving=false;muted=false;expr=none;};compRate{rate=12/1;};"#; assert_eq!(crate::analysis::frames_key::signature_text(&s.project, COMP, layer), key); } @@ -143,6 +143,14 @@ fn todays_keys_survive_edits_that_keep_their_frames() { let mut s = open(&s.project.to_json()); add_to_render_queue(&mut s); assert_eq!(saved(&s), before); + // Offline and back online (#3534). + for missing in [true, false] { + if let Some(effectcraft_project::ItemKind::Footage(f)) = std::sync::Arc::make_mut(&mut s.project).item_mut(ItemId(1)).map(|i| &mut i.kind) { + f.missing = missing; + } + add_to_render_queue(&mut s); + assert_eq!(saved(&s), before); + } // Names and labels only show things. s.execute_checked("layer.rename", json!({"layer": CLIP.0, "name": "Renamed"})).unwrap(); s.edit("rename", None, |p, _| { @@ -160,3 +168,33 @@ fn todays_keys_survive_edits_that_keep_their_frames() { s.execute_checked("file.interpretFootage", json!({"items": [1], "invertAlpha": true})).unwrap(); assert!(saved(&s).iter().all(|(data, _)| data.is_empty())); } + +/// Footage going offline (or coming back) is an observation, not an edit: the next edit keeps +/// every analysis (#3534). +#[test] +fn offline_footage_keeps_analyses() { + let mut s = open(ANALYSED_0_24); + let before = saved(&s); + let missing = |s: &Session| matches!(&s.project.item(ItemId(1)).unwrap().kind, effectcraft_project::ItemKind::Footage(f) if f.missing); + // The fixture's footage path doesn't exist here: the check marks it missing. + let r = s.execute_checked("footage.check", json!({"wait": true})).unwrap(); + assert_eq!(r["missing"], 1, "{r}"); + assert!(missing(&s)); + add_to_render_queue(&mut s); + assert_eq!(saved(&s), before); + + // Saved and reopened while offline, then back online. + let mut s = open(&s.project.to_json()); + assert!(missing(&s)); + add_to_render_queue(&mut s); + assert_eq!(saved(&s), before); + if let Some(effectcraft_project::ItemKind::Footage(f)) = std::sync::Arc::make_mut(&mut s.project).item_mut(ItemId(1)).map(|i| &mut i.kind) { + f.missing = false; + } + add_to_render_queue(&mut s); + assert_eq!(saved(&s), before); + + // A real change while offline still clears them. + s.execute_checked("file.interpretFootage", json!({"items": [1], "invertAlpha": true})).unwrap(); + assert!(saved(&s).iter().all(|(data, _)| data.is_empty())); +}