M13.4: hevcenc / av1enc crate skeletons (API contract, layers)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-03 04:42:12 -04:00
parent f713eb9bfd
commit 3a8b401e81
7 changed files with 283 additions and 0 deletions

14
Cargo.lock generated
View File

@ -1164,6 +1164,13 @@ dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "effectcraft-av1enc"
version = "0.1.0"
dependencies = [
"filmcraft-av1",
]
[[package]]
name = "effectcraft-cli"
version = "0.1.0"
@ -1301,6 +1308,13 @@ dependencies = [
"wgpu",
]
[[package]]
name = "effectcraft-hevcenc"
version = "0.1.0"
dependencies = [
"filmcraft-hevc",
]
[[package]]
name = "effectcraft-host"
version = "0.1.0"

View File

@ -38,6 +38,8 @@ effectcraft-ui-egui = { path = "crates/ui-egui" }
effectcraft-automation = { path = "crates/automation" }
effectcraft-opusenc = { path = "crates/opusenc" }
effectcraft-script = { path = "crates/script" }
effectcraft-hevcenc = { path = "crates/hevcenc" }
effectcraft-av1enc = { path = "crates/av1enc" }
# foundation
thiserror = "2"
@ -76,6 +78,9 @@ filmcraft-isobmff = { git = "https://github.com/storytold/filmcraft", rev = "89c
# decoder used as the round-trip oracle of effectcraft-opusenc (crates/export dev-dependency)
filmcraft-opus = { git = "https://github.com/storytold/filmcraft", rev = "89cfbf24f698b0f0e538eb7795929f19bbfb1dbb" }
filmcraft-vp9 = { git = "https://github.com/storytold/filmcraft", rev = "89cfbf24f698b0f0e538eb7795929f19bbfb1dbb" }
# decoders used as the round-trip oracles of effectcraft-hevcenc / effectcraft-av1enc (dev-dependencies only)
filmcraft-hevc = { git = "https://github.com/storytold/filmcraft", rev = "89cfbf24f698b0f0e538eb7795929f19bbfb1dbb" }
filmcraft-av1 = { git = "https://github.com/storytold/filmcraft", rev = "89cfbf24f698b0f0e538eb7795929f19bbfb1dbb" }
# gpu (the same wgpu eframe / egui-wgpu use)
wgpu = { version = "30.0.1", features = ["fragile-send-sync-non-atomic-wasm"] }
@ -148,6 +153,12 @@ opt-level = 3
[profile.dev.package.effectcraft-vp9enc]
opt-level = 3
[profile.dev.package.effectcraft-hevcenc]
opt-level = 3
[profile.dev.package.effectcraft-av1enc]
opt-level = 3
# FilmCraft codec hot loops (same settings as FilmCraft's own workspace).
[profile.dev.package.filmcraft-h264]
opt-level = 3

15
crates/av1enc/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "effectcraft-av1enc"
description = "Clean-room pure-Rust AV1 encoder (Main profile, key and inter frames) for MP4/WebM export, from the AV1 Bitstream & Decoding Process Specification"
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[lints]
workspace = true
[dependencies]
[dev-dependencies]
filmcraft-av1 = { workspace = true }

98
crates/av1enc/src/lib.rs Normal file
View File

@ -0,0 +1,98 @@
//! Clean-room pure-Rust AV1 encoder — API skeleton (implementation in progress).
//!
//! The public API below is the contract `crates/export` codes against; keep it stable.
/// Rate control.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RateControl {
/// Constant quantizer index (`base_q_idx`, 1–255; lower is better).
ConstantQ(u8),
/// Average bitrate target in kbit/s (frame-level quantizer adaptation).
Bitrate { kbps: u32 },
}
/// Encoder settings. Main profile (seq_profile 0), 4:2:0.
#[derive(Clone, Debug, PartialEq)]
pub struct EncoderConfig {
pub width: u32,
pub height: u32,
pub fps_num: u32,
pub fps_den: u32,
/// 8 or 10.
pub bit_depth: u8,
/// `seq_level_idx` (e.g. 8 = level 4.0); `None` picks the lowest level that fits.
pub level_idx: Option<u8>,
pub rate: RateControl,
/// Frames between key frames (1 = all key frames).
pub keyint: u32,
/// BT.709 limited range is signalled unless this is set.
pub full_range: bool,
}
impl EncoderConfig {
pub fn new(width: u32, height: u32, fps_num: u32, fps_den: u32) -> Self {
EncoderConfig { width, height, fps_num, fps_den, bit_depth: 8, level_idx: None, rate: RateControl::ConstantQ(100), keyint: 60, full_range: false }
}
}
/// One input picture, 4:2:0, samples at the configured bit depth.
#[derive(Clone, Copy, Debug)]
pub struct Frame<'a> {
pub y: &'a [u16],
pub u: &'a [u16],
pub v: &'a [u16],
pub y_stride: usize,
pub uv_stride: usize,
}
/// One temporal unit as stored in MP4 / WebM samples: OBUs with `obu_has_size_field` = 1 and
/// no temporal delimiter (key frames start with the sequence header OBU).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Packet {
pub data: Vec<u8>,
pub keyframe: bool,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
InvalidConfig(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::InvalidConfig(s) => write!(f, "invalid AV1 encoder config: {s}"),
}
}
}
impl std::error::Error for Error {}
/// AV1 encoder. Frames come out in input order (no hidden/reordered frames).
pub struct Encoder {
cfg: EncoderConfig,
}
impl Encoder {
pub fn new(cfg: EncoderConfig) -> Result<Encoder, Error> {
if cfg.width == 0 || cfg.height == 0 {
return Err(Error::InvalidConfig("empty frame".into()));
}
Ok(Encoder { cfg })
}
pub fn config(&self) -> &EncoderConfig {
&self.cfg
}
/// The sequence header OBU (with size field).
pub fn sequence_header_obu(&self) -> Vec<u8> {
vec![]
}
/// AV1CodecConfigurationRecord (`av1C` payload, also the WebM CodecPrivate).
pub fn av1c(&self) -> Vec<u8> {
vec![]
}
/// Encodes one frame.
pub fn encode(&mut self, _frame: &Frame) -> Packet {
Packet { data: vec![], keyframe: true }
}
}

