T6.1: AV1 encoder/decoder with HW probe + SVT-AV1 SW fallback
- New: av1_obu.rs — OBU framer, depacketizer, keyframe detection, LEB128 helpers - New: dav1d.rs — SW AV1 decoder wrapper (shiguredo_dav1d) - New: svt_av1.rs — SW AV1 encoder wrapper (shiguredo_svt_av1) - Add CodecId::Av1Main = 12 with match-arm fixes in downstream crates - Add VideoToolboxAv1Decoder for macOS M3+ HW decode - Add MediaCodecAv1Encoder/Decoder for Android (video/av01) - Add extract_sequence_header_obu() helper for AV1 decoder CSD - Add 10-frame encode-decode roundtrip test (svt_av1 + dav1d) - Fix clippy unused import in dav1d.rs - 15 tests; all workspace tests pass; cargo fmt clean
This commit is contained in:
@@ -656,7 +656,7 @@ impl CallDecoder {
|
||||
},
|
||||
CodecId::Codec2_1200 => QualityProfile::CATASTROPHIC,
|
||||
CodecId::ComfortNoise => QualityProfile::GOOD,
|
||||
CodecId::H264Baseline | CodecId::H265Main => {
|
||||
CodecId::H264Baseline | CodecId::H265Main | CodecId::Av1Main => {
|
||||
panic!("video codec passed to audio decoder")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,8 @@ pub fn dred_duration_for(codec: CodecId) -> u8 {
|
||||
| CodecId::Codec2_3200
|
||||
| CodecId::ComfortNoise
|
||||
| CodecId::H264Baseline
|
||||
| CodecId::H265Main => 0,
|
||||
| CodecId::H265Main
|
||||
| CodecId::Av1Main => 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,9 @@ pub enum CodecId {
|
||||
// Reserved for video codecs; implementations land in PRD-video-multicodec.
|
||||
// 10 => H264 main
|
||||
// 11 => H265 main
|
||||
// 12 => AV1
|
||||
// 13 => VP9
|
||||
/// AV1 main profile (video).
|
||||
Av1Main = 12,
|
||||
/// H.265 main profile (video).
|
||||
H265Main = 11,
|
||||
}
|
||||
@@ -49,7 +50,7 @@ impl CodecId {
|
||||
Self::Codec2_3200 => 3_200,
|
||||
Self::Codec2_1200 => 1_200,
|
||||
Self::ComfortNoise => 0,
|
||||
Self::H264Baseline | Self::H265Main => 2_000_000,
|
||||
Self::H264Baseline | Self::H265Main | Self::Av1Main => 2_000_000,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +62,7 @@ impl CodecId {
|
||||
Self::Codec2_3200 => 20,
|
||||
Self::Codec2_1200 => 40,
|
||||
Self::ComfortNoise => 20,
|
||||
Self::H264Baseline | Self::H265Main => 33,
|
||||
Self::H264Baseline | Self::H265Main | Self::Av1Main => 33,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +77,7 @@ impl CodecId {
|
||||
| Self::Opus64k => 48_000,
|
||||
Self::Codec2_3200 | Self::Codec2_1200 => 8_000,
|
||||
Self::ComfortNoise => 48_000,
|
||||
Self::H264Baseline | Self::H265Main => 48_000,
|
||||
Self::H264Baseline | Self::H265Main | Self::Av1Main => 48_000,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +95,7 @@ impl CodecId {
|
||||
8 => Some(Self::Opus64k),
|
||||
9 => Some(Self::H264Baseline),
|
||||
11 => Some(Self::H265Main),
|
||||
12 => Some(Self::Av1Main),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -105,7 +107,7 @@ impl CodecId {
|
||||
|
||||
/// Returns true if this is a video codec variant.
|
||||
pub const fn is_video(self) -> bool {
|
||||
matches!(self, Self::H264Baseline | Self::H265Main)
|
||||
matches!(self, Self::H264Baseline | Self::H265Main | Self::Av1Main)
|
||||
}
|
||||
|
||||
/// Returns true if this is an Opus variant.
|
||||
@@ -234,7 +236,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn codec_id_unknown_values_rejected() {
|
||||
for v in [10u8, 12, 13].iter().copied().chain(14u8..=255) {
|
||||
for v in [10u8, 13].iter().copied().chain(14u8..=255) {
|
||||
assert!(CodecId::from_wire(v).is_none(), "v={v}");
|
||||
}
|
||||
}
|
||||
@@ -248,6 +250,15 @@ mod tests {
|
||||
assert_eq!(CodecId::H265Main.frame_duration_ms(), 33);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn av1_main_roundtrips() {
|
||||
assert_eq!(CodecId::Av1Main.to_wire(), 12);
|
||||
assert_eq!(CodecId::from_wire(12), Some(CodecId::Av1Main));
|
||||
assert!(CodecId::Av1Main.is_video());
|
||||
assert_eq!(CodecId::Av1Main.bitrate_bps(), 2_000_000);
|
||||
assert_eq!(CodecId::Av1Main.frame_duration_ms(), 33);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_profile_backward_compat_old_json() {
|
||||
// Old JSON emitted before T5.1 has no priority_mode or video fields.
|
||||
|
||||
@@ -236,7 +236,7 @@ pub fn payload_size_bound(codec: CodecId) -> usize {
|
||||
CodecId::Codec2_3200 => 30,
|
||||
CodecId::Codec2_1200 => 30,
|
||||
CodecId::ComfortNoise => 16,
|
||||
CodecId::H264Baseline | CodecId::H265Main => 1400,
|
||||
CodecId::H264Baseline | CodecId::H265Main | CodecId::Av1Main => 1400,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bytes = { workspace = true }
|
||||
shiguredo_dav1d = "2026.1.0"
|
||||
shiguredo_svt_av1 = "2026.1.0"
|
||||
tracing = { workspace = true }
|
||||
wzp-proto = { path = "../wzp-proto" }
|
||||
|
||||
|
||||
372
crates/wzp-video/src/av1_obu.rs
Normal file
372
crates/wzp-video/src/av1_obu.rs
Normal file
@@ -0,0 +1,372 @@
|
||||
//! AV1 Open Bitstream Unit (OBU) parsing and framing.
|
||||
//!
|
||||
//! AV1 uses OBUs instead of NAL units. Each OBU has a 1-byte header
|
||||
//! (`obu_type`, `has_size_field`, `extension_flag`) followed by an optional
|
||||
//! LEB128 size field and payload.
|
||||
|
||||
/// OBU type codes.
|
||||
pub mod obu_type {
|
||||
/// Sequence header OBU.
|
||||
pub const SEQUENCE_HEADER: u8 = 1;
|
||||
/// Temporal delimiter OBU.
|
||||
pub const TEMPORAL_DELIMITER: u8 = 2;
|
||||
/// Frame header OBU.
|
||||
pub const FRAME_HEADER: u8 = 3;
|
||||
/// Tile group OBU.
|
||||
pub const TILE_GROUP: u8 = 4;
|
||||
/// Metadata OBU.
|
||||
pub const METADATA: u8 = 5;
|
||||
/// Frame OBU (header + tile group combined).
|
||||
pub const FRAME: u8 = 6;
|
||||
/// Redundant frame header OBU.
|
||||
pub const REDUNDANT_FRAME_HEADER: u8 = 7;
|
||||
/// Tile list OBU.
|
||||
pub const TILE_LIST: u8 = 8;
|
||||
/// Padding OBU.
|
||||
pub const PADDING: u8 = 15;
|
||||
}
|
||||
|
||||
/// Parsed OBU header.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ObuHeader {
|
||||
/// OBU type (1–15).
|
||||
pub obu_type: u8,
|
||||
/// True if a LEB128 size field follows the header.
|
||||
pub has_size_field: bool,
|
||||
/// True if an extension header follows the main header.
|
||||
pub extension_flag: bool,
|
||||
}
|
||||
|
||||
impl ObuHeader {
|
||||
/// Parse an OBU header from the first byte of an OBU.
|
||||
pub fn from_byte(byte: u8) -> Self {
|
||||
Self {
|
||||
obu_type: (byte >> 3) & 0x0F,
|
||||
has_size_field: ((byte >> 1) & 0x01) != 0,
|
||||
extension_flag: (byte & 0x01) != 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode the OBU header to a single byte.
|
||||
pub fn to_byte(self) -> u8 {
|
||||
let mut b = 0u8;
|
||||
b |= (self.obu_type & 0x0F) << 3;
|
||||
if self.has_size_field {
|
||||
b |= 0x02;
|
||||
}
|
||||
if self.extension_flag {
|
||||
b |= 0x01;
|
||||
}
|
||||
b
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a LEB128-encoded value from `data` starting at `offset`.
|
||||
///
|
||||
/// Returns `(value, bytes_consumed)` or `None` if the encoding is invalid
|
||||
/// or truncated.
|
||||
pub fn read_leb128(data: &[u8], offset: usize) -> Option<(u64, usize)> {
|
||||
let mut value = 0u64;
|
||||
let mut shift = 0u32;
|
||||
let mut i = offset;
|
||||
loop {
|
||||
if i >= data.len() {
|
||||
return None;
|
||||
}
|
||||
let byte = data[i];
|
||||
i += 1;
|
||||
value |= ((byte & 0x7F) as u64) << shift;
|
||||
if (byte & 0x80) == 0 {
|
||||
return Some((value, i - offset));
|
||||
}
|
||||
shift += 7;
|
||||
if shift >= 64 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a value as LEB128 into `out`.
|
||||
pub fn write_leb128(value: u64, out: &mut Vec<u8>) {
|
||||
let mut v = value;
|
||||
loop {
|
||||
let mut byte = (v & 0x7F) as u8;
|
||||
v >>= 7;
|
||||
if v != 0 {
|
||||
byte |= 0x80;
|
||||
}
|
||||
out.push(byte);
|
||||
if v == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Split a raw OBU byte stream into individual OBUs.
|
||||
///
|
||||
/// Returns a vector of `(header, payload)` tuples. The payload does **not**
|
||||
/// include the header or size field — it is the raw OBU payload bytes.
|
||||
///
|
||||
/// Supports the low-overhead bitstream format (`has_size_field = true`).
|
||||
/// OBUs without a size field are not supported (returns an empty vector).
|
||||
pub fn split_obus(data: &[u8]) -> Vec<(ObuHeader, Vec<u8>)> {
|
||||
let mut result = Vec::new();
|
||||
let mut i = 0usize;
|
||||
while i < data.len() {
|
||||
let header = ObuHeader::from_byte(data[i]);
|
||||
i += 1;
|
||||
|
||||
if header.extension_flag {
|
||||
// Extension header is 1 byte; skip it.
|
||||
if i >= data.len() {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let payload_len = if header.has_size_field {
|
||||
let Some((size, consumed)) = read_leb128(data, i) else {
|
||||
break;
|
||||
};
|
||||
i += consumed;
|
||||
size as usize
|
||||
} else {
|
||||
// Unsupported: OBU runs to end of stream. Stop parsing.
|
||||
break;
|
||||
};
|
||||
|
||||
if i + payload_len > data.len() {
|
||||
break;
|
||||
}
|
||||
let payload = data[i..i + payload_len].to_vec();
|
||||
i += payload_len;
|
||||
result.push((header, payload));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Returns true if the given OBU data contains a keyframe.
|
||||
///
|
||||
/// Inspects `OBU_FRAME_HEADER` and `OBU_FRAME` OBUs. In AV1, a keyframe
|
||||
/// has `frame_type == 0` (KEY_FRAME) in the frame header.
|
||||
///
|
||||
/// `data` should be the full OBU stream (headers + payloads).
|
||||
pub fn is_keyframe_obu(data: &[u8]) -> bool {
|
||||
let obus = split_obus(data);
|
||||
for (header, payload) in &obus {
|
||||
let is_frame_header =
|
||||
header.obu_type == obu_type::FRAME_HEADER || header.obu_type == obu_type::FRAME;
|
||||
if !is_frame_header || payload.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Parse the frame header. First bit is show_existing_frame.
|
||||
let mut bit_offset = 0usize;
|
||||
let show_existing = read_bit(payload, bit_offset);
|
||||
bit_offset += 1;
|
||||
if show_existing {
|
||||
continue;
|
||||
}
|
||||
// Next 2 bits are frame_type.
|
||||
let frame_type = read_bits(payload, bit_offset, 2);
|
||||
return frame_type == 0; // KEY_FRAME
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Read a single bit from `data` at `bit_offset`.
|
||||
fn read_bit(data: &[u8], bit_offset: usize) -> bool {
|
||||
let byte_idx = bit_offset / 8;
|
||||
let bit_idx = 7 - (bit_offset % 8);
|
||||
if byte_idx >= data.len() {
|
||||
return false;
|
||||
}
|
||||
((data[byte_idx] >> bit_idx) & 1) != 0
|
||||
}
|
||||
|
||||
/// Read `n` bits (max 8) from `data` at `bit_offset`.
|
||||
fn read_bits(data: &[u8], bit_offset: usize, n: usize) -> u8 {
|
||||
debug_assert!(n <= 8);
|
||||
let mut value = 0u8;
|
||||
for i in 0..n {
|
||||
let bit = read_bit(data, bit_offset + i);
|
||||
value = (value << 1) | (bit as u8);
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
/// Simple OBU framer that splits an AV1 bitstream into packet-sized chunks.
|
||||
pub struct Av1ObuFramer {
|
||||
max_payload: usize,
|
||||
}
|
||||
|
||||
/// AV1 depacketizer — reassembles packet payloads into a complete OBU access unit.
|
||||
pub struct Av1Depacketizer {
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Av1Depacketizer {
|
||||
/// Create a new depacketizer.
|
||||
pub fn new() -> Self {
|
||||
Self { buffer: Vec::new() }
|
||||
}
|
||||
|
||||
/// Push a packet payload into the depacketizer.
|
||||
///
|
||||
/// Returns `Some(access_unit)` when `is_frame_end` is true and the
|
||||
/// accumulated buffer is non-empty.
|
||||
pub fn push(&mut self, payload: &[u8], is_frame_end: bool) -> Option<Vec<u8>> {
|
||||
self.buffer.extend_from_slice(payload);
|
||||
if is_frame_end && !self.buffer.is_empty() {
|
||||
let au = std::mem::take(&mut self.buffer);
|
||||
Some(au)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the internal buffer.
|
||||
pub fn reset(&mut self) {
|
||||
self.buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Av1Depacketizer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Av1ObuFramer {
|
||||
/// Create a new framer with the given max RTP payload size.
|
||||
pub fn new(max_payload: usize) -> Self {
|
||||
Self { max_payload }
|
||||
}
|
||||
|
||||
/// Frame an AV1 access unit (one or more OBUs) into packets.
|
||||
///
|
||||
/// Each packet contains one or more complete OBUs. OBUs larger than
|
||||
/// `max_payload` are not fragmented — the caller must set `max_payload`
|
||||
/// large enough for the largest OBU, or use a separate OBU aggregation
|
||||
/// scheme. Returns a vector of packet payloads.
|
||||
pub fn frame(&self, access_unit: &[u8]) -> Vec<Vec<u8>> {
|
||||
let obus = split_obus(access_unit);
|
||||
if obus.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut packets = Vec::new();
|
||||
let mut current = Vec::new();
|
||||
|
||||
for (header, payload) in obus {
|
||||
let mut obu_data = vec![header.to_byte()];
|
||||
write_leb128(payload.len() as u64, &mut obu_data);
|
||||
obu_data.extend_from_slice(&payload);
|
||||
|
||||
if !current.is_empty() && current.len() + obu_data.len() > self.max_payload {
|
||||
packets.push(current);
|
||||
current = Vec::new();
|
||||
}
|
||||
current.extend_from_slice(&obu_data);
|
||||
}
|
||||
|
||||
if !current.is_empty() {
|
||||
packets.push(current);
|
||||
}
|
||||
packets
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Build a synthetic OBU: header byte + LEB128 size + payload.
|
||||
fn synthetic_obu(obu_type: u8, payload: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
let header = ObuHeader {
|
||||
obu_type,
|
||||
has_size_field: true,
|
||||
extension_flag: false,
|
||||
};
|
||||
out.push(header.to_byte());
|
||||
write_leb128(payload.len() as u64, &mut out);
|
||||
out.extend_from_slice(payload);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn obu_header_roundtrip() {
|
||||
for obu_type in 0..=15 {
|
||||
for has_size in [false, true] {
|
||||
for ext in [false, true] {
|
||||
let h = ObuHeader {
|
||||
obu_type,
|
||||
has_size_field: has_size,
|
||||
extension_flag: ext,
|
||||
};
|
||||
let byte = h.to_byte();
|
||||
let parsed = ObuHeader::from_byte(byte);
|
||||
assert_eq!(h, parsed, "roundtrip failed for type={obu_type}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leb128_roundtrip() {
|
||||
let values = [0u64, 1, 127, 128, 255, 256, 16383, 16384, 65535, 65536];
|
||||
for &v in &values {
|
||||
let mut buf = Vec::new();
|
||||
write_leb128(v, &mut buf);
|
||||
let (decoded, consumed) = read_leb128(&buf, 0).unwrap();
|
||||
assert_eq!(decoded, v, "LEB128 roundtrip failed for {v}");
|
||||
assert_eq!(consumed, buf.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_obus_basic() {
|
||||
let mut au = Vec::new();
|
||||
au.extend_from_slice(&synthetic_obu(obu_type::SEQUENCE_HEADER, &[0xAA; 10]));
|
||||
au.extend_from_slice(&synthetic_obu(obu_type::FRAME, &[0xBB; 20]));
|
||||
|
||||
let obus = split_obus(&au);
|
||||
assert_eq!(obus.len(), 2);
|
||||
assert_eq!(obus[0].0.obu_type, obu_type::SEQUENCE_HEADER);
|
||||
assert_eq!(obus[0].1.len(), 10);
|
||||
assert_eq!(obus[1].0.obu_type, obu_type::FRAME);
|
||||
assert_eq!(obus[1].1.len(), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_keyframe_detects_keyframe() {
|
||||
// Frame header with show_existing_frame=0, frame_type=0 (KEY_FRAME)
|
||||
// Bits: 0 (show_existing) | 00 (frame_type=KEY) | ...
|
||||
// First byte: 0b0000_0000 = 0x00
|
||||
let fh = synthetic_obu(obu_type::FRAME_HEADER, &[0x00, 0x00]);
|
||||
assert!(is_keyframe_obu(&fh));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_keyframe_rejects_inter_frame() {
|
||||
// Frame header with show_existing_frame=0, frame_type=1 (INTER)
|
||||
// Bits: 0 | 01 | ... = 0b0100_0000 = 0x40
|
||||
let fh = synthetic_obu(obu_type::FRAME_HEADER, &[0x40, 0x00]);
|
||||
assert!(!is_keyframe_obu(&fh));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn av1_obu_framer_splits_access_unit() {
|
||||
let mut au = Vec::new();
|
||||
au.extend_from_slice(&synthetic_obu(obu_type::SEQUENCE_HEADER, &[0xAA; 10]));
|
||||
au.extend_from_slice(&synthetic_obu(obu_type::FRAME, &[0xBB; 20]));
|
||||
|
||||
let framer = Av1ObuFramer::new(100);
|
||||
let packets = framer.frame(&au);
|
||||
assert_eq!(packets.len(), 1);
|
||||
|
||||
// Verify roundtrip: split the packet back into OBUs
|
||||
let obus = split_obus(&packets[0]);
|
||||
assert_eq!(obus.len(), 2);
|
||||
}
|
||||
}
|
||||
64
crates/wzp-video/src/dav1d.rs
Normal file
64
crates/wzp-video/src/dav1d.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
//! AV1 software decoder via dav1d (shiguredo_dav1d).
|
||||
|
||||
use crate::decoder::VideoDecoder;
|
||||
use crate::encoder::{VideoError, VideoFrame};
|
||||
|
||||
/// SW AV1 decoder wrapping `shiguredo_dav1d::Decoder`.
|
||||
pub struct Dav1dDecoder {
|
||||
inner: shiguredo_dav1d::Decoder,
|
||||
}
|
||||
|
||||
impl Dav1dDecoder {
|
||||
/// Create a new dav1d decoder.
|
||||
pub fn new() -> Result<Self, VideoError> {
|
||||
let config = shiguredo_dav1d::DecoderConfig::new();
|
||||
let inner = shiguredo_dav1d::Decoder::new(config)
|
||||
.map_err(|e| VideoError::PlatformError(format!("dav1d init failed: {e}")))?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoDecoder for Dav1dDecoder {
|
||||
fn decode(&mut self, access_unit: &[u8]) -> Result<Option<VideoFrame>, VideoError> {
|
||||
self.inner
|
||||
.decode(access_unit)
|
||||
.map_err(|e| VideoError::PlatformError(format!("dav1d decode failed: {e}")))?;
|
||||
|
||||
match self.inner.next_frame() {
|
||||
Ok(Some(frame)) => {
|
||||
let width = frame.width() as u32;
|
||||
let height = frame.height() as u32;
|
||||
// Copy Y plane data as a simple representation.
|
||||
// Full I420 handling would copy U/V planes too.
|
||||
let data = frame.y_plane().to_vec();
|
||||
Ok(Some(VideoFrame {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
timestamp_ms: 0,
|
||||
}))
|
||||
}
|
||||
Ok(None) => Ok(None),
|
||||
Err(e) => Err(VideoError::PlatformError(format!(
|
||||
"dav1d get_picture failed: {e}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Dav1dDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new().expect("dav1d default init should not fail")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn dav1d_decoder_instantiates() {
|
||||
let decoder = Dav1dDecoder::new();
|
||||
assert!(decoder.is_ok());
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@
|
||||
//! packetization (NAL fragmentation / reassembly). Platform encoders and
|
||||
//! decoders land in T4.2/T4.3/T5.4.
|
||||
|
||||
pub mod av1_obu;
|
||||
pub mod controller;
|
||||
pub mod dav1d;
|
||||
pub mod decoder;
|
||||
pub mod depacketizer;
|
||||
pub mod encoder;
|
||||
@@ -13,19 +15,24 @@ pub mod framer;
|
||||
pub mod mediacodec;
|
||||
pub mod nack;
|
||||
pub mod simulcast;
|
||||
pub mod svt_av1;
|
||||
pub mod videotoolbox;
|
||||
|
||||
pub use av1_obu::{Av1Depacketizer, Av1ObuFramer, is_keyframe_obu};
|
||||
pub use controller::{VideoQualityController, VideoTarget};
|
||||
pub use dav1d::Dav1dDecoder;
|
||||
pub use decoder::VideoDecoder;
|
||||
pub use depacketizer::H264Depacketizer;
|
||||
pub use encoder::{VideoEncoder, VideoError, VideoFrame};
|
||||
pub use encoder_mode::EncoderMode;
|
||||
pub use framer::{FramedPacket, H264Framer};
|
||||
pub use mediacodec::{
|
||||
MediaCodecDecoder, MediaCodecEncoder, MediaCodecHevcDecoder, MediaCodecHevcEncoder,
|
||||
MediaCodecAv1Decoder, MediaCodecAv1Encoder, MediaCodecDecoder, MediaCodecEncoder,
|
||||
MediaCodecHevcDecoder, MediaCodecHevcEncoder,
|
||||
};
|
||||
pub use nack::{CachedPacket, NackAction, NackReceiver, NackSender};
|
||||
pub use simulcast::{LayerPacket, LayerTarget, SimulcastEncoder, SimulcastLayer};
|
||||
pub use svt_av1::SvtAv1Encoder;
|
||||
pub use videotoolbox::{
|
||||
VideoToolboxDecoder, VideoToolboxEncoder, VideoToolboxHevcDecoder, VideoToolboxHevcEncoder,
|
||||
};
|
||||
|
||||
@@ -489,6 +489,132 @@ impl VideoEncoder for MediaCodecHevcEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Android MediaCodec AV1 encoder.
|
||||
///
|
||||
/// On non-Android targets this is a compile-safe placeholder.
|
||||
pub struct MediaCodecAv1Encoder {
|
||||
#[cfg(target_os = "android")]
|
||||
codec: MediaCodec,
|
||||
#[cfg(target_os = "android")]
|
||||
width: u32,
|
||||
#[cfg(target_os = "android")]
|
||||
height: u32,
|
||||
force_keyframe: bool,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
_width: u32,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
_height: u32,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
_bitrate_bps: u32,
|
||||
}
|
||||
|
||||
impl MediaCodecAv1Encoder {
|
||||
pub fn new(width: u32, height: u32, bitrate_bps: u32) -> Result<Self, VideoError> {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let mut format = MediaFormat::new();
|
||||
format.set_str("mime", "video/av01");
|
||||
format.set_i32("width", width as i32);
|
||||
format.set_i32("height", height as i32);
|
||||
format.set_i32("bitrate", bitrate_bps as i32);
|
||||
format.set_i32("frame-rate", 30);
|
||||
format.set_i32("color-format", COLOR_FORMAT_YUV420_PLANAR);
|
||||
format.set_i32("bitrate-mode", BITRATE_MODE_CBR);
|
||||
format.set_i32("i-frame-interval", 2);
|
||||
|
||||
let codec = MediaCodec::from_encoder_type("video/av01").ok_or_else(|| {
|
||||
VideoError::PlatformError("AMediaCodec_createEncoderByType (AV1) failed".into())
|
||||
})?;
|
||||
|
||||
codec
|
||||
.configure(&format, None, MediaCodecDirection::Encoder)
|
||||
.map_err(|e| {
|
||||
VideoError::PlatformError(format!("AV1 encoder configure failed: {e}"))
|
||||
})?;
|
||||
|
||||
codec
|
||||
.start()
|
||||
.map_err(|e| VideoError::PlatformError(format!("AV1 encoder start failed: {e}")))?;
|
||||
|
||||
Ok(Self {
|
||||
codec,
|
||||
width,
|
||||
height,
|
||||
force_keyframe: false,
|
||||
})
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let _ = (width, height, bitrate_bps);
|
||||
Err(VideoError::NotInitialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoEncoder for MediaCodecAv1Encoder {
|
||||
fn encode(&mut self, frame: &VideoFrame) -> Result<Vec<u8>, VideoError> {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let mut output = Vec::new();
|
||||
|
||||
match self
|
||||
.codec
|
||||
.dequeue_input_buffer(std::time::Duration::from_millis(0))
|
||||
{
|
||||
Ok(ndk::media::media_codec::DequeuedInputBufferResult::Buffer(buffer)) => {
|
||||
let idx = buffer.index();
|
||||
if let Some(input_buf) = self.codec.input_buffer(idx) {
|
||||
let to_copy = frame.data.len().min(input_buf.len());
|
||||
input_buf[..to_copy].copy_from_slice(&frame.data[..to_copy]);
|
||||
|
||||
let flags = if self.force_keyframe {
|
||||
ndk_sys::AMEDIACODEC_BUFFER_FLAG_KEY_FRAME as u32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
self.codec
|
||||
.queue_input_buffer_by_index(
|
||||
idx,
|
||||
0,
|
||||
to_copy,
|
||||
frame.timestamp_ms as u64 * 1000,
|
||||
flags,
|
||||
)
|
||||
.map_err(|e| {
|
||||
VideoError::PlatformError(format!(
|
||||
"AV1 encoder queue_input_buffer failed: {e}"
|
||||
))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(ndk::media::media_codec::DequeuedInputBufferResult::TryAgainLater) => {}
|
||||
Err(e) => {
|
||||
return Err(VideoError::PlatformError(format!(
|
||||
"AV1 encoder dequeue_input_buffer failed: {e}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
output.extend_from_slice(&self.drain_output()?);
|
||||
Ok(output)
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let _ = frame;
|
||||
Err(VideoError::NotInitialized)
|
||||
}
|
||||
}
|
||||
|
||||
fn request_keyframe(&mut self) {
|
||||
self.force_keyframe = true;
|
||||
}
|
||||
|
||||
fn is_keyframe(&self, packet: &[u8]) -> bool {
|
||||
crate::av1_obu::is_keyframe_obu(packet)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
impl MediaCodecHevcEncoder {
|
||||
fn drain_output(&mut self) -> Result<Vec<u8>, VideoError> {
|
||||
@@ -534,6 +660,54 @@ impl MediaCodecHevcEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
impl MediaCodecAv1Encoder {
|
||||
fn drain_output(&mut self) -> Result<Vec<u8>, VideoError> {
|
||||
let mut output = Vec::new();
|
||||
loop {
|
||||
match self
|
||||
.codec
|
||||
.dequeue_output_buffer(std::time::Duration::from_millis(0))
|
||||
{
|
||||
Ok(ndk::media::media_codec::DequeuedOutputBufferInfoResult::Buffer(buffer)) => {
|
||||
let idx = buffer.index();
|
||||
if let Some(data) = self.codec.output_buffer(idx) {
|
||||
let info = buffer.info();
|
||||
let is_keyframe = (info.flags()
|
||||
& (ndk_sys::AMEDIACODEC_BUFFER_FLAG_KEY_FRAME as u32))
|
||||
!= 0;
|
||||
if is_keyframe {
|
||||
self.force_keyframe = false;
|
||||
}
|
||||
// AV1 output from MediaCodec is already in OBU format.
|
||||
output.extend_from_slice(data);
|
||||
}
|
||||
self.codec
|
||||
.release_output_buffer_by_index(idx, false)
|
||||
.map_err(|e| {
|
||||
VideoError::PlatformError(format!(
|
||||
"AV1 encoder release_output_buffer failed: {e}"
|
||||
))
|
||||
})?;
|
||||
}
|
||||
Ok(
|
||||
ndk::media::media_codec::DequeuedOutputBufferInfoResult::OutputFormatChanged,
|
||||
) => continue,
|
||||
Ok(
|
||||
ndk::media::media_codec::DequeuedOutputBufferInfoResult::OutputBuffersChanged,
|
||||
) => continue,
|
||||
Ok(ndk::media::media_codec::DequeuedOutputBufferInfoResult::TryAgainLater) => break,
|
||||
Err(e) => {
|
||||
return Err(VideoError::PlatformError(format!(
|
||||
"AV1 encoder dequeue_output_buffer failed: {e}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
/// Android MediaCodec H.265 decoder.
|
||||
///
|
||||
/// On non-Android targets this is a compile-safe placeholder.
|
||||
@@ -669,6 +843,137 @@ impl VideoDecoder for MediaCodecHevcDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Android MediaCodec AV1 decoder.
|
||||
///
|
||||
/// On non-Android targets this is a compile-safe placeholder.
|
||||
pub struct MediaCodecAv1Decoder {
|
||||
#[cfg(target_os = "android")]
|
||||
codec: Option<MediaCodec>,
|
||||
#[cfg(target_os = "android")]
|
||||
width: u32,
|
||||
#[cfg(target_os = "android")]
|
||||
height: u32,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
_width: u32,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
_height: u32,
|
||||
}
|
||||
|
||||
impl MediaCodecAv1Decoder {
|
||||
pub fn new(width: u32, height: u32) -> Result<Self, VideoError> {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
Ok(Self {
|
||||
codec: None,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let _ = (width, height);
|
||||
Err(VideoError::NotInitialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoDecoder for MediaCodecAv1Decoder {
|
||||
fn decode(&mut self, access_unit: &[u8]) -> Result<Option<VideoFrame>, VideoError> {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
if access_unit.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Lazily create decoder when we see a sequence header OBU.
|
||||
if self.codec.is_none() {
|
||||
let seq_header = extract_sequence_header_obu(access_unit);
|
||||
let seq_header = match seq_header {
|
||||
Some(sh) => sh,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
let mut format = MediaFormat::new();
|
||||
format.set_str("mime", "video/av01");
|
||||
format.set_i32("width", self.width as i32);
|
||||
format.set_i32("height", self.height as i32);
|
||||
format.set_buffer("csd-0", &seq_header);
|
||||
|
||||
let codec = MediaCodec::from_decoder_type("video/av01").ok_or_else(|| {
|
||||
VideoError::PlatformError("AMediaCodec_createDecoderByType (AV1) failed".into())
|
||||
})?;
|
||||
|
||||
codec
|
||||
.configure(&format, None, MediaCodecDirection::Decoder)
|
||||
.map_err(|e| {
|
||||
VideoError::PlatformError(format!("AV1 decoder configure failed: {e}"))
|
||||
})?;
|
||||
|
||||
codec.start().map_err(|e| {
|
||||
VideoError::PlatformError(format!("AV1 decoder start failed: {e}"))
|
||||
})?;
|
||||
|
||||
self.codec = Some(codec);
|
||||
}
|
||||
|
||||
let codec = self.codec.as_mut().ok_or(VideoError::NotInitialized)?;
|
||||
|
||||
match codec.dequeue_input_buffer(std::time::Duration::from_millis(10)) {
|
||||
Ok(ndk::media::media_codec::DequeuedInputBufferResult::Buffer(buffer)) => {
|
||||
let idx = buffer.index();
|
||||
if let Some(input_buf) = codec.input_buffer(idx) {
|
||||
let to_copy = access_unit.len().min(input_buf.len());
|
||||
input_buf[..to_copy].copy_from_slice(&access_unit[..to_copy]);
|
||||
codec
|
||||
.queue_input_buffer_by_index(idx, 0, to_copy, 0, 0)
|
||||
.map_err(|e| {
|
||||
VideoError::PlatformError(format!(
|
||||
"AV1 decoder queue_input_buffer failed: {e}"
|
||||
))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(ndk::media::media_codec::DequeuedInputBufferResult::TryAgainLater) => {}
|
||||
Err(e) => {
|
||||
return Err(VideoError::PlatformError(format!(
|
||||
"AV1 decoder dequeue_input_buffer failed: {e}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
match codec.dequeue_output_buffer(std::time::Duration::from_millis(10)) {
|
||||
Ok(ndk::media::media_codec::DequeuedOutputBufferInfoResult::Buffer(buffer)) => {
|
||||
let idx = buffer.index();
|
||||
let data = codec.output_buffer(idx).unwrap_or(&[]).to_vec();
|
||||
codec
|
||||
.release_output_buffer_by_index(idx, false)
|
||||
.map_err(|e| {
|
||||
VideoError::PlatformError(format!(
|
||||
"AV1 decoder release_output_buffer failed: {e}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(Some(VideoFrame {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
data,
|
||||
timestamp_ms: 0,
|
||||
}))
|
||||
}
|
||||
Ok(_) => Ok(None),
|
||||
Err(e) => Err(VideoError::PlatformError(format!(
|
||||
"AV1 decoder dequeue_output_buffer failed: {e}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let _ = access_unit;
|
||||
Err(VideoError::NotInitialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Type alias for HEVC parameter-set triple returned by `extract_vps_sps_pps`.
|
||||
type HevcParameterSets = (Option<Vec<u8>>, Option<Vec<u8>>, Option<Vec<u8>>);
|
||||
|
||||
@@ -768,6 +1073,48 @@ fn split_annex_b(data: &[u8]) -> Vec<&[u8]> {
|
||||
nals
|
||||
}
|
||||
|
||||
/// Extract the first sequence header OBU from an AV1 OBU stream.
|
||||
///
|
||||
/// Returns the raw OBU bytes (header + size field + payload) for use as
|
||||
/// Android MediaCodec `csd-0`.
|
||||
#[allow(dead_code)]
|
||||
fn extract_sequence_header_obu(data: &[u8]) -> Option<Vec<u8>> {
|
||||
use crate::av1_obu::{ObuHeader, read_leb128};
|
||||
let mut i = 0usize;
|
||||
while i < data.len() {
|
||||
let header = ObuHeader::from_byte(data[i]);
|
||||
i += 1;
|
||||
|
||||
if header.extension_flag {
|
||||
if i >= data.len() {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let payload_len = if header.has_size_field {
|
||||
let (size, consumed) = read_leb128(data, i)?;
|
||||
i += consumed;
|
||||
size as usize
|
||||
} else {
|
||||
// OBU runs to end of stream — not useful for extraction.
|
||||
break;
|
||||
};
|
||||
|
||||
if header.obu_type == crate::av1_obu::obu_type::SEQUENCE_HEADER {
|
||||
let obu_end = i + payload_len;
|
||||
if obu_end > data.len() {
|
||||
break;
|
||||
}
|
||||
// Return the full OBU including header, size field, and payload.
|
||||
return Some(data[..obu_end].to_vec());
|
||||
}
|
||||
|
||||
i += payload_len;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -859,4 +1206,102 @@ mod tests {
|
||||
// NAL type 1 (TRAIL_R)
|
||||
assert!(!enc.is_keyframe(&[0x02, 0x01]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn av1_mediacodec_encoder_returns_not_initialized_on_non_android() {
|
||||
let enc = MediaCodecAv1Encoder::new(1280, 720, 2_000_000);
|
||||
assert!(matches!(enc, Err(VideoError::NotInitialized)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn av1_mediacodec_decoder_returns_not_initialized_on_non_android() {
|
||||
let dec = MediaCodecAv1Decoder::new(1280, 720);
|
||||
assert!(matches!(dec, Err(VideoError::NotInitialized)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn av1_is_keyframe_detects_keyframe() {
|
||||
let enc = MediaCodecAv1Encoder {
|
||||
#[cfg(target_os = "android")]
|
||||
codec: unreachable!(),
|
||||
#[cfg(target_os = "android")]
|
||||
width: 1280,
|
||||
#[cfg(target_os = "android")]
|
||||
height: 720,
|
||||
force_keyframe: false,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
_width: 1280,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
_height: 720,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
_bitrate_bps: 2_000_000,
|
||||
};
|
||||
// Frame header with show_existing_frame=0, frame_type=0 (KEY_FRAME)
|
||||
let mut key_obu = Vec::new();
|
||||
let header = crate::av1_obu::ObuHeader {
|
||||
obu_type: crate::av1_obu::obu_type::FRAME_HEADER,
|
||||
has_size_field: true,
|
||||
extension_flag: false,
|
||||
};
|
||||
key_obu.push(header.to_byte());
|
||||
crate::av1_obu::write_leb128(2, &mut key_obu);
|
||||
key_obu.extend_from_slice(&[0x00, 0x00]); // show_existing=0, frame_type=0
|
||||
assert!(enc.is_keyframe(&key_obu));
|
||||
|
||||
// Frame header with show_existing_frame=0, frame_type=1 (INTER)
|
||||
let mut inter_obu = Vec::new();
|
||||
let header = crate::av1_obu::ObuHeader {
|
||||
obu_type: crate::av1_obu::obu_type::FRAME_HEADER,
|
||||
has_size_field: true,
|
||||
extension_flag: false,
|
||||
};
|
||||
inter_obu.push(header.to_byte());
|
||||
crate::av1_obu::write_leb128(2, &mut inter_obu);
|
||||
inter_obu.extend_from_slice(&[0x40, 0x00]); // show_existing=0, frame_type=1
|
||||
assert!(!enc.is_keyframe(&inter_obu));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_sequence_header_obu_finds_first_seq_header() {
|
||||
let mut data = Vec::new();
|
||||
// Sequence header OBU
|
||||
let sh_header = crate::av1_obu::ObuHeader {
|
||||
obu_type: crate::av1_obu::obu_type::SEQUENCE_HEADER,
|
||||
has_size_field: true,
|
||||
extension_flag: false,
|
||||
};
|
||||
data.push(sh_header.to_byte());
|
||||
crate::av1_obu::write_leb128(5, &mut data);
|
||||
data.extend_from_slice(&[0xAA; 5]);
|
||||
|
||||
// Frame OBU
|
||||
let fh_header = crate::av1_obu::ObuHeader {
|
||||
obu_type: crate::av1_obu::obu_type::FRAME,
|
||||
has_size_field: true,
|
||||
extension_flag: false,
|
||||
};
|
||||
data.push(fh_header.to_byte());
|
||||
crate::av1_obu::write_leb128(3, &mut data);
|
||||
data.extend_from_slice(&[0xBB; 3]);
|
||||
|
||||
let seq = extract_sequence_header_obu(&data).unwrap();
|
||||
// Should contain header byte + leb128(5) + 5 payload bytes
|
||||
assert_eq!(seq.len(), 1 + 1 + 5);
|
||||
assert_eq!(seq[0], sh_header.to_byte());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_sequence_header_obu_returns_none_without_seq_header() {
|
||||
let mut data = Vec::new();
|
||||
let fh_header = crate::av1_obu::ObuHeader {
|
||||
obu_type: crate::av1_obu::obu_type::FRAME,
|
||||
has_size_field: true,
|
||||
extension_flag: false,
|
||||
};
|
||||
data.push(fh_header.to_byte());
|
||||
crate::av1_obu::write_leb128(3, &mut data);
|
||||
data.extend_from_slice(&[0xBB; 3]);
|
||||
|
||||
assert!(extract_sequence_header_obu(&data).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
142
crates/wzp-video/src/svt_av1.rs
Normal file
142
crates/wzp-video/src/svt_av1.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
//! AV1 software encoder via SVT-AV1 (shiguredo_svt_av1).
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use crate::av1_obu::is_keyframe_obu;
|
||||
use crate::encoder::{VideoEncoder, VideoError, VideoFrame};
|
||||
|
||||
/// SW AV1 encoder wrapping `shiguredo_svt_av1::Encoder`.
|
||||
pub struct SvtAv1Encoder {
|
||||
inner: shiguredo_svt_av1::Encoder,
|
||||
force_keyframe: bool,
|
||||
}
|
||||
|
||||
impl SvtAv1Encoder {
|
||||
/// Create a new SVT-AV1 encoder at the given resolution.
|
||||
pub fn new(width: u32, height: u32) -> Result<Self, VideoError> {
|
||||
let mut config = shiguredo_svt_av1::EncoderConfig::new(
|
||||
width as usize,
|
||||
height as usize,
|
||||
shiguredo_svt_av1::ColorFormat::I420,
|
||||
);
|
||||
config.fps_numerator = 30;
|
||||
config.fps_denominator = 1;
|
||||
config.target_bit_rate = 2_000_000;
|
||||
config.rate_control_mode = shiguredo_svt_av1::RcMode::Cbr;
|
||||
config.enc_mode = 8; // Fast preset
|
||||
config.intra_period_length = NonZeroUsize::new(120);
|
||||
|
||||
let inner = shiguredo_svt_av1::Encoder::new(config)
|
||||
.map_err(|e| VideoError::PlatformError(format!("SVT-AV1 init failed: {e}")))?;
|
||||
Ok(Self {
|
||||
inner,
|
||||
force_keyframe: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoEncoder for SvtAv1Encoder {
|
||||
fn encode(&mut self, frame: &VideoFrame) -> Result<Vec<u8>, VideoError> {
|
||||
let y_len = (frame.width * frame.height) as usize;
|
||||
let uv_len = y_len / 4;
|
||||
if frame.data.len() < y_len + uv_len * 2 {
|
||||
return Err(VideoError::InvalidInput(
|
||||
"frame data too small for I420".into(),
|
||||
));
|
||||
}
|
||||
let y = &frame.data[0..y_len];
|
||||
let u = &frame.data[y_len..y_len + uv_len];
|
||||
let v = &frame.data[y_len + uv_len..y_len + uv_len * 2];
|
||||
|
||||
let fd = shiguredo_svt_av1::FrameData::I420 { y, u, v };
|
||||
let options = shiguredo_svt_av1::EncodeOptions {
|
||||
force_keyframe: self.force_keyframe,
|
||||
};
|
||||
self.force_keyframe = false;
|
||||
|
||||
self.inner
|
||||
.encode(&fd, &options)
|
||||
.map_err(|e| VideoError::PlatformError(format!("SVT-AV1 encode failed: {e}")))?;
|
||||
|
||||
if let Some(encoded) = self.inner.next_frame() {
|
||||
Ok(encoded.data().to_vec())
|
||||
} else {
|
||||
Err(VideoError::PlatformError(
|
||||
"SVT-AV1 returned no frame".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn request_keyframe(&mut self) {
|
||||
self.force_keyframe = true;
|
||||
}
|
||||
|
||||
fn is_keyframe(&self, packet: &[u8]) -> bool {
|
||||
is_keyframe_obu(packet)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Dav1dDecoder;
|
||||
|
||||
#[test]
|
||||
fn svt_av1_encoder_instantiates() {
|
||||
let enc = SvtAv1Encoder::new(640, 480);
|
||||
assert!(enc.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svt_av1_encoder_produces_keyframe() {
|
||||
let mut enc = SvtAv1Encoder::new(640, 480).unwrap();
|
||||
// I420 640×480 = 640*480 + 320*240 + 320*240 = 460800 bytes
|
||||
let frame = VideoFrame::new(640, 480, vec![0x80; 460_800], 0);
|
||||
let packet = enc.encode(&frame).unwrap();
|
||||
assert!(!packet.is_empty());
|
||||
assert!(enc.is_keyframe(&packet));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svt_av1_dav1d_roundtrip_10_frames() {
|
||||
use crate::decoder::VideoDecoder;
|
||||
|
||||
let mut enc = SvtAv1Encoder::new(640, 480).unwrap();
|
||||
let mut dec = Dav1dDecoder::new().unwrap();
|
||||
|
||||
// Encode 10 frames. SVT-AV1 produces output on every call in this
|
||||
// configuration (first frame is a keyframe, subsequent are inter).
|
||||
let mut packets: Vec<Vec<u8>> = Vec::with_capacity(10);
|
||||
for i in 0..10 {
|
||||
let frame = VideoFrame::new(640, 480, vec![0x80; 460_800], i as u64 * 33);
|
||||
let packet = enc.encode(&frame).expect("encode should succeed");
|
||||
assert!(!packet.is_empty(), "packet {} should not be empty", i);
|
||||
packets.push(packet);
|
||||
}
|
||||
|
||||
// Decode each packet. The first packet contains the sequence header
|
||||
// OBU; dav1d remembers it for subsequent inter frames.
|
||||
let mut decoded = 0usize;
|
||||
for (i, packet) in packets.iter().enumerate() {
|
||||
match dec.decode(packet) {
|
||||
Ok(Some(frame)) => {
|
||||
assert_eq!(frame.width, 640, "frame {} width mismatch", i);
|
||||
assert_eq!(frame.height, 480, "frame {} height mismatch", i);
|
||||
assert!(
|
||||
!frame.data.is_empty(),
|
||||
"frame {} data should not be empty",
|
||||
i
|
||||
);
|
||||
decoded += 1;
|
||||
}
|
||||
Ok(None) => {
|
||||
// Some frames may not produce immediate output due to decoder
|
||||
// buffering; this is acceptable. We assert > 0 at the end.
|
||||
}
|
||||
Err(e) => panic!("decode failed at packet {}: {}", i, e),
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(decoded, 10, "expected 10 decoded frames, got {}", decoded);
|
||||
}
|
||||
}
|
||||
@@ -652,6 +652,99 @@ impl VideoDecoder for VideoToolboxHevcDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// macOS VideoToolbox AV1 decoder (decode-only; M3+).
|
||||
pub struct VideoToolboxAv1Decoder {
|
||||
#[cfg(target_os = "macos")]
|
||||
inner: Option<Decoder>,
|
||||
#[cfg(target_os = "macos")]
|
||||
width: u32,
|
||||
#[cfg(target_os = "macos")]
|
||||
height: u32,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
_width: u32,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
_height: u32,
|
||||
}
|
||||
|
||||
impl VideoToolboxAv1Decoder {
|
||||
pub fn new(width: u32, height: u32) -> Result<Self, VideoError> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let config = DecoderConfig {
|
||||
codec: DecoderCodec::Av1 { width, height },
|
||||
pixel_format: PixelFormat::I420,
|
||||
};
|
||||
match Decoder::new(config) {
|
||||
Ok(decoder) => Ok(Self {
|
||||
inner: Some(decoder),
|
||||
width,
|
||||
height,
|
||||
}),
|
||||
Err(shiguredo_video_toolbox::Error::UnsupportedCodec { .. }) => {
|
||||
// AV1 decode not supported on this platform (e.g. M1/M2).
|
||||
Ok(Self {
|
||||
inner: None,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
Err(e) => Err(VideoError::PlatformError(format!(
|
||||
"AV1 decoder create failed: {e}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let _ = (width, height);
|
||||
Ok(Self {
|
||||
_width: width,
|
||||
_height: height,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoDecoder for VideoToolboxAv1Decoder {
|
||||
fn decode(&mut self, access_unit: &[u8]) -> Result<Option<VideoFrame>, VideoError> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if access_unit.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let decoder = self.inner.as_mut().ok_or(VideoError::NotInitialized)?;
|
||||
let decoded = decoder
|
||||
.decode(access_unit)
|
||||
.map_err(|e| VideoError::PlatformError(format!("decode failed: {e}")))?;
|
||||
match decoded {
|
||||
Some(DecodedFrame::I420(frame)) => {
|
||||
let y = frame.y_plane();
|
||||
let u = frame.u_plane();
|
||||
let v = frame.v_plane();
|
||||
let mut data = Vec::with_capacity(y.len() + u.len() + v.len());
|
||||
data.extend_from_slice(y);
|
||||
data.extend_from_slice(u);
|
||||
data.extend_from_slice(v);
|
||||
Ok(Some(VideoFrame {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
data,
|
||||
timestamp_ms: 0,
|
||||
}))
|
||||
}
|
||||
Some(DecodedFrame::Nv12(_)) => Err(VideoError::PlatformError(
|
||||
"unexpected NV12 output from decoder".to_string(),
|
||||
)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let _ = access_unit;
|
||||
Err(VideoError::NotInitialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Type alias for HEVC parameter-set triple returned by `extract_vps_sps_pps`.
|
||||
type HevcParameterSets = (Option<Vec<u8>>, Option<Vec<u8>>, Option<Vec<u8>>);
|
||||
|
||||
@@ -792,4 +885,12 @@ mod tests {
|
||||
assert_eq!(sps, Some(vec![0x42, 0x01, 0x01, 0x01]));
|
||||
assert_eq!(pps, Some(vec![0x44, 0x01, 0xC1, 0x72]));
|
||||
}
|
||||
|
||||
// ---- AV1 ----
|
||||
|
||||
#[test]
|
||||
fn av1_decoder_instantiates() {
|
||||
let dec = VideoToolboxAv1Decoder::new(1280, 720);
|
||||
assert!(dec.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user