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:
Siavash Sameni
2026-05-12 18:33:43 +04:00
parent 553c8a4ce1
commit 9334aa5ccd
14 changed files with 1318 additions and 12 deletions

View File

@@ -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());
}
}