Report offline footage and recover automatically when media returns (#3161)

* 3118: report and recover offline footage consistently

* 3118: preserve manual reload and refresh data watch paths

* 3118: document availability and reload contracts
This commit is contained in:
ash
2026-11-06 11:38:57 +01:00
committed by GitHub
parent ce9eaec650
commit 7fc1be0bf2
25 changed files with 1347 additions and 142 deletions
+5
View File
@@ -1004,6 +1004,11 @@ pub fn footage_salt_with(project: &effectcraft_project::Project, virtual_id: Opt
hash_debug(&mut h, f);
footage_stamp(&mut h, f, virtual_id);
}
if let Some(proxy) = it.proxy.as_ref().filter(|p| p.enabled) {
h.write_u64(id.0);
hash_debug(&mut h, proxy);
footage_stamp(&mut h, &proxy.footage, virtual_id);
}
}
let v = h.finish();
(v as u64) ^ ((v >> 64) as u64)
+21 -3
View File
@@ -99,20 +99,38 @@ pub trait FootageSource: Send + Sync {
fn problem(&self, _footage: &Footage) -> Option<String> {
None
}
/// [`Self::problem`] of what `item` shows: its proxy's while Use Proxy shows the proxy
/// Missing files and [`Self::problem`] of what `item` shows: its proxy's while Use Proxy shows the proxy
/// instead (the reason then says it is the proxy's), else the footage item's own. A
/// composition has one only through its proxy.
fn item_problem(&self, item: &effectcraft_project::Item) -> Option<String> {
match item.proxy.as_deref().filter(|px| px.enabled && px.footage.has_video) {
Some(px) => self.problem(&px.footage).map(|why| format!("Proxy: {why}")),
Some(px) => {
missing_problem(&px.footage, "Use Set Proxy to locate the proxy file.").or_else(|| self.problem(&px.footage)).map(|why| format!("Proxy: {why}"))
}
None => match &item.kind {
ItemKind::Footage(f) => self.problem(f),
ItemKind::Footage(f) => missing_problem(f, "Use File ▸ Dependencies ▸ Relink Missing Footage or Replace Footage.").or_else(|| self.problem(f)),
_ => None,
},
}
}
}
/// A known offline source, without filesystem access while painting or querying.
fn missing_problem(f: &Footage, remedy: &str) -> Option<String> {
(f.missing && !f.path.is_empty() && f.kind != effectcraft_project::FootageKind::Data).then(|| format!("Missing footage: {}. {remedy}", f.path))
}
/// Inventory warning, including an unused proxy or an original currently replaced by a proxy.
/// Viewer warnings instead use `item_problem`, which follows the active render source.
pub fn missing_item_problem(item: &effectcraft_project::Item) -> Option<String> {
if let ItemKind::Footage(f) = &item.kind
&& let Some(why) = missing_problem(f, "Use File ▸ Dependencies ▸ Relink Missing Footage or Replace Footage.")
{
return Some(why);
}
item.proxy.as_ref().and_then(|p| missing_problem(&p.footage, "Use Set Proxy to locate the proxy file.")).map(|why| format!("Proxy: {why}"))
}
/// A render's view of a [`FootageSource`] whose frame reads can be called off with `cancel`
/// (a viewer frame nobody waits for any more, #329). Every read goes through
/// [`FootageSource::cancellable`] on its own, so reads made for this render on another thread
+1 -1
View File
@@ -46,7 +46,7 @@ mod opts;
mod pipeline;
pub use accel::{Accelerator, Backend, FxStep, FxTarget};
pub use footage::{CancellableFootage, FloatCurve, FootageSource, NoFootage, is_vector_footage};
pub use footage::{CancellableFootage, FloatCurve, FootageSource, NoFootage, is_vector_footage, missing_item_problem};
pub use frames::{
adaptive_samples, blend_frames, cineon_frame, content_bounds, extended_region, field_frame, frame_position, frame_size, interpret_pixels, kurbo_path,
render_frame, roi_inside,