From c1e13b1f667118f825a1814f2201eef2d376c769 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Wed, 7 Oct 2026 15:45:53 +0700 Subject: [PATCH] Export: AV1 WebM refuses an explicit RGB + Alpha request (#166) apply_output checked `channels: rgba` against the module before it applied `webmCodec`, and choosing AV1 then quietly dropped the channels to RGB. So `render --format webm --webm-codec av1 --channels rgba` (one renderQueue.add) succeeded and wrote an opaque AV1 stream. The codec options now apply before Channels, so the existing "has no alpha channel" error (how an unsupported channels request is already reported) refuses the combination; for WebM it names the codec: "AV1 WebM has no alpha channel (VP9 WebM has)". Choosing AV1 alone on an RGBA VP9 item still switches it to RGB, as changing the format does. Test: av1_webm_refuses_requested_alpha. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/commands/render_queue.rs | 10 ++++++++-- crates/engine/src/tests_codec_options.rs | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/commands/render_queue.rs b/crates/engine/src/commands/render_queue.rs index 66fa0e2d..8abc855c 100644 --- a/crates/engine/src/commands/render_queue.rs +++ b/crates/engine/src/commands/render_queue.rs @@ -219,6 +219,9 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option< let f = OutputFormat::from_name(f).ok_or_else(|| bad(cmd, FORMATS))?; om.set_format(f); } + // Before Channels: the WebM codec decides whether alpha can be written, so an explicit + // `channels: rgba` with `webmCodec: av1` is refused rather than dropped (#166). + any |= apply_codec_options(om, p, cmd)?; if let Some(c) = str_p(p, "channels") { om.channels = match c.to_ascii_lowercase().replace([' ', '+'], "").as_str() { "rgb" => Channels::Rgb, @@ -227,7 +230,11 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option< _ => return Err(bad(cmd, "channels: rgb|rgba|alpha")), }; if om.channels == Channels::Rgba && !om.supports_alpha() { - return Err(bad(cmd, format!("{} has no alpha channel", om.format.label()))); + let what = match om.format { + OutputFormat::WebM => format!("{} WebM has no alpha channel (VP9 WebM has)", om.webm_codec.label()), + f => format!("{} has no alpha channel", f.label()), + }; + return Err(bad(cmd, what)); } } if let Some(c) = str_p(p, "color").or(str_p(p, "alphaMode")) { @@ -237,7 +244,6 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option< _ => return Err(bad(cmd, "color: straight|premultiplied")), }; } - any |= apply_codec_options(om, p, cmd)?; if let Some(q) = f_p(p, "quality") { om.quality = q.clamp(1.0, 100.0) as u8; } diff --git a/crates/engine/src/tests_codec_options.rs b/crates/engine/src/tests_codec_options.rs index 80703063..8adabba5 100644 --- a/crates/engine/src/tests_codec_options.rs +++ b/crates/engine/src/tests_codec_options.rs @@ -110,6 +110,25 @@ fn output_to_keeps_a_format_that_writes_the_extension() { assert_eq!((&r["output"]["format"], &r["outputModuleSummary"]), (&json!("Hevc"), &a["outputModuleSummary"]), "{r}"); } +/// An explicit RGB + Alpha request with the AV1 WebM codec (which has no alpha) is refused, not +/// silently rendered opaque (#166). Without `channels`, AV1 WebM renders RGB as before. +#[test] +fn av1_webm_refuses_requested_alpha() { + let mut s = session(); + let n = s.project.render_queue.len(); + let e = s.execute("renderQueue.add", json!({"format": "webm", "webmCodec": "av1", "channels": "rgba"})).unwrap_err().to_string(); + assert!(e.contains("AV1 WebM has no alpha channel"), "{e}"); + assert_eq!(s.project.render_queue.len(), n, "nothing queued"); + let a = s.execute("renderQueue.add", json!({"format": "webm", "channels": "rgba"})).unwrap(); + let e = s.execute("renderQueue.setOutputModule", json!({"item": a["item"], "webmCodec": "av1", "channels": "rgba"})).unwrap_err().to_string(); + assert!(e.contains("AV1 WebM has no alpha channel"), "{e}"); + let om = &s.project.render_queue.last().unwrap().output; + assert_eq!((om.webm_codec, om.channels), (WebmVideoCodec::Vp9, Channels::Rgba), "unchanged"); + s.execute("renderQueue.add", json!({"format": "webm", "webmCodec": "av1"})).unwrap(); + let om = &s.project.render_queue.last().unwrap().output; + assert_eq!((om.webm_codec, om.channels), (WebmVideoCodec::Av1, Channels::Rgb)); +} + /// A `comp` that names no composition says so, rather than "no active composition" (#155). #[test] fn an_unknown_comp_is_named_in_the_error() {