#3534: footage going offline no longer drops saved analyses

Whether a footage file is there is an observation (footage.check sets
Footage::missing without an edit), not part of the footage. The
signature leaves it out, and keys saved by 0.22-0.24, which hashed it,
are recognised with the file either online or offline. Warp Stabilizer,
3D Camera Tracker and Roto Brush analyses are kept through the next edit
while the footage is offline and after it comes back.
This commit is contained in:
arnav-hiwarkar committed 2026-11-12 14:10:23 +05:30
1 parent 54d53fff76
commit 54cf31f841
3 files changed
+51 -9

No files matched your search

+3 -2
View File
@@ -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);
@@ -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<String> {
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;
}
+39 -1
View File
@@ -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()));
}