From 247187872d85e93c39b773e2b405e3f2680a0a1f Mon Sep 17 00:00:00 2001 From: bflatastic Date: Wed, 7 Oct 2026 15:31:31 +0700 Subject: [PATCH] Render Queue: Output To an .mp4 file keeps an HEVC or AV1 module (#179) apply_output switched the format whenever the Output To extension mapped to a different format, and `.mp4` maps to H.264 only, so renderQueue.setOutput with an .mp4 path (even the item's own default name) turned an HEVC or AV1 module into H.264 and the render wrote an H.264 stream. The extension now changes the format only when the current format doesn't write it (OutputFormat::extension): HEVC, AV1 and H.264 all write .mp4, so they stay; `.mov` on H.264 still picks ProRes and `.mp4` on ProRes still picks H.264. Test: output_to_keeps_a_format_that_writes_the_extension. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/commands/render_queue.rs | 3 ++- crates/engine/src/tests_codec_options.rs | 27 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/commands/render_queue.rs b/crates/engine/src/commands/render_queue.rs index a7841e7e0..66fa0e2de 100644 --- a/crates/engine/src/commands/render_queue.rs +++ b/crates/engine/src/commands/render_queue.rs @@ -360,7 +360,8 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option< } if let Some(o) = str_p(p, "output").or(str_p(p, "path")) { om.output = o.to_string(); - if let Some(f) = OutputFormat::from_path(o).filter(|f| *f != om.format && !o.contains("[fileExtension]")) { + // Only an extension the format doesn't write changes it: `.mp4` is H.264's, HEVC's and AV1's. + if let Some(f) = OutputFormat::from_path(o).filter(|f| f.extension() != om.format.extension() && !o.contains("[fileExtension]")) { if p.get("format").is_some() { // An explicit format wins and the extension follows it, as choosing a format in // the Output Module renames Output To (#154: `--format hevc --out x.mp4`). diff --git a/crates/engine/src/tests_codec_options.rs b/crates/engine/src/tests_codec_options.rs index 757f7c431..807030638 100644 --- a/crates/engine/src/tests_codec_options.rs +++ b/crates/engine/src/tests_codec_options.rs @@ -83,6 +83,33 @@ fn an_explicit_format_wins_over_the_output_extension() { assert_eq!((om.format, om.output.as_str()), (OutputFormat::PngSequence, "/out/still.png")); } +/// Output To only changes the format when the format can't write the new extension: HEVC and AV1 +/// write `.mp4` too, so they stay (#179). Other extensions still pick a file type. +#[test] +fn output_to_keeps_a_format_that_writes_the_extension() { + let mut s = session(); + let cases = [ + ("hevc", "/out/x.mp4", OutputFormat::Hevc, "/out/x.mp4"), + ("av1", "/out/x.mp4", OutputFormat::Av1, "/out/x.mp4"), + ("h264", "/out/x.mov", OutputFormat::ProRes, "/out/x.mov"), + ("prores", "/out/x.mp4", OutputFormat::H264, "/out/x.mp4"), + ("h264", "/out/x.mp4", OutputFormat::H264, "/out/x.mp4"), + ]; + for (format, out, want, path) in cases { + let a = s.execute("renderQueue.add", json!({"format": format})).unwrap(); + let r = s.execute("renderQueue.setOutput", json!({"item": a["item"], "path": out})).unwrap(); + let om = &s.project.render_queue.last().unwrap().output; + assert_eq!((om.format, om.output.as_str()), (want, path), "{format} → {out}"); + if OutputFormat::from_name(format) == Some(want) { + assert_eq!(r["outputModuleSummary"], a["outputModuleSummary"], "{format} → {out}: the module is unchanged"); + } + } + // The module's own default name (HEVC resolves to `Main.mp4`) changes nothing either. + let a = s.execute("renderQueue.add", json!({"format": "hevc"})).unwrap(); + let r = s.execute("renderQueue.setOutput", json!({"item": a["item"], "path": a["outputPath"]})).unwrap(); + assert_eq!((&r["output"]["format"], &r["outputModuleSummary"]), (&json!("Hevc"), &a["outputModuleSummary"]), "{r}"); +} + /// A `comp` that names no composition says so, rather than "no active composition" (#155). #[test] fn an_unknown_comp_is_named_in_the_error() {