mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
M10: render queue + export
- crates/export (effectcraft-export): render a comp's span to H.264 MP4 (+AAC), ProRes 422/4444 MOV (+PCM, alpha), PNG/JPEG/TIFF/EXR sequences and animated GIF via FilmCraft's pure-Rust encoders and MP4/MOV muxer; parallel batched frame rendering, progress callback with cancellation. - project: Render Queue model (Render Settings, Output Module, output path templates, status) saved with the project. - render: FootageSource::audio hook + comp audio mixdown (levels, in/out, solo, precomps); media implements it. - engine: Exporter hook, background/blocking render job, renderQueue.* commands (add Cmd+M, remove, setRender, setRenderSettings, setOutputModule, setOutput, move, duplicate, render, stop, list, formats). - host wires the exporter; CLI `render` subcommand. - ui-egui: After Effects-style Render Queue panel with automation ids. - tests: encode/decode round trips for every format (FilmCraft decoders, ffmpeg/ffprobe oracle when installed), audio, cancel, engine commands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dee16181fe
commit
7f9c3a85c0
@@ -2,27 +2,141 @@
|
||||
//!
|
||||
//! ```text
|
||||
//! effectcraft-cli frame [project.ecproj | --demo] [--comp NAME] [--time S | --frame N] [--scale K] --out out.png
|
||||
//! effectcraft-cli render [project.ecproj | --demo] [--comp NAME] --out FILE [--format F] [--start S] [--end S]
|
||||
//! [--work-area] [--fps N] [--resolution full|half|third|quarter|K] [--quality best|draft] [--channels rgb|rgba]
|
||||
//! [--jpeg-quality N] [--bitrate KBPS] [--prores proxy|lt|standard|hq|4444|4444xq] [--audio auto|on|off]
|
||||
//! effectcraft-cli render project.ecproj --queue (renders the project's Render Queue)
|
||||
//! effectcraft-cli run [project.ecproj | --demo] '<command-id>' '<json params>' … (prints results)
|
||||
//! effectcraft-cli commands (lists commands)
|
||||
//! ```
|
||||
//!
|
||||
//! `render` formats: h264 (.mp4), prores (.mov), png / jpeg / tiff / exr sequences (`name_[#####].png`), gif.
|
||||
//! The format defaults to the `--out` extension; the time span to the whole comp.
|
||||
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::project::render_queue::RenderStatus;
|
||||
use effectcraft_render::RenderOpts;
|
||||
use effectcraft_time::Tick;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn usage() -> ! {
|
||||
eprintln!("usage: effectcraft-cli <frame|run|commands> [args] (see --help in source docs)");
|
||||
eprintln!(
|
||||
"usage: effectcraft-cli <frame|render|run|commands> [args]
|
||||
|
||||
frame [project.ecproj|--demo] [--comp NAME] [--time S|--frame N] [--scale K] --out out.png
|
||||
render [project.ecproj|--demo] [--comp NAME] --out FILE [--format h264|prores|png|jpeg|tiff|exr|gif]
|
||||
[--start S] [--end S] [--work-area] [--fps N] [--resolution full|half|third|quarter|K]
|
||||
[--quality best|draft] [--channels rgb|rgba] [--jpeg-quality N] [--bitrate KBPS]
|
||||
[--prores proxy|lt|standard|hq|4444|4444xq] [--audio auto|on|off]
|
||||
render project.ecproj --queue (render the project's Render Queue)
|
||||
run [project.ecproj|--demo] '<command-id>' '<json params>' …
|
||||
commands"
|
||||
);
|
||||
std::process::exit(2)
|
||||
}
|
||||
|
||||
fn fail(msg: impl std::fmt::Display) -> ! {
|
||||
eprintln!("effectcraft: {msg}");
|
||||
std::process::exit(1)
|
||||
}
|
||||
|
||||
/// Queue `--comp` (or the active comp) with the given settings unless `--queue`, then render the
|
||||
/// queue with a progress line on stderr. Exit status 1 if any item fails.
|
||||
fn render(s: &mut Session, rest: &[String]) {
|
||||
if !rest.iter().any(|a| a == "--queue") {
|
||||
let out = opt(rest, "--out").unwrap_or_else(|| fail("render: --out FILE is required (or --queue)"));
|
||||
let mut p = json!({"output": out});
|
||||
if let Some(c) = opt(rest, "--comp") {
|
||||
p["comp"] = json!(c);
|
||||
}
|
||||
let ext = std::path::Path::new(out).extension().map(|e| e.to_string_lossy().to_string());
|
||||
if let Some(f) = opt(rest, "--format").map(str::to_string).or(ext) {
|
||||
p["format"] = json!(f);
|
||||
}
|
||||
let num = |k: &str| opt(rest, k).map(|v| v.parse::<f64>().unwrap_or_else(|_| fail(format!("{k}: not a number: {v}"))));
|
||||
match (num("--start"), num("--end")) {
|
||||
(None, None) => p["timeSpan"] = json!(if rest.iter().any(|a| a == "--work-area") { "workArea" } else { "comp" }),
|
||||
(a, b) => {
|
||||
p["start"] = json!(a.unwrap_or(0.0));
|
||||
if let Some(b) = b {
|
||||
p["end"] = json!(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(v) = num("--fps") {
|
||||
p["frameRate"] = json!(v);
|
||||
}
|
||||
if let Some(r) = opt(rest, "--resolution").or(opt(rest, "--scale")) {
|
||||
p["resolution"] = r.parse::<f64>().map(|v| json!(v)).unwrap_or(json!(r));
|
||||
}
|
||||
for (flag, key) in [("--quality", "quality"), ("--channels", "channels"), ("--prores", "proresProfile"), ("--audio", "audio")] {
|
||||
if let Some(v) = opt(rest, flag) {
|
||||
p[key] = json!(v);
|
||||
}
|
||||
}
|
||||
if let Some(v) = num("--jpeg-quality") {
|
||||
p["quality"] = json!(v);
|
||||
}
|
||||
if let Some(v) = num("--bitrate") {
|
||||
p["bitrate"] = json!(v);
|
||||
}
|
||||
// Render exactly this item: unqueue whatever the project's queue already holds.
|
||||
for k in 0..s.project.render_queue.len() {
|
||||
let _ = s.execute("renderQueue.setRender", json!({"index": k + 1, "render": false}));
|
||||
}
|
||||
let r = s.execute("renderQueue.add", p).unwrap_or_else(|e| fail(e));
|
||||
eprintln!(
|
||||
"{} → {} ({}×{}, {} frames, {})",
|
||||
r["compName"].as_str().unwrap_or("?"),
|
||||
r["outputPath"].as_str().unwrap_or("?"),
|
||||
r["width"],
|
||||
r["height"],
|
||||
r["frames"],
|
||||
r["outputModuleSummary"].as_str().unwrap_or("")
|
||||
);
|
||||
}
|
||||
s.execute("renderQueue.render", json!({"wait": false})).unwrap_or_else(|e| fail(e));
|
||||
let tty = std::io::IsTerminal::is_terminal(&std::io::stderr());
|
||||
while s.is_rendering() {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
if tty && let Some(p) = s.render_progress() {
|
||||
let left = p.remaining.map(|r| format!(", ~{r:.1}s left")).unwrap_or_default();
|
||||
eprint!("\r [{}/{}] frame {}/{} {:.1}s{left}\x1b[K", (p.items_done + 1).min(p.items_total), p.items_total, p.done, p.total, p.item_elapsed);
|
||||
}
|
||||
s.poll_render();
|
||||
}
|
||||
s.poll_render();
|
||||
if tty {
|
||||
eprintln!();
|
||||
}
|
||||
let mut failed = false;
|
||||
for it in s.project.render_queue.iter().filter(|i| i.render && i.started.is_some()) {
|
||||
let name = s.project.item(it.comp).map(|i| i.name.as_str()).unwrap_or("?");
|
||||
match &it.status {
|
||||
RenderStatus::Done => eprintln!("done: {name} → {} in {:.2}s", it.last_output.as_deref().unwrap_or("?"), it.render_time.unwrap_or(0.0)),
|
||||
RenderStatus::Failed(e) => {
|
||||
failed = true;
|
||||
eprintln!("failed: {name}: {e}");
|
||||
}
|
||||
other => {
|
||||
failed = true;
|
||||
eprintln!("{name}: {}", other.label());
|
||||
}
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn open(s: &mut Session, args: &[String]) {
|
||||
if let Some(p) = args.iter().find(|a| a.ends_with(".ecproj")) {
|
||||
if let Err(e) = s.execute("file.open", json!({"path": p})) {
|
||||
eprintln!("effectcraft: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else if args.iter().any(|a| a == "--demo") || true {
|
||||
} else {
|
||||
// `--demo` (or no project): the built-in demo project.
|
||||
let _ = s.execute("file.openDemoProject", json!({}));
|
||||
}
|
||||
}
|
||||
@@ -35,8 +149,12 @@ fn main() {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let Some(cmd) = args.first().cloned() else { usage() };
|
||||
let rest = &args[1..];
|
||||
let mut s = Session::default();
|
||||
let mut s = effectcraft_host::session();
|
||||
match cmd.as_str() {
|
||||
"render" => {
|
||||
open(&mut s, rest);
|
||||
render(&mut s, rest);
|
||||
}
|
||||
"frame" => {
|
||||
open(&mut s, rest);
|
||||
if let Some(c) = opt(rest, "--comp") {
|
||||
|
||||
Reference in New Issue
Block a user