M13.4: hevcenc / av1enc crate skeletons (API contract, layers)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f713eb9bfd
commit
3a8b401e81
|
|
@ -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"
|
||||
|
|
|
|||
11
Cargo.toml
11
Cargo.toml
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,8 @@ const LAYERS: &[(&str, u8)] = &[
|
|||
("vp9enc", 0),
|
||||
("testkit", 0),
|
||||
("opusenc", 0),
|
||||
("hevcenc", 0),
|
||||
("av1enc", 0),
|
||||
("raster", 1),
|
||||
("keyframe", 1),
|
||||
("path", 1),
|
||||
|
|
|
|||
Loading…
Reference in New Issue