mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 21:16:52 -06:00
Footage: a proxy in use that can't be decoded says why (#1701)
The picture comes from an item's proxy while Use Proxy is on, but the Project panel, the Viewer, project.summary and footage.info asked only about the item's own file: a proxy that couldn't be decoded (an AVI with an unsupported codec, say) left the layer empty with no reason given. They now ask FootageSource::item_problem, which gives the proxy's reason, said to be the proxy's, for footage and compositions alike.
This commit is contained in:
@@ -207,10 +207,7 @@ fn info(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let item = s.project.item(v.item);
|
||||
let name = item.map(|i| i.name.clone()).unwrap_or_default();
|
||||
let frame = fr.frame_at(v.time);
|
||||
let problem = match item.map(|i| &i.kind) {
|
||||
Some(ItemKind::Footage(f)) => s.footage.problem(f),
|
||||
_ => None,
|
||||
};
|
||||
let problem = item.and_then(|i| s.footage.item_problem(i));
|
||||
Ok(
|
||||
json!({"item": v.item.0, "name": name, "time": v.time.seconds(), "frame": frame, "timecode": timecode(item, fr, frame), "in": v.in_point.map(|t| t.seconds()), "out": v.out_point.map(|t| t.seconds()), "duration": dur.map(|d| d.seconds()), "frameRate": fr.as_f64(), "problem": problem}),
|
||||
)
|
||||
|
||||
@@ -46,11 +46,9 @@ fn summary(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
o["layers"] = json!(c.layers.len());
|
||||
o["frameRate"] = json!(c.frame_rate.as_f64());
|
||||
}
|
||||
// Footage that shows no picture although its file is there (a codec or profile the
|
||||
// decoders lack): why, so an agent can tell the user.
|
||||
if let ItemKind::Footage(f) = &i.kind
|
||||
&& let Some(why) = s.footage.problem(f)
|
||||
{
|
||||
// Footage (or the proxy in use) that shows no picture although its file is there (a
|
||||
// codec or profile the decoders lack): why, so an agent can tell the user.
|
||||
if let Some(why) = s.footage.item_problem(i) {
|
||||
o["problem"] = json!(why);
|
||||
}
|
||||
o
|
||||
|
||||
@@ -672,3 +672,47 @@ fn footage_that_cannot_be_decoded_reports_why() {
|
||||
s.execute("footage.open", json!({"item": item.0})).unwrap();
|
||||
assert_eq!(s.execute("footage.info", json!({})).unwrap()["problem"], it["problem"]);
|
||||
}
|
||||
|
||||
/// A proxy in use that can't be decoded leaves its item without a picture although the item's
|
||||
/// own file is fine: `project.summary` and `footage.info` give the proxy's reason, said to be the
|
||||
/// proxy's, for footage and compositions alike, and nothing once Use Proxy is off.
|
||||
#[test]
|
||||
fn a_proxy_in_use_that_cannot_be_decoded_reports_why() {
|
||||
struct BrokenProxies;
|
||||
impl FootageSource for BrokenProxies {
|
||||
fn frame(&self, _: ItemId, _: &Footage, _: Tick) -> Option<Arc<Image>> {
|
||||
None
|
||||
}
|
||||
fn problem(&self, f: &Footage) -> Option<String> {
|
||||
f.path.ends_with(".avi").then(|| format!("{} isn't supported by the built-in decoder", f.codec))
|
||||
}
|
||||
}
|
||||
let mut s = Session { footage: Arc::new(BrokenProxies), ..Default::default() };
|
||||
let comp = s.execute("comp.new", json!({"name": "C", "width": 64, "height": 36, "duration": 2})).unwrap()["comp"].as_u64().unwrap();
|
||||
let still = Footage { kind: FootageKind::Still, path: "a.png".into(), width: 64, height: 36, has_video: true, ..Default::default() };
|
||||
let item = std::sync::Arc::make_mut(&mut s.project).add_item("Plate", Default::default(), None, ItemKind::Footage(still));
|
||||
let proxy = Footage {
|
||||
kind: FootageKind::Video,
|
||||
path: "plate.avi".into(),
|
||||
codec: "MPEG-4 Part 2".into(),
|
||||
width: 32,
|
||||
height: 18,
|
||||
has_video: true,
|
||||
..Default::default()
|
||||
};
|
||||
let want = json!("Proxy: MPEG-4 Part 2 isn't supported by the built-in decoder");
|
||||
let problem = |s: &mut Session, id: u64| {
|
||||
let sum = s.execute("project.summary", json!({})).unwrap();
|
||||
sum["items"].as_array().unwrap().iter().find(|i| i["id"] == json!(id)).unwrap().get("problem").cloned()
|
||||
};
|
||||
for id in [item.0, comp] {
|
||||
let p = std::sync::Arc::make_mut(&mut s.project);
|
||||
p.items.get_mut(&ItemId(id)).unwrap().proxy = Some(Box::new(effectcraft_project::Proxy { footage: proxy.clone(), enabled: true }));
|
||||
assert_eq!(problem(&mut s, id), Some(want.clone()), "item {id}");
|
||||
std::sync::Arc::make_mut(&mut s.project).items.get_mut(&ItemId(id)).unwrap().proxy.as_mut().unwrap().enabled = false;
|
||||
assert_eq!(problem(&mut s, id), None, "item {id}: its own file shows, and is fine");
|
||||
}
|
||||
std::sync::Arc::make_mut(&mut s.project).items.get_mut(&item).unwrap().proxy.as_mut().unwrap().enabled = true;
|
||||
s.execute("footage.open", json!({"item": item.0})).unwrap();
|
||||
assert_eq!(s.execute("footage.info", json!({})).unwrap()["problem"], want);
|
||||
}
|
||||
|
||||
@@ -130,6 +130,18 @@ 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
|
||||
/// 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}")),
|
||||
None => match &item.kind {
|
||||
ItemKind::Footage(f) => self.problem(f),
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A render's view of a [`FootageSource`] whose frame reads can be called off with `cancel`
|
||||
|
||||
@@ -678,12 +678,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
actions.push(("file.useProxy".into(), json!({"item": id.0})));
|
||||
}
|
||||
}
|
||||
// Footage that can't be decoded (no decoder for its codec / profile): a warning marker
|
||||
// at the end of the name cell, the reason on hover.
|
||||
let problem = match &it.kind {
|
||||
ItemKind::Footage(f) => app.session.footage.problem(f),
|
||||
_ => None,
|
||||
};
|
||||
// Footage (or the proxy in use) that can't be decoded (no decoder for its codec /
|
||||
// profile): a warning marker at the end of the name cell, the reason on hover.
|
||||
let problem = app.session.footage.item_problem(it);
|
||||
let name_clip =
|
||||
Rect::from_min_max(pos2(x0 + 30.0, r.min.y), pos2(r.min.x + name_w + 4.0 - if problem.is_some() { 16.0 } else { 0.0 }, r.max.y)).intersect(list);
|
||||
if let Some(why) = &problem {
|
||||
|
||||
@@ -1155,13 +1155,14 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
app.auto.add("viewer.warpBanner", r, &text);
|
||||
}
|
||||
|
||||
// Footage that can't be decoded (no decoder for its codec / profile): say why on the layer
|
||||
// instead of leaving a black picture.
|
||||
// Footage (or the proxy in use for footage or a precomp) that can't be decoded (no decoder
|
||||
// for its codec / profile): say why on the layer instead of leaving a black picture.
|
||||
let mut notices = 0;
|
||||
for layer in comp.layers.iter().filter(|l| l.is_active_at(time) && l.switches.video) {
|
||||
let effectcraft_engine::project::LayerSource::Footage { item } = &layer.source else { continue };
|
||||
let Some(effectcraft_engine::project::ItemKind::Footage(f)) = app.session.project.item(*item).map(|i| &i.kind) else { continue };
|
||||
let Some(why) = app.session.footage.problem(f) else { continue };
|
||||
let (effectcraft_engine::project::LayerSource::Footage { item } | effectcraft_engine::project::LayerSource::Comp { item }) = &layer.source else {
|
||||
continue;
|
||||
};
|
||||
let Some(why) = app.session.project.item(*item).and_then(|i| app.session.footage.item_problem(i)) else { continue };
|
||||
if notices >= 3 {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
//! Footage whose codec the decoders lack says so: a warning marker in the Project panel and a
|
||||
//! notice on the layer in the Composition viewer, instead of a black picture with no reason.
|
||||
//! Footage (or the proxy in use for it) whose codec the decoders lack says so: a warning marker in
|
||||
//! the Project panel and a notice on the layer in the Composition viewer, instead of a black
|
||||
//! picture with no reason.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::project::{Footage, FootageKind, ItemId, ItemKind};
|
||||
use effectcraft_engine::project::{Footage, FootageKind, ItemId, ItemKind, Proxy};
|
||||
use effectcraft_engine::render::FootageSource;
|
||||
use effectcraft_render::Image;
|
||||
use effectcraft_time::Tick;
|
||||
@@ -14,19 +15,19 @@ use serde_json::json;
|
||||
|
||||
const WHY: &str = "H.264 (High 4:2:2) isn't supported by the built-in decoder: chroma_format_idc 2 (only 4:2:0 is implemented)";
|
||||
|
||||
/// A source that decodes nothing and, for `broken` movies, says why.
|
||||
struct Stub(bool);
|
||||
/// A source that decodes nothing and says why for the files `broken` picks.
|
||||
struct Stub(fn(&Footage) -> bool);
|
||||
|
||||
impl FootageSource for Stub {
|
||||
fn frame(&self, _: ItemId, _: &Footage, _: Tick) -> Option<Arc<Image>> {
|
||||
None
|
||||
}
|
||||
fn problem(&self, _: &Footage) -> Option<String> {
|
||||
self.0.then(|| WHY.to_string())
|
||||
fn problem(&self, f: &Footage) -> Option<String> {
|
||||
(self.0)(f).then(|| WHY.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn harness(broken: bool) -> (Harness<'static, EffectcraftApp>, ItemId) {
|
||||
fn harness(broken: fn(&Footage) -> bool, proxy: Option<Footage>) -> (Harness<'static, EffectcraftApp>, ItemId) {
|
||||
let mut s = Session { footage: Arc::new(Stub(broken)), ..Default::default() };
|
||||
s.execute("comp.new", json!({"name": "Main", "width": 320, "height": 180, "duration": 4})).unwrap();
|
||||
let footage = Footage {
|
||||
@@ -38,7 +39,11 @@ fn harness(broken: bool) -> (Harness<'static, EffectcraftApp>, ItemId) {
|
||||
duration: Tick::from_seconds_f64(2.0),
|
||||
..Default::default()
|
||||
};
|
||||
let clip = std::sync::Arc::make_mut(&mut s.project).add_item("Clip", Default::default(), None, ItemKind::Footage(footage));
|
||||
let p = std::sync::Arc::make_mut(&mut s.project);
|
||||
let clip = p.add_item("Clip", Default::default(), None, ItemKind::Footage(footage));
|
||||
if let (Some(item), Some(footage)) = (p.items.get_mut(&clip), proxy) {
|
||||
item.proxy = Some(Box::new(Proxy { footage, enabled: true }));
|
||||
}
|
||||
s.execute("layer.addItem", json!({"item": clip.0})).unwrap();
|
||||
s.execute("project.select", json!({"items": []})).unwrap();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
|
||||
@@ -48,7 +53,7 @@ fn harness(broken: bool) -> (Harness<'static, EffectcraftApp>, ItemId) {
|
||||
|
||||
#[test]
|
||||
fn undecodable_footage_is_marked_in_the_project_panel_and_the_viewer() {
|
||||
let (h, clip) = harness(true);
|
||||
let (h, clip) = harness(|_| true, None);
|
||||
let marker = h.state().auto.find(&format!("project.item.{}.problem", clip.0)).expect("a warning on the footage item").clone();
|
||||
assert!(marker.label.contains("4:2:2"), "{marker:?}");
|
||||
let notice = h.state().auto.query("viewer.footageProblem.").first().copied().expect("a notice in the viewer").clone();
|
||||
@@ -57,7 +62,19 @@ fn undecodable_footage_is_marked_in_the_project_panel_and_the_viewer() {
|
||||
|
||||
#[test]
|
||||
fn readable_footage_has_no_notice() {
|
||||
let (h, clip) = harness(false);
|
||||
let (h, clip) = harness(|_| false, None);
|
||||
assert!(h.state().auto.find(&format!("project.item.{}.problem", clip.0)).is_none());
|
||||
assert!(h.state().auto.query("viewer.footageProblem.").is_empty());
|
||||
}
|
||||
|
||||
/// The clip's own file is fine, but the proxy in use for it can't be decoded: the item and the
|
||||
/// layer say why, and that it is the proxy.
|
||||
#[test]
|
||||
fn an_undecodable_proxy_in_use_is_marked_as_the_proxy() {
|
||||
let proxy = Footage { kind: FootageKind::Video, path: "clip_proxy.avi".into(), width: 32, height: 18, has_video: true, ..Default::default() };
|
||||
let (h, clip) = harness(|f| f.path.ends_with(".avi"), Some(proxy));
|
||||
let marker = h.state().auto.find(&format!("project.item.{}.problem", clip.0)).expect("a warning on the footage item").clone();
|
||||
assert!(marker.label.starts_with("Proxy: ") && marker.label.contains("4:2:2"), "{marker:?}");
|
||||
let notice = h.state().auto.query("viewer.footageProblem.").first().copied().expect("a notice in the viewer").clone();
|
||||
assert!(notice.label.contains(&format!("Proxy: {WHY}")), "{notice:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user