Files
wz-phone/crates/wzp-video/src/svt_av1.rs
Siavash Sameni 9334aa5ccd 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
2026-05-12 18:44:44 +04:00

143 lines
4.9 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.
//! 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);
}
}