mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-28 12:07:48 -06:00
MCP: report render progress and handle cancellation
Keep serving during long renders, recover failed workers, and remove only files opened by the interrupted export. Cover progress, cancellation, EOF and output ownership with regression tests. Co-Authored-By: OpenAI Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
OpenAI Codex
Claude Opus 5.5
parent
164538fecb
commit
7b632bdbc1
@@ -277,7 +277,7 @@ pub(crate) async fn movie(job: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut St
|
||||
st.advance(first_end)?;
|
||||
|
||||
let path = job.place(job.path, 1);
|
||||
let mut file = crate::out::create(job.sink, &path)?;
|
||||
let mut file = job.create(&path)?;
|
||||
let opts = WriterOptions::new(brand);
|
||||
let movie_ts = opts.movie_timescale.max(1) as i128;
|
||||
let mut mux: Writer = Mp4Writer::new(&mut file, opts).map_err(mux_err)?;
|
||||
|
||||
@@ -171,6 +171,10 @@ pub async fn export_async(job: &Job<'_>, progress: &mut dyn FnMut(&Progress) ->
|
||||
let t0 = Instant::now();
|
||||
let cx = Cx::new(job);
|
||||
let mut r = export_with(&cx, progress, t0).await;
|
||||
if matches!(r, Err(ExportError::Cancelled)) {
|
||||
cx.remove_cancelled_outputs()?;
|
||||
return r;
|
||||
}
|
||||
let log = cx.write_log(&r, t0.elapsed().as_secs_f64());
|
||||
if let Ok(rep) = &mut r {
|
||||
rep.log = log;
|
||||
@@ -263,7 +267,7 @@ async fn sequence(cx: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut State<'_>)
|
||||
write_still(cx, &mut buf, fmt, &img, comp, channels, w, h, cx.output.quality)?;
|
||||
let data = buf.into_inner();
|
||||
let path = cx.place(&path, data.len() as u64);
|
||||
let mut f = out::create(cx.sink, &path)?;
|
||||
let mut f = cx.create(&path)?;
|
||||
std::io::Write::write_all(&mut f, &data).map_err(io)?;
|
||||
f.finish()
|
||||
})
|
||||
@@ -350,7 +354,7 @@ async fn gif_export(job: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut State<'_
|
||||
return Err(ExportError::Unsupported("GIF frames are limited to 65535 px".into()));
|
||||
}
|
||||
let path = job.place(job.path, 1);
|
||||
let mut file = out::create(job.sink, &path)?;
|
||||
let mut file = job.create(&path)?;
|
||||
let mut enc = gif::Encoder::new(&mut file, w as u16, h as u16, &[]).map_err(|e| ExportError::Encode(e.to_string()))?;
|
||||
enc.set_repeat(if job.output.gif_loop { gif::Repeat::Infinite } else { gif::Repeat::Finite(0) }).map_err(|e| ExportError::Encode(e.to_string()))?;
|
||||
let rate = job.settings.rate(comp);
|
||||
|
||||
@@ -24,6 +24,7 @@ pub(crate) struct Cx<'a> {
|
||||
pub frame_times: Option<Mutex<Vec<(u64, f64)>>>,
|
||||
/// Files written to an overflow folder.
|
||||
pub overflowed: Mutex<Vec<String>>,
|
||||
written: Mutex<Vec<String>>,
|
||||
}
|
||||
|
||||
impl<'a> std::ops::Deref for Cx<'a> {
|
||||
@@ -68,7 +69,7 @@ impl<'a> Cx<'a> {
|
||||
let mut project = job.project.clone();
|
||||
apply_overrides(&mut project, job);
|
||||
let frame_times = (job.options.log == RenderLog::PlusPerFrameInfo).then(|| Mutex::new(Vec::new()));
|
||||
Cx { job, project, frame_times, overflowed: Mutex::new(vec![]) }
|
||||
Cx { job, project, frame_times, overflowed: Mutex::new(vec![]), written: Mutex::new(vec![]) }
|
||||
}
|
||||
|
||||
pub fn comp(&self) -> Option<&Comp> {
|
||||
@@ -187,6 +188,30 @@ impl<'a> Cx<'a> {
|
||||
crate::fit(px, img.width, img.height, w, h)
|
||||
}
|
||||
|
||||
/// Open a file and remember ownership for cancellation cleanup (including overflow paths).
|
||||
pub fn create(&self, path: &str) -> crate::Result<out::Out<'_>> {
|
||||
let file = out::create(self.sink, path)?;
|
||||
if self.sink.is_none() {
|
||||
self.written.lock().unwrap_or_else(std::sync::PoisonError::into_inner).push(path.to_owned());
|
||||
}
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
pub fn remove_cancelled_outputs(&self) -> crate::Result<()> {
|
||||
let mut failure = None;
|
||||
for path in self.written.lock().unwrap_or_else(std::sync::PoisonError::into_inner).iter() {
|
||||
if let Err(e) = std::fs::remove_file(path)
|
||||
&& e.kind() != std::io::ErrorKind::NotFound
|
||||
{
|
||||
failure.get_or_insert_with(|| crate::ExportError::Io(format!("cannot remove cancelled output {path}: {e}")));
|
||||
}
|
||||
}
|
||||
match failure {
|
||||
Some(error) => Err(error),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a file goes: `path`, or (Use Storage Overflow) the first overflow folder with room
|
||||
/// for `bytes` when the quota hook says the output volume is full.
|
||||
pub fn place(&self, path: &str, bytes: u64) -> String {
|
||||
@@ -246,7 +271,7 @@ impl<'a> Cx<'a> {
|
||||
s.push_str(&format!(" Frame {} ({:.3} s): rendered in {:.1} ms\n", f0 + i as i64, t.seconds(), secs * 1000.0));
|
||||
}
|
||||
}
|
||||
let mut f = out::create(self.job.sink, &path).ok()?;
|
||||
let mut f = self.create(&path).ok()?;
|
||||
std::io::Write::write_all(&mut f, s.as_bytes()).ok()?;
|
||||
f.finish().ok()?;
|
||||
Some(path)
|
||||
|
||||
@@ -162,7 +162,7 @@ pub(crate) async fn webm(job: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut Sta
|
||||
let duration_ms = frame_ms(total) as f64;
|
||||
|
||||
let path = job.place(job.path, 1);
|
||||
let mut file = crate::out::create(job.sink, &path)?;
|
||||
let mut file = job.create(&path)?;
|
||||
// EBML header.
|
||||
let mut head = vec![];
|
||||
let mut eb = vec![];
|
||||
@@ -403,7 +403,7 @@ pub(crate) fn audio_file(job: &Cx, comp: &Comp, aiff: bool, st: &mut State) -> R
|
||||
}
|
||||
}
|
||||
let path = job.place(job.path, out.len() as u64);
|
||||
let mut file = crate::out::create(job.sink, &path)?;
|
||||
let mut file = job.create(&path)?;
|
||||
file.write_all(&out).map_err(io)?;
|
||||
let bytes = file.finish()?;
|
||||
Ok(Report { path, frames: 0, width: 0, height: 0, seconds: 0.0, bytes, audio: true, log: None, overflow: vec![] })
|
||||
|
||||
@@ -51,7 +51,7 @@ pub(crate) async fn webm_av1(job: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut
|
||||
let duration_ms = frame_ms(total) as f64;
|
||||
|
||||
let path = job.place(job.path, 1);
|
||||
let mut file = crate::out::create(job.sink, &path)?;
|
||||
let mut file = job.create(&path)?;
|
||||
let mut head = vec![];
|
||||
let mut eb = vec![];
|
||||
el_uint(&mut eb, 0x4286, 1);
|
||||
|
||||
Reference in New Issue
Block a user