15
crates/hevcenc/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "effectcraft-hevcenc"
description = "Clean-room pure-Rust HEVC (H.265) encoder (Main / Main 10, I and P slices, CABAC) for MP4 export, from ITU-T H.265"
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[lints]
workspace = true
[dependencies]
[dev-dependencies]
filmcraft-hevc = { workspace = true }

128
crates/hevcenc/src/lib.rs Normal file
View File

@ -0,0 +1,128 @@
//! Clean-room pure-Rust HEVC (H.265) encoder — API skeleton (implementation in progress).
//!
//! The public API below is the contract `crates/export` codes against; keep it stable.
/// Profile written in the parameter sets.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Profile {
/// Main: 8-bit 4:2:0.
#[default]
Main,
/// Main 10: 10-bit 4:2:0.
Main10,
}
impl Profile {
pub fn bit_depth(self) -> u8 {
match self {
Profile::Main => 8,
Profile::Main10 => 10,
}
}
}
/// Rate control.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RateControl {
/// Constant QP (0–51; lower is better).
ConstantQp(u8),
/// Average bitrate target in kbit/s (frame-level QP adaptation).
Bitrate { kbps: u32 },
}
/// Encoder settings.
#[derive(Clone, Debug, PartialEq)]
pub struct EncoderConfig {
pub width: u32,
pub height: u32,
/// Frame rate numerator / denominator (VUI timing and rate control).
pub fps_num: u32,
pub fps_den: u32,
pub profile: Profile,
/// `general_level_idc` (30 × level, e.g. 93 = level 3.1); `None` picks the lowest level that fits.
pub level_idc: Option<u8>,
pub rate: RateControl,
/// Frames between IDR pictures (1 = all intra).
pub keyint: u32,
/// BT.709 limited range is signalled in the VUI unless this is set.
pub full_range: bool,
}
impl EncoderConfig {
pub fn new(width: u32, height: u32, fps_num: u32, fps_den: u32) -> Self {
EncoderConfig {
width,
height,
fps_num,
fps_den,
profile: Profile::Main,
level_idc: None,
rate: RateControl::ConstantQp(28),
keyint: 60,
full_range: false,
}
}
}
/// One input picture, 4:2:0, samples at the profile's bit depth (8-bit values 0–255 for Main).
#[derive(Clone, Copy, Debug)]
pub struct Frame<'a> {
pub y: &'a [u16],
pub u: &'a [u16],
pub v: &'a [u16],
pub y_stride: usize,
pub uv_stride: usize,
}
/// One coded picture (an access unit): NAL units, each prefixed by a 4-byte big-endian length
/// (ISO/IEC 14496-15 sample format; parameter sets are only in [`Encoder::hvcc`]).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Packet {
pub data: Vec<u8>,
/// IDR picture.
pub keyframe: bool,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
InvalidConfig(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::InvalidConfig(s) => write!(f, "invalid HEVC encoder config: {s}"),
}
}
}
impl std::error::Error for Error {}
/// HEVC encoder. Pictures come out in input order (no reordering: I and P only).
pub struct Encoder {
cfg: EncoderConfig,
}
impl Encoder {
pub fn new(cfg: EncoderConfig) -> Result<Encoder, Error> {
if cfg.width == 0 || cfg.height == 0 {
return Err(Error::InvalidConfig("empty frame".into()));
}
Ok(Encoder { cfg })
}
pub fn config(&self) -> &EncoderConfig {
&self.cfg
}
/// VPS, SPS, PPS NAL units (without start codes or length prefixes).
pub fn parameter_sets(&self) -> [Vec<u8>; 3] {
[vec![], vec![], vec![]]
}
/// HEVCDecoderConfigurationRecord (`hvcC` payload) with the parameter sets.
pub fn hvcc(&self) -> Vec<u8> {
vec![]
}
/// Encodes one picture.
pub fn encode(&mut self, _frame: &Frame) -> Packet {
Packet { data: vec![], keyframe: true }
}
}

View File

@ -26,6 +26,8 @@ const LAYERS: &[(&str, u8)] = &[
("vp9enc", 0),
("testkit", 0),
("opusenc", 0),
("hevcenc", 0),
("av1enc", 0),
("raster", 1),
("keyframe", 1),
("path", 1),