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:
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user