Files
effectcraft/crates/export/src/hevc_av1.rs
T
JonRC 6d5e84e7a5 Export: H.264, HEVC and AV1 tag the frames' own colour (#2879)
Every MP4 export said BT.709 whatever the Output color space, in the
colr box and in the bitstream, so Rec. 2020, P3, PQ and HLG renders
were labelled as BT.709.

Move the ProRes colr mapping to output_codes and use it for every movie
path: the nclx colr box, FilmCraft's H.264 ColorConfig (VUI), and new
colour fields in effectcraft-hevcenc (VUI) and effectcraft-av1enc
(color_config), which wrote 1-1-1 as constants. The matrix stays BT.709,
the coefficients every path converts with. Default renders don't change.
2026-11-02 14:11:33 -03:00

212 lines
8.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! HEVC (`effectcraft-hevcenc`) and AV1 (`effectcraft-av1enc`) video encoders for the MP4
//! movie writer (`hvc1` / `av01` sample entries) and AV1 in WebM, configured from the Output
//! Module's [`VideoCodecOptions`].
use effectcraft_project::render_queue::{CodecProfile, OutputModule, RateControlMode, VideoCodecOptions};
use effectcraft_time::FrameRate;
use filmcraft_isobmff::{Av1Config, CodecConfig, FourCc, HevcConfig, SampleEntry};
use rayon::prelude::*;
use crate::encode::{Codes, Packet, VideoEncoder};
use crate::{ExportError, Result};
fn enc_err(e: impl std::fmt::Display) -> ExportError {
ExportError::Encode(e.to_string())
}
/// BT.709 limited-range 4:2:0 planes at `bits` (8 or 10) from RGBA8 (2×2 chroma average;
/// alpha ignored). Chroma planes are `ceil(w/2)` × `ceil(h/2)`.
pub(crate) fn rgba_to_yuv420_bits(rgba: &[u8], w: usize, h: usize, bits: u8, y: &mut Vec<u16>, u: &mut Vec<u16>, v: &mut Vec<u16>) {
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
let scale = (1u32 << (bits - 8)) as f32;
let (lo, hi) = (scale, 254.0 * scale + (scale - 1.0));
y.resize(w * h, 0);
u.resize(cw * ch, 0);
v.resize(cw * ch, 0);
y.par_chunks_mut(w * 2).zip(u.par_chunks_mut(cw).zip(v.par_chunks_mut(cw))).enumerate().for_each(|(cy, (yr, (ur, vr)))| {
let rows = yr.len() / w;
let mut acc = vec![(0f32, 0f32, 0f32); cw];
for dy in 0..rows {
let src = &rgba[(cy * 2 + dy) * w * 4..][..w * 4];
for x in 0..w {
let (r, g, b) = (src[x * 4] as f32 / 255.0, src[x * 4 + 1] as f32 / 255.0, src[x * 4 + 2] as f32 / 255.0);
let l = 0.2126 * r + 0.7152 * g + 0.0722 * b;
yr[dy * w + x] = ((16.0 + 219.0 * l) * scale).round().clamp(lo, hi) as u16;
let a = &mut acc[x / 2];
a.0 += (b - l) / 1.8556;
a.1 += (r - l) / 1.5748;
a.2 += 1.0;
}
}
for (cx, a) in acc.iter().enumerate() {
ur[cx] = ((128.0 + 224.0 * a.0 / a.2) * scale).round().clamp(lo, hi) as u16;
vr[cx] = ((128.0 + 224.0 * a.1 / a.2) * scale).round().clamp(lo, hi) as u16;
}
});
}
#[derive(Default)]
struct Planes {
y: Vec<u16>,
u: Vec<u16>,
v: Vec<u16>,
}
// ---------------------------------------------------------------- HEVC
pub(crate) struct Hevc {
enc: effectcraft_hevcenc::Encoder,
w: u32,
h: u32,
bits: u8,
codes: Codes,
planes: Planes,
}
/// The HEVC encoder settings for an output module.
pub(crate) fn hevc_config(w: u32, h: u32, rate: FrameRate, om: &OutputModule) -> effectcraft_hevcenc::EncoderConfig {
use effectcraft_hevcenc as he;
let o: &VideoCodecOptions = &om.codec;
let mut cfg = he::EncoderConfig::new(w, h, rate.num as u32, rate.den as u32);
cfg.profile = match o.profile {
CodecProfile::Main => he::Profile::Main,
CodecProfile::Main10 => he::Profile::Main10,
};
cfg.level_idc = o.hevc_level_idc();
cfg.rate = match o.rate_control {
RateControlMode::Bitrate => he::RateControl::Bitrate { kbps: om.bitrate_kbps.max(50) },
RateControlMode::Quality => he::RateControl::ConstantQp(o.hevc_qp()),
};
cfg.keyint = om.keyint(rate.as_f64());
cfg
}
impl Hevc {
pub(crate) fn new(w: u32, h: u32, rate: FrameRate, om: &OutputModule, codes: Codes) -> Result<Hevc> {
let mut cfg = hevc_config(w, h, rate, om);
(cfg.colour_primaries, cfg.transfer_characteristics, cfg.matrix_coeffs) = (codes.primaries, codes.transfer, codes.matrix);
let bits = cfg.profile.bit_depth();
let enc = effectcraft_hevcenc::Encoder::new(cfg).map_err(enc_err)?;
Ok(Hevc { enc, w, h, bits, codes, planes: Planes::default() })
}
}
impl VideoEncoder for Hevc {
fn sample_entry(&self) -> SampleEntry {
let cfg = HevcConfig::parse(&self.enc.hvcc()).unwrap_or_default();
let mut entry = SampleEntry::hevc(cfg, self.w as u16, self.h as u16);
crate::encode::tag_nclx_limited(&mut entry, self.codes);
entry
}
fn encode(&mut self, rgba: &[u8], _index: u64) -> Result<Vec<Packet>> {
let (w, h) = (self.w as usize, self.h as usize);
let p = &mut self.planes;
rgba_to_yuv420_bits(rgba, w, h, self.bits, &mut p.y, &mut p.u, &mut p.v);
let frame = effectcraft_hevcenc::Frame { y: &p.y, u: &p.u, v: &p.v, y_stride: w, uv_stride: w.div_ceil(2) };
let pk = self.enc.encode(&frame);
Ok(vec![Packet { data: pk.data, key: pk.keyframe, cto: 0 }])
}
fn flush(&mut self) -> Result<Vec<Packet>> {
Ok(vec![])
}
}
// ---------------------------------------------------------------- AV1
pub(crate) struct Av1 {
enc: effectcraft_av1enc::Encoder,
w: u32,
h: u32,
bits: u8,
codes: Codes,
planes: Planes,
}
/// The AV1 encoder settings for an output module.
pub(crate) fn av1_config(w: u32, h: u32, rate: FrameRate, om: &OutputModule) -> effectcraft_av1enc::EncoderConfig {
use effectcraft_av1enc as ae;
let o: &VideoCodecOptions = &om.codec;
let mut cfg = ae::EncoderConfig::new(w, h, rate.num as u32, rate.den as u32);
cfg.bit_depth = o.profile.bit_depth();
cfg.level_idx = o.av1_level_idx();
cfg.rate = match o.rate_control {
RateControlMode::Bitrate => ae::RateControl::Bitrate { kbps: om.bitrate_kbps.max(50) },
RateControlMode::Quality => ae::RateControl::ConstantQ(o.av1_qindex()),
};
cfg.keyint = om.keyint(rate.as_f64());
cfg
}
impl Av1 {
pub(crate) fn new(w: u32, h: u32, rate: FrameRate, om: &OutputModule, codes: Codes) -> Result<Av1> {
let mut cfg = av1_config(w, h, rate, om);
(cfg.color_primaries, cfg.transfer_characteristics, cfg.matrix_coefficients) = (codes.primaries, codes.transfer, codes.matrix);
let bits = cfg.bit_depth;
let enc = effectcraft_av1enc::Encoder::new(cfg).map_err(enc_err)?;
Ok(Av1 { enc, w, h, bits, codes, planes: Planes::default() })
}
/// The `av1C` record (also the WebM CodecPrivate).
pub(crate) fn av1c(&self) -> Vec<u8> {
self.enc.av1c()
}
}
impl VideoEncoder for Av1 {
fn sample_entry(&self) -> SampleEntry {
let cfg = Av1Config::parse(&self.enc.av1c()).unwrap_or_default();
let mut entry = SampleEntry::video(FourCc(*b"av01"), CodecConfig::Av1(cfg), self.w as u16, self.h as u16);
crate::encode::tag_nclx_limited(&mut entry, self.codes);
entry
}
fn encode(&mut self, rgba: &[u8], _index: u64) -> Result<Vec<Packet>> {
let (w, h) = (self.w as usize, self.h as usize);
let p = &mut self.planes;
rgba_to_yuv420_bits(rgba, w, h, self.bits, &mut p.y, &mut p.u, &mut p.v);
let frame = effectcraft_av1enc::Frame { y: &p.y, u: &p.u, v: &p.v, y_stride: w, uv_stride: w.div_ceil(2) };
let pk = self.enc.encode(&frame);
Ok(vec![Packet { data: pk.data, key: pk.keyframe, cto: 0 }])
}
fn flush(&mut self) -> Result<Vec<Packet>> {
Ok(vec![])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ten_bit_planes_scale_eight_bit_ones() {
let rgba = [255u8, 255, 255, 255, 0, 0, 0, 255, 255, 0, 0, 255, 0, 255, 0, 255];
let (mut y, mut u, mut v) = (vec![], vec![], vec![]);
rgba_to_yuv420_bits(&rgba, 2, 2, 8, &mut y, &mut u, &mut v);
assert_eq!((y[0], y[1]), (235, 16));
rgba_to_yuv420_bits(&rgba, 2, 2, 10, &mut y, &mut u, &mut v);
assert_eq!((y[0], y[1]), (940, 64));
assert_eq!(u.len(), 1);
}
#[test]
fn codec_options_map_to_encoder_settings() {
let mut om = OutputModule::for_format(effectcraft_project::render_queue::OutputFormat::Hevc);
om.codec.profile = CodecProfile::Main10;
om.codec.level = Some(41);
om.codec.rate_control = RateControlMode::Quality;
om.codec.quality = 100;
om.keyframe_interval = 12;
let rate = FrameRate::new(30, 1);
let h = hevc_config(64, 32, rate, &om);
assert_eq!(h.profile, effectcraft_hevcenc::Profile::Main10);
assert_eq!(h.level_idc, Some(123));
assert_eq!(h.rate, effectcraft_hevcenc::RateControl::ConstantQp(4));
assert_eq!(h.keyint, 12);
let a = av1_config(64, 32, rate, &om);
assert_eq!((a.bit_depth, a.level_idx, a.keyint), (10, Some(9), 12));
om.codec.rate_control = RateControlMode::Bitrate;
om.keyframe_interval = 0;
om.bitrate_kbps = 3000;
let a = av1_config(64, 32, rate, &om);
assert_eq!((a.rate, a.keyint), (effectcraft_av1enc::RateControl::Bitrate { kbps: 3000 }, 60));
}
}