T4.2: VideoToolbox H.264 encoder/decoder traits (macOS, MVP)
This commit is contained in:
15
crates/wzp-video/src/decoder.rs
Normal file
15
crates/wzp-video/src/decoder.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
//! Video decoder trait and platform implementations.
|
||||
|
||||
use crate::encoder::{VideoError, VideoFrame};
|
||||
|
||||
/// Trait for video decoders.
|
||||
///
|
||||
/// Implementations are platform-specific (VideoToolbox on macOS, MediaCodec on
|
||||
/// Android, OpenH264 as software fallback).
|
||||
pub trait VideoDecoder: Send {
|
||||
/// Decode one H.264 access unit into a raw video frame.
|
||||
///
|
||||
/// Returns `Ok(Some(frame))` when a frame is ready, `Ok(None)` if more
|
||||
/// data is needed (e.g., for reordering), or an error.
|
||||
fn decode(&mut self, access_unit: &[u8]) -> Result<Option<VideoFrame>, VideoError>;
|
||||
}
|
||||
65
crates/wzp-video/src/encoder.rs
Normal file
65
crates/wzp-video/src/encoder.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
//! Video encoder trait and platform implementations.
|
||||
|
||||
/// Errors that can occur during video encoding or decoding.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum VideoError {
|
||||
/// Platform codec failed (e.g., VTCompressionSession error).
|
||||
PlatformError(String),
|
||||
/// Invalid input parameters.
|
||||
InvalidInput(String),
|
||||
/// Codec is not initialized.
|
||||
NotInitialized,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VideoError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
VideoError::PlatformError(s) => write!(f, "platform error: {s}"),
|
||||
VideoError::InvalidInput(s) => write!(f, "invalid input: {s}"),
|
||||
VideoError::NotInitialized => write!(f, "codec not initialized"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for VideoError {}
|
||||
|
||||
/// Trait for video encoders.
|
||||
///
|
||||
/// Implementations are platform-specific (VideoToolbox on macOS, MediaCodec on
|
||||
/// Android, OpenH264 as software fallback).
|
||||
pub trait VideoEncoder: Send {
|
||||
/// Encode one raw video frame into a H.264 access unit.
|
||||
///
|
||||
/// Returns the encoded bytes (one complete access unit) or an error.
|
||||
fn encode(&mut self, frame: &VideoFrame) -> Result<Vec<u8>, VideoError>;
|
||||
|
||||
/// Request the next encoded frame to be an I-frame (keyframe).
|
||||
fn request_keyframe(&mut self);
|
||||
|
||||
/// Returns true if the given encoded packet is a keyframe.
|
||||
fn is_keyframe(&self, packet: &[u8]) -> bool;
|
||||
}
|
||||
|
||||
/// Raw video frame input for encoding.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VideoFrame {
|
||||
/// Width in pixels.
|
||||
pub width: u32,
|
||||
/// Height in pixels.
|
||||
pub height: u32,
|
||||
/// Pixel data (NV12 or I420, depending on platform).
|
||||
pub data: Vec<u8>,
|
||||
/// Presentation timestamp in milliseconds.
|
||||
pub timestamp_ms: u64,
|
||||
}
|
||||
|
||||
impl VideoFrame {
|
||||
pub fn new(width: u32, height: u32, data: Vec<u8>, timestamp_ms: u64) -> Self {
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
timestamp_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,17 @@
|
||||
//! packetization (NAL fragmentation / reassembly). Platform encoders and
|
||||
//! decoders land in T4.2/T4.3.
|
||||
|
||||
pub mod decoder;
|
||||
pub mod depacketizer;
|
||||
pub mod encoder;
|
||||
pub mod framer;
|
||||
pub mod videotoolbox;
|
||||
|
||||
pub use decoder::VideoDecoder;
|
||||
pub use depacketizer::H264Depacketizer;
|
||||
pub use encoder::{VideoEncoder, VideoError, VideoFrame};
|
||||
pub use framer::{FramedPacket, H264Framer};
|
||||
pub use videotoolbox::{VideoToolboxDecoder, VideoToolboxEncoder};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
107
crates/wzp-video/src/videotoolbox.rs
Normal file
107
crates/wzp-video/src/videotoolbox.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
//! Apple VideoToolbox H.264 encoder / decoder (macOS only).
|
||||
|
||||
use crate::decoder::VideoDecoder;
|
||||
use crate::encoder::{VideoEncoder, VideoError, VideoFrame};
|
||||
|
||||
/// macOS VideoToolbox H.264 encoder.
|
||||
///
|
||||
/// Wraps `VTCompressionSession`. Minimum viable: API compiles and is
|
||||
/// instantiable; full hardware encode/decode lands in a follow-up task.
|
||||
pub struct VideoToolboxEncoder {
|
||||
width: u32,
|
||||
height: u32,
|
||||
bitrate_bps: u32,
|
||||
force_keyframe: bool,
|
||||
}
|
||||
|
||||
impl VideoToolboxEncoder {
|
||||
/// Create a new encoder.
|
||||
///
|
||||
/// * `width` / `height` — frame dimensions in pixels.
|
||||
/// * `bitrate_bps` — target bitrate in bits per second.
|
||||
pub fn new(width: u32, height: u32, bitrate_bps: u32) -> Result<Self, VideoError> {
|
||||
Ok(Self {
|
||||
width,
|
||||
height,
|
||||
bitrate_bps,
|
||||
force_keyframe: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoEncoder for VideoToolboxEncoder {
|
||||
fn encode(&mut self, _frame: &VideoFrame) -> Result<Vec<u8>, VideoError> {
|
||||
// TODO(T4.2-MVP): Wire VTCompressionSession.
|
||||
// For now return an empty AU so the API compiles and callers can
|
||||
// integrate the shape.
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn request_keyframe(&mut self) {
|
||||
self.force_keyframe = true;
|
||||
}
|
||||
|
||||
fn is_keyframe(&self, packet: &[u8]) -> bool {
|
||||
if packet.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let nal_type = packet[0] & 0x1F;
|
||||
// NAL type 5 = IDR slice (keyframe).
|
||||
nal_type == 5
|
||||
}
|
||||
}
|
||||
|
||||
/// macOS VideoToolbox H.264 decoder.
|
||||
///
|
||||
/// Wraps `VTDecompressionSession`. Minimum viable: API compiles and is
|
||||
/// instantiable.
|
||||
pub struct VideoToolboxDecoder {
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl VideoToolboxDecoder {
|
||||
/// Create a new decoder.
|
||||
pub fn new(width: u32, height: u32) -> Result<Self, VideoError> {
|
||||
Ok(Self { width, height })
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoDecoder for VideoToolboxDecoder {
|
||||
fn decode(&mut self, _access_unit: &[u8]) -> Result<Option<VideoFrame>, VideoError> {
|
||||
// TODO(T4.2-MVP): Wire VTDecompressionSession.
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn encoder_instantiates() {
|
||||
let enc = VideoToolboxEncoder::new(1280, 720, 2_000_000);
|
||||
assert!(enc.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoder_instantiates() {
|
||||
let dec = VideoToolboxDecoder::new(1280, 720);
|
||||
assert!(dec.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_keyframe_detects_idr() {
|
||||
let enc = VideoToolboxEncoder::new(1280, 720, 2_000_000).unwrap();
|
||||
assert!(enc.is_keyframe(&[0x65, 0x01, 0x02]));
|
||||
assert!(!enc.is_keyframe(&[0x41, 0x01, 0x02]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_keyframe_sets_flag() {
|
||||
let mut enc = VideoToolboxEncoder::new(1280, 720, 2_000_000).unwrap();
|
||||
assert!(!enc.force_keyframe);
|
||||
enc.request_keyframe();
|
||||
assert!(enc.force_keyframe);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user