diff --git a/crates/engine/src/analysis/camera_track.rs b/crates/engine/src/analysis/camera_track.rs index 2d997ec37..ca52f3fe0 100644 --- a/crates/engine/src/analysis/camera_track.rs +++ b/crates/engine/src/analysis/camera_track.rs @@ -28,7 +28,7 @@ use serde::Serialize; use crate::offload::JobKind; use crate::tracking::lock; -use crate::warp::{analysis_times, signature}; +use crate::warp::{analysis_times, analysis_truncated, signature}; use crate::{Event, Session}; /// Live analysis progress (serde for agents / the control channel). @@ -320,6 +320,9 @@ impl Session { if times.len() < 2 { return Err("the layer must be at least two frames long".into()); } + if analysis_truncated(c, l, ×) { + return Err(format!("the layer is longer than {} frames: trim it before analysing", crate::warp::MAX_ANALYSIS_FRAMES)); + } let size = effectcraft_render::effect_bounds(&self.project, c, l).0; let params = static_params(g); let key = tracks_key(&self.project, comp, l, g); diff --git a/crates/engine/src/analysis/warp.rs b/crates/engine/src/analysis/warp.rs index 8a79a6279..b89cdc852 100644 --- a/crates/engine/src/analysis/warp.rs +++ b/crates/engine/src/analysis/warp.rs @@ -274,6 +274,12 @@ pub(crate) fn analysis_times(comp: &effectcraft_project::Comp, layer: &Layer, li v } +/// Whether `times` (from [`analysis_times`]) stopped at the cap with layer frames left over, so +/// the analysis would cover only the start of the layer. +pub(crate) fn analysis_truncated(comp: &effectcraft_project::Comp, layer: &Layer, times: &[Tick]) -> bool { + times.len() >= MAX_ANALYSIS_FRAMES && times.last().is_some_and(|&t| t + comp.frame_duration() < layer.out_point.min(comp.duration)) +} + impl Session { pub fn is_warp_analyzing(&self) -> bool { self.warp_job.as_ref().is_some_and(|j| !j.is_finished()) || self.offloaded(JobKind::Warp).is_some() @@ -328,6 +334,9 @@ impl Session { if times.len() < 2 { return Err("the layer must be at least two frames long".into()); } + if analysis_truncated(c, l, ×) { + return Err(format!("the layer is longer than {MAX_ANALYSIS_FRAMES} frames: trim it before analysing")); + } let size = effectcraft_render::effect_bounds(&self.project, c, l).0; let detailed = g.prop("advanced/detailedAnalysis").is_some_and(|p| p.value.as_bool()); let n = times.len(); @@ -440,7 +449,7 @@ mod schedule_tests { use effectcraft_project::{Comp, LayerSource, Project, build}; use effectcraft_time::{FrameRate, Tick}; - use super::{MAX_ANALYSIS_FRAMES, analysis_times}; + use super::{MAX_ANALYSIS_FRAMES, analysis_times, analysis_truncated}; #[test] fn a_long_layer_schedules_at_most_the_limit() { @@ -450,5 +459,11 @@ mod schedule_tests { let l = build::layer(&mut p, &c, "N", LayerSource::Null, (4, 4), None); assert_eq!(analysis_times(&c, &l, usize::MAX).len(), MAX_ANALYSIS_FRAMES); assert_eq!(analysis_times(&c, &l, 10).len(), 10); + assert!(analysis_truncated(&c, &l, &analysis_times(&c, &l, usize::MAX))); + // A short layer, or a short schedule from a limit, is not a capped analysis. + let short = Comp::new(4, 4, FrameRate::new(1000, 1), Tick::from_seconds_f64(1.0)); + let sl = build::layer(&mut p, &short, "S", LayerSource::Null, (4, 4), None); + assert!(!analysis_truncated(&short, &sl, &analysis_times(&short, &sl, usize::MAX))); + assert!(!analysis_truncated(&c, &l, &analysis_times(&c, &l, 10))); } } diff --git a/crates/engine/src/cmd/track.rs b/crates/engine/src/cmd/track.rs index 78b6540d7..3e59a469c 100644 --- a/crates/engine/src/cmd/track.rs +++ b/crates/engine/src/cmd/track.rs @@ -461,6 +461,8 @@ fn analyze(s: &mut Session, p: &V) -> Result { let prog = s.track_progress(); Ok(json!({ "frames": times.len() - 1, + // Hit the frame cap: track again from the last frame to go further. + "truncated": times.len() >= crate::warp::MAX_ANALYSIS_FRAMES, "start": times[0].seconds(), "end": times.last().map(|t| t.seconds()), "running": s.is_tracking(),