M13.4: HEVC / AV1 output modules (MP4 hvc1/av01, WebM AV1), codec + Opus options in engine, UI, CLI

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-03 05:01:43 -04:00
co-authored by Claude Opus 5.5
parent 3a8b401e81
commit 280f7d509c
15 changed files with 906 additions and 49 deletions
+75 -4
View File
@@ -135,7 +135,7 @@ fn apply_settings(rs: &mut RenderSettings, p: &Value, cmd: &str) -> Result<bool>
fn apply_output(om: &mut OutputModule, p: &Value, cmd: &str) -> Result<bool> {
let mut any = false;
if let Some(f) = str_p(p, "format") {
let f = OutputFormat::from_name(f).ok_or_else(|| bad(cmd, "format: h264|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff"))?;
let f = OutputFormat::from_name(f).ok_or_else(|| bad(cmd, "format: h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff"))?;
om.set_format(f);
any = true;
}
@@ -145,11 +145,12 @@ fn apply_output(om: &mut OutputModule, p: &Value, cmd: &str) -> Result<bool> {
"rgba" | "rgbalpha" => Channels::Rgba,
_ => return Err(bad(cmd, "channels: rgb|rgba")),
};
if om.channels == Channels::Rgba && !om.format.supports_alpha() {
if om.channels == Channels::Rgba && !om.supports_alpha() {
return Err(bad(cmd, format!("{} has no alpha channel", om.format.label())));
}
any = true;
}
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;
any = true;
@@ -199,6 +200,66 @@ fn apply_output(om: &mut OutputModule, p: &Value, cmd: &str) -> Result<bool> {
Ok(any)
}
/// HEVC / AV1 codec options, the WebM video codec and the Opus audio options.
fn apply_codec_options(om: &mut OutputModule, p: &Value, cmd: &str) -> Result<bool> {
use effectcraft_project::render_queue::{CodecProfile, OpusApplication, RateControlMode, VideoCodecOptions, WebmVideoCodec};
let mut any = false;
if let Some(c) = str_p(p, "webmCodec") {
om.webm_codec = match c.to_ascii_lowercase().as_str() {
"vp9" => WebmVideoCodec::Vp9,
"av1" => WebmVideoCodec::Av1,
_ => return Err(bad(cmd, "webmCodec: vp9|av1")),
};
if !om.supports_alpha() {
om.channels = Channels::Rgb;
}
any = true;
}
if let Some(v) = str_p(p, "profile") {
om.codec.profile = CodecProfile::from_name(v).ok_or_else(|| bad(cmd, "profile: main|main10"))?;
any = true;
}
match p.get("level") {
Some(Value::Null) => {
om.codec.level = None;
any = true;
}
Some(v) => {
let s = match v {
Value::String(s) => s.clone(),
other => other.to_string(),
};
om.codec.level = VideoCodecOptions::parse_level(&s).ok_or_else(|| bad(cmd, "level: auto|null or a level like 4.1"))?;
any = true;
}
None => {}
}
if let Some(v) = str_p(p, "rateControl") {
om.codec.rate_control = RateControlMode::from_name(v).ok_or_else(|| bad(cmd, "rateControl: bitrate|quality"))?;
any = true;
}
if let Some(q) = f_p(p, "quality") {
om.codec.quality = q.clamp(1.0, 100.0) as u8;
}
if let Some(k) = f_p(p, "keyframeInterval") {
om.codec.keyframe_interval = k.clamp(0.0, 100_000.0) as u32;
any = true;
}
if let Some(b) = f_p(p, "audioBitrate") {
om.opus_bitrate_kbps = b.clamp(6.0, 510.0) as u32;
any = true;
}
if let Some(a) = str_p(p, "opusApplication") {
om.opus_application = match a.to_ascii_lowercase().as_str() {
"audio" | "music" => OpusApplication::Audio,
"voip" | "voice" | "speech" => OpusApplication::Voip,
_ => return Err(bad(cmd, "opusApplication: audio|voice")),
};
any = true;
}
Ok(any)
}
/// `module` (1-based; 1 = the first output module).
fn module_p(s: &Session, i: usize, p: &Value, cmd: &str) -> Result<usize> {
let n = p.get("module").and_then(Value::as_u64).unwrap_or(1) as usize;
@@ -419,7 +480,17 @@ fn formats(s: &mut Session, _: &Value) -> Result<Value> {
})
})
.collect();
Ok(json!({"formats": v, "proresProfiles": ProResProfile::ALL.iter().map(|p| p.label()).collect::<Vec<_>>()}))
use effectcraft_project::render_queue::{CodecProfile, VideoCodecOptions};
let level = |l: &u8| format!("{}.{}", l / 10, l % 10);
Ok(json!({
"formats": v,
"proresProfiles": ProResProfile::ALL.iter().map(|p| p.label()).collect::<Vec<_>>(),
"codecProfiles": CodecProfile::ALL.iter().map(|p| p.label()).collect::<Vec<_>>(),
"hevcLevels": VideoCodecOptions::HEVC_LEVELS.iter().map(level).collect::<Vec<_>>(),
"av1Levels": VideoCodecOptions::AV1_LEVELS.iter().map(level).collect::<Vec<_>>(),
"webmCodecs": ["vp9", "av1"],
"opusApplications": ["audio", "voice"],
}))
}
/// `item?|index?`, else the last item (the queue's newest entry).
@@ -540,7 +611,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Output Module Settings...",
[],
None,
"{item?|index?, module?: n (1 = first), format?, channels?: rgb|rgba, quality?: 1-100, bitrate?: kbps, proresProfile?: proxy|lt|standard|hq|4444|4444xq, audio?: auto|on|off, sampleRate?, loop?: bool, output?}",
"{item?|index?, module?: n (1 = first), format?: h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff, channels?: rgb|rgba, quality?: 1-100, bitrate?: kbps, proresProfile?: proxy|lt|standard|hq|4444|4444xq, profile?: main|main10 (HEVC/AV1), level?: auto|4.1, rateControl?: bitrate|quality, keyframeInterval?: frames (0 = auto), webmCodec?: vp9|av1, audioBitrate?: Opus kbps, opusApplication?: audio|voice, audio?: auto|on|off, sampleRate?, loop?: bool, output?}",
has_items,
set_output_module
),