use super::Encoded;
use super::backend::{self, Backend};
use crate::{Error, Frame, Size};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum Codec {
#[default]
H264,
H265,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum Kind {
#[default]
Auto,
Hardware,
Software,
Named(String),
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Config {
pub width: u32,
pub height: u32,
pub framerate: u32,
pub bitrate: Option<u64>,
pub gop: u32,
pub codec: Codec,
pub kind: Kind,
}
impl Config {
pub fn new(width: u32, height: u32, framerate: u32) -> Self {
Self {
width,
height,
framerate,
bitrate: None,
gop: framerate.saturating_mul(2).max(1),
codec: Codec::default(),
kind: Kind::Auto,
}
}
pub fn size(&self) -> Size {
Size::new(self.width, self.height)
}
pub(crate) fn resolved_bitrate(&self) -> u64 {
self.bitrate.unwrap_or_else(|| {
((self.size().pixels() * self.framerate as u64) as f64 * 0.07) as u64
})
}
}
pub struct Encoder {
backend: Box<dyn Backend>,
codec: Codec,
size: Size,
bitrate: u64,
pending_keyframe: bool,
}
impl Encoder {
pub fn new(config: &Config) -> Result<Self, Error> {
if config.framerate == 0 {
return Err(Error::InvalidFramerate(0));
}
let size = config.size();
size.validate("encoder")?;
let backend = backend::open(config)?;
Ok(Self {
backend,
codec: config.codec,
size,
bitrate: config.resolved_bitrate(),
pending_keyframe: false,
})
}
pub fn name(&self) -> &str {
self.backend.name()
}
pub fn size(&self) -> Size {
self.size
}
pub fn bitrate(&self) -> u64 {
self.bitrate
}
pub fn set_bitrate(&mut self, bitrate: u64) -> Result<(), Error> {
if bitrate == self.bitrate {
return Ok(());
}
self.backend.set_bitrate(bitrate)?;
self.bitrate = bitrate;
Ok(())
}
pub fn codec(&self) -> Codec {
self.codec
}
pub fn keyframe(&mut self) {
self.pending_keyframe = true;
}
pub fn encode(&mut self, frame: &Frame) -> Result<Vec<Encoded>, Error> {
let size = frame.size();
if size != self.size {
return Err(Error::Codec(anyhow::anyhow!(
"frame {size} does not match encoder {}",
self.size
)));
}
let encoded = self.backend.encode(frame, self.pending_keyframe)?;
self.pending_keyframe = false;
Ok(encoded)
}
pub fn finish(mut self) -> Result<Vec<Encoded>, Error> {
self.backend.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{I420, Surface};
fn gray_rgba(width: u32, height: u32) -> Vec<u8> {
vec![0x80u8; width as usize * height as usize * 4]
}
fn gray_frame(width: u32, height: u32, index: u64) -> Frame {
let surface = Surface::rgba(&gray_rgba(width, height), Size::new(width, height)).unwrap();
Frame::new(surface, at(index))
}
fn at(index: u64) -> moq_net::Timestamp {
moq_net::Timestamp::from_micros(index * 33_333).unwrap()
}
fn payloads(frames: &[Encoded]) -> Vec<bytes::Bytes> {
frames.iter().map(|f| f.payload.clone()).collect()
}
#[test]
fn software_encoder_emits_annexb() {
let config = Config {
kind: Kind::Software,
..Config::new(320, 240, 30)
};
let mut encoder = Encoder::new(&config).expect("openh264 is vendored, always available");
assert_eq!(encoder.name(), "openh264");
let mut frames = Vec::new();
for i in 0..30 {
if i == 0 {
encoder.keyframe();
}
frames.extend(encoder.encode(&gray_frame(320, 240, i)).unwrap());
}
frames.extend(encoder.finish().unwrap());
assert!(!frames.is_empty(), "encoder produced no packets");
let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
assert!(
micros.windows(2).all(|w| w[0] < w[1]),
"encoded timestamps not strictly increasing: {micros:?}"
);
assert!(
micros.iter().all(|&t| t % 33_333 == 0 && t < 30 * 33_333),
"encoded timestamp outside the fed set: {micros:?}"
);
let packets = payloads(&frames);
let first = &packets[0];
let has_start_code = first.starts_with(&[0, 0, 0, 1]) || first.starts_with(&[0, 0, 1]);
assert!(
has_start_code,
"first packet is not Annex-B: {:02x?}",
&first[..first.len().min(8)]
);
}
#[test]
fn encode_rgba_surface_emits_annexb() {
let config = Config {
kind: Kind::Software,
..Config::new(320, 240, 30)
};
let mut encoder = Encoder::new(&config).unwrap();
let mut frames = encoder.encode(&gray_frame(320, 240, 0)).unwrap();
frames.extend(encoder.finish().unwrap());
assert!(!frames.is_empty());
let packets = payloads(&frames);
assert!(packets[0].starts_with(&[0, 0, 0, 1]) || packets[0].starts_with(&[0, 0, 1]));
}
#[test]
fn encode_i420_surface_emits_annexb() {
let config = Config {
kind: Kind::Software,
..Config::new(320, 240, 30)
};
let mut encoder = Encoder::new(&config).unwrap();
let i420 = I420::new(320, 240, vec![0x80u8; I420::len(320, 240)]).unwrap();
let frame = Frame::new(Surface::I420(i420), at(0));
let mut frames = encoder.encode(&frame).unwrap();
frames.extend(encoder.finish().unwrap());
assert!(!frames.is_empty());
let packets = payloads(&frames);
assert!(packets[0].starts_with(&[0, 0, 0, 1]) || packets[0].starts_with(&[0, 0, 1]));
}
#[test]
fn encode_rejects_dimension_mismatch() {
let Ok(mut encoder) = Encoder::new(&Config::new(320, 240, 30)) else {
return;
};
assert!(matches!(encoder.encode(&gray_frame(640, 480, 0)), Err(Error::Codec(_))));
}
#[test]
fn encode_rejects_transposed_frame() {
let Ok(mut encoder) = Encoder::new(&Config::new(320, 240, 30)) else {
return;
};
let transposed = gray_frame(240, 320, 0);
assert_eq!(
gray_rgba(240, 320).len(),
gray_rgba(320, 240).len(),
"the byte counts must collide"
);
assert!(matches!(encoder.encode(&transposed), Err(Error::Codec(_))));
}
#[test]
fn new_rejects_zero_framerate() {
let config = Config::new(320, 240, 0);
assert!(matches!(Encoder::new(&config), Err(Error::InvalidFramerate(0))));
}
#[test]
fn unknown_named_encoder_errors() {
let config = Config {
kind: Kind::Named("definitely_not_a_codec".into()),
..Config::new(320, 240, 30)
};
assert!(matches!(Encoder::new(&config), Err(Error::NoEncoder(_))));
}
#[cfg(target_os = "macos")]
#[test]
fn videotoolbox_emits_annexb_keyframe() {
let config = Config {
kind: Kind::Named("videotoolbox".into()),
..Config::new(320, 240, 30)
};
let mut encoder = Encoder::new(&config).expect("videotoolbox is available on macOS");
assert_eq!(encoder.name(), "videotoolbox");
let mut frames = Vec::new();
for i in 0..10 {
if i == 0 {
encoder.keyframe();
}
frames.extend(encoder.encode(&gray_frame(320, 240, i)).unwrap());
}
frames.extend(encoder.finish().unwrap());
assert!(!frames.is_empty(), "encoder produced no packets");
let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
assert!(
micros.windows(2).all(|w| w[0] < w[1]),
"encoded timestamps not strictly increasing: {micros:?}"
);
let packets = payloads(&frames);
let first = &packets[0];
assert!(
first.starts_with(&[0, 0, 0, 1]) || first.starts_with(&[0, 0, 1]),
"first packet is not Annex-B"
);
let types = nal_types(first);
assert!(types.contains(&7), "no SPS in first packet: {types:?}");
assert!(types.contains(&8), "no PPS in first packet: {types:?}");
assert!(types.contains(&5), "first packet is not an IDR: {types:?}");
}
#[cfg(target_os = "macos")]
#[test]
fn videotoolbox_emits_annexb_keyframe_h265() {
let config = Config {
codec: Codec::H265,
kind: Kind::Named("videotoolbox".into()),
..Config::new(320, 240, 30)
};
let mut encoder = Encoder::new(&config).expect("videotoolbox HEVC is available on macOS");
assert_eq!(encoder.name(), "videotoolbox");
assert_eq!(encoder.codec(), Codec::H265);
let mut frames = Vec::new();
for i in 0..10 {
if i == 0 {
encoder.keyframe();
}
frames.extend(encoder.encode(&gray_frame(320, 240, i)).unwrap());
}
frames.extend(encoder.finish().unwrap());
assert!(!frames.is_empty(), "encoder produced no packets");
let packets = payloads(&frames);
let first = &packets[0];
assert!(
first.starts_with(&[0, 0, 0, 1]) || first.starts_with(&[0, 0, 1]),
"first packet is not Annex-B"
);
let types = hevc_nal_types(first);
assert!(types.contains(&32), "no VPS in first packet: {types:?}");
assert!(types.contains(&33), "no SPS in first packet: {types:?}");
assert!(types.contains(&34), "no PPS in first packet: {types:?}");
assert!(
types.iter().any(|t| (16..=23).contains(t)),
"first packet is not an IRAP: {types:?}"
);
}
#[cfg(target_os = "macos")]
fn hevc_nal_types(annexb: &[u8]) -> Vec<u8> {
let mut types = Vec::new();
let mut i = 0;
while i + 3 < annexb.len() {
if annexb[i..i + 3] == [0, 0, 1] {
types.push((annexb[i + 3] >> 1) & 0x3f);
i += 3;
} else {
i += 1;
}
}
types
}
#[cfg(target_os = "macos")]
#[test]
fn videotoolbox_encodes_surface_zero_copy() {
let config = Config {
kind: Kind::Named("videotoolbox".into()),
..Config::new(320, 240, 30)
};
let mut encoder = Encoder::new(&config).unwrap();
let mut frames = Vec::new();
for i in 0..10 {
if i == 0 {
encoder.keyframe();
}
let frame = Frame::new(Surface::PixelBuffer(nv12_surface(320, 240)), at(i));
frames.extend(encoder.encode(&frame).unwrap());
}
frames.extend(encoder.finish().unwrap());
assert!(!frames.is_empty());
let packets = payloads(&frames);
let types = nal_types(&packets[0]);
assert!(
types.contains(&7) && types.contains(&8) && types.contains(&5),
"no IDR: {types:?}"
);
}
#[cfg(target_os = "macos")]
#[test]
fn openh264_downloads_surface() {
let config = Config {
kind: Kind::Software,
..Config::new(320, 240, 30)
};
let mut encoder = Encoder::new(&config).unwrap();
encoder.keyframe();
let frame = Frame::new(Surface::PixelBuffer(nv12_surface(320, 240)), at(0));
let mut frames = encoder.encode(&frame).unwrap();
frames.extend(encoder.finish().unwrap());
assert!(!frames.is_empty());
let packets = payloads(&frames);
assert!(packets[0].starts_with(&[0, 0, 0, 1]) || packets[0].starts_with(&[0, 0, 1]));
}
#[cfg(target_os = "macos")]
fn nv12_surface(width: u32, height: u32) -> crate::frame::macos::PixelBuffer {
use std::ptr::{self, NonNull};
use objc2_core_foundation::CFRetained;
use objc2_core_video::{
CVPixelBuffer, CVPixelBufferCreate, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane,
CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferUnlockBaseAddress,
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
};
let mut raw: *mut CVPixelBuffer = ptr::null_mut();
let status = unsafe {
CVPixelBufferCreate(
None,
width as usize,
height as usize,
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
None,
NonNull::new(&mut raw).unwrap(),
)
};
assert_eq!(status, 0, "CVPixelBufferCreate failed");
let buffer = unsafe { CFRetained::from_raw(NonNull::new(raw).unwrap()) };
let flags = CVPixelBufferLockFlags(0);
assert_eq!(unsafe { CVPixelBufferLockBaseAddress(&buffer, flags) }, 0);
for (plane, rows) in [(0usize, height as usize), (1usize, height as usize / 2)] {
let base = CVPixelBufferGetBaseAddressOfPlane(&buffer, plane) as *mut u8;
let stride = CVPixelBufferGetBytesPerRowOfPlane(&buffer, plane);
unsafe { ptr::write_bytes(base, 128, stride * rows) };
}
unsafe { CVPixelBufferUnlockBaseAddress(&buffer, flags) };
crate::frame::macos::PixelBuffer::new(buffer, width, height)
}
fn nal_types(annexb: &[u8]) -> Vec<u8> {
let mut types = Vec::new();
let mut i = 0;
while i + 3 < annexb.len() {
if annexb[i..i + 3] == [0, 0, 1] {
types.push(annexb[i + 3] & 0x1f);
i += 3;
} else {
i += 1;
}
}
types
}
#[cfg(target_os = "windows")]
#[test]
#[ignore]
fn mediafoundation_cpu_rgba() {
let config = Config {
kind: Kind::Named("mediafoundation".into()),
..Config::new(640, 480, 30)
};
let mut encoder = Encoder::new(&config).expect("hardware H.264 encoder available");
assert_eq!(encoder.name(), "mediafoundation");
let mut frames = Vec::new();
for i in 0..30 {
if i == 0 {
encoder.keyframe();
}
frames.extend(encoder.encode(&gray_frame(640, 480, i)).unwrap());
}
frames.extend(encoder.finish().unwrap());
assert!(!frames.is_empty(), "encoder produced no packets");
let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
assert!(
micros.windows(2).all(|w| w[0] < w[1]),
"encoded timestamps not strictly increasing: {micros:?}"
);
assert!(
micros.iter().all(|&t| t % 33_333 == 0 && t < 30 * 33_333),
"encoded timestamp outside the fed set: {micros:?}"
);
let packets = payloads(&frames);
let types = nal_types(&packets[0]);
assert!(types.contains(&7), "no SPS in first packet: {types:?}");
assert!(types.contains(&8), "no PPS in first packet: {types:?}");
assert!(types.contains(&5), "first packet is not an IDR: {types:?}");
}
#[cfg(target_os = "windows")]
#[tokio::test]
#[ignore]
async fn mediafoundation_camera_texture() {
let mut camera = crate::capture::open(&crate::capture::Config::default())
.await
.expect("open default camera");
let (w, h) = (camera.width(), camera.height());
let config = Config {
kind: Kind::Named("mediafoundation".into()),
..Config::new(w, h, camera.framerate().unwrap_or(30))
};
let mut encoder = Encoder::new(&config).expect("hardware H.264 encoder available");
let mut frames = Vec::new();
let mut textures = 0;
for i in 0..30 {
let surface = camera.read().await.expect("frame, not end of stream");
if matches!(surface, Surface::Texture(_)) {
textures += 1;
}
if i == 0 {
encoder.keyframe();
}
frames.extend(encoder.encode(&Frame::new(surface, at(i))).unwrap());
}
frames.extend(encoder.finish().unwrap());
assert!(textures > 0, "capture never produced a GPU texture");
assert!(!frames.is_empty(), "encoder produced no packets");
let packets = payloads(&frames);
let types = nal_types(&packets[0]);
assert!(
types.contains(&7) && types.contains(&8) && types.contains(&5),
"no IDR: {types:?}"
);
}
#[test]
fn set_bitrate_retunes_software_encoder() {
let config = Config {
kind: Kind::Software,
..Config::new(320, 240, 30)
};
let mut encoder = Encoder::new(&config).unwrap();
let opened = encoder.bitrate();
assert_eq!(opened, config.resolved_bitrate());
encoder.encode(&gray_frame(320, 240, 0)).unwrap();
let halved = opened / 2;
encoder.set_bitrate(halved).unwrap();
assert_eq!(encoder.bitrate(), halved);
let frames = encoder.encode(&gray_frame(320, 240, 1)).unwrap();
assert!(!frames.is_empty(), "encoder produced nothing after a retune");
let packets = payloads(&frames);
assert!(packets[0].starts_with(&[0, 0, 0, 1]) || packets[0].starts_with(&[0, 0, 1]));
}
#[test]
fn set_bitrate_before_the_first_frame_is_deferred() {
let config = Config {
kind: Kind::Software,
..Config::new(320, 240, 30)
};
let mut encoder = Encoder::new(&config).unwrap();
let halved = encoder.bitrate() / 2;
encoder.set_bitrate(halved).expect("a retune before the first frame");
assert_eq!(encoder.bitrate(), halved);
let frames = encoder.encode(&gray_frame(320, 240, 0)).unwrap();
assert!(!frames.is_empty());
encoder.set_bitrate(halved / 2).unwrap();
assert!(encoder.encode(&gray_frame(320, 240, 1)).is_ok());
}
#[test]
fn set_bitrate_to_current_is_a_noop() {
let config = Config {
kind: Kind::Software,
..Config::new(320, 240, 30)
};
let mut encoder = Encoder::new(&config).unwrap();
let opened = encoder.bitrate();
encoder.set_bitrate(opened).unwrap();
assert_eq!(encoder.bitrate(), opened);
}
#[test]
fn default_bitrate_scales_with_resolution() {
let small = Config::new(320, 240, 30).resolved_bitrate();
let large = Config::new(1920, 1080, 30).resolved_bitrate();
assert!(large > small);
assert!(small > 0);
}
struct Delayed {
pending: Option<Encoded>,
}
impl Backend for Delayed {
fn encode(&mut self, frame: &Frame, _keyframe: bool) -> Result<Vec<Encoded>, Error> {
let payload = bytes::Bytes::from(frame.timestamp.as_micros().to_string());
let previous = self.pending.replace(Encoded::new(payload, frame.timestamp));
Ok(previous.into_iter().collect())
}
fn finish(&mut self) -> Result<Vec<Encoded>, Error> {
Ok(self.pending.take().into_iter().collect())
}
fn set_bitrate(&mut self, _bitrate: u64) -> Result<(), Error> {
Ok(())
}
fn name(&self) -> &str {
"delayed"
}
}
fn encoder_with(backend: Box<dyn Backend>, config: &Config) -> Encoder {
Encoder {
backend,
codec: config.codec,
size: config.size(),
bitrate: config.resolved_bitrate(),
pending_keyframe: false,
}
}
struct Recorder(std::sync::Arc<std::sync::Mutex<Vec<bool>>>);
impl Backend for Recorder {
fn encode(&mut self, _frame: &Frame, keyframe: bool) -> Result<Vec<Encoded>, Error> {
self.0.lock().unwrap().push(keyframe);
Ok(Vec::new())
}
fn finish(&mut self) -> Result<Vec<Encoded>, Error> {
Ok(Vec::new())
}
fn set_bitrate(&mut self, _bitrate: u64) -> Result<(), Error> {
Ok(())
}
fn name(&self) -> &str {
"recorder"
}
}
#[test]
fn a_keyframe_request_waits_for_the_next_frame_then_clears() {
let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let config = Config::new(320, 240, 30);
let mut encoder = encoder_with(Box::new(Recorder(log.clone())), &config);
encoder.encode(&gray_frame(320, 240, 0)).unwrap();
encoder.keyframe();
encoder.keyframe();
encoder.encode(&gray_frame(320, 240, 1)).unwrap();
encoder.encode(&gray_frame(320, 240, 2)).unwrap();
assert_eq!(*log.lock().unwrap(), vec![false, true, false]);
}
#[test]
fn a_mid_stream_keyframe_request_emits_an_idr() {
let config = Config {
kind: Kind::Software,
..Config::new(320, 240, 30)
};
let mut encoder = Encoder::new(&Config { gop: 1000, ..config }).unwrap();
let mut per_frame = Vec::new();
for i in 0..6 {
if i == 3 {
encoder.keyframe();
}
let encoded = encoder.encode(&gray_frame(320, 240, i)).unwrap();
let joined: Vec<u8> = encoded.iter().flat_map(|f| f.payload.iter()).copied().collect();
per_frame.push(nal_types(&joined));
}
let asked = &per_frame[3];
assert!(asked.contains(&5), "the requested frame is not an IDR: {asked:?}");
assert!(asked.contains(&7), "no SPS with the requested IDR: {asked:?}");
assert!(asked.contains(&8), "no PPS with the requested IDR: {asked:?}");
for i in [1, 2, 4, 5] {
assert!(
!per_frame[i].contains(&5),
"frame {i} was keyed without being asked: {:?}",
per_frame[i]
);
}
}
struct Failing;
impl Backend for Failing {
fn encode(&mut self, _frame: &Frame, _keyframe: bool) -> Result<Vec<Encoded>, Error> {
Err(Error::Codec(anyhow::anyhow!("no")))
}
fn finish(&mut self) -> Result<Vec<Encoded>, Error> {
Ok(Vec::new())
}
fn set_bitrate(&mut self, _bitrate: u64) -> Result<(), Error> {
Ok(())
}
fn name(&self) -> &str {
"failing"
}
}
#[test]
fn a_failed_encode_keeps_the_keyframe_request() {
let config = Config::new(320, 240, 30);
let mut encoder = encoder_with(Box::new(Failing), &config);
encoder.keyframe();
assert!(encoder.encode(&gray_frame(320, 240, 0)).is_err());
assert!(encoder.pending_keyframe, "the backend error swallowed the request");
let mut encoder = encoder_with(Box::new(Delayed { pending: None }), &config);
encoder.keyframe();
assert!(encoder.encode(&gray_frame(640, 480, 0)).is_err());
assert!(encoder.pending_keyframe, "the size check swallowed the request");
encoder.encode(&gray_frame(320, 240, 1)).unwrap();
assert!(!encoder.pending_keyframe);
}
#[test]
fn a_buffering_backend_keeps_each_frames_timestamp() {
let config = Config::new(320, 240, 30);
let mut encoder = encoder_with(Box::new(Delayed { pending: None }), &config);
for i in 0..5 {
let encoded = encoder.encode(&gray_frame(320, 240, i)).unwrap();
if i == 0 {
assert!(encoded.is_empty(), "the first frame is still buffered");
continue;
}
assert_eq!(encoded.len(), 1);
assert_eq!(encoded[0].timestamp, at(i - 1));
assert_eq!(&encoded[0].payload[..], at(i - 1).as_micros().to_string().as_bytes());
}
let tail = encoder.finish().unwrap();
assert_eq!(tail.len(), 1);
assert_eq!(tail[0].timestamp, at(4));
assert!(
encoder_with(Box::new(Delayed { pending: None }), &config)
.finish()
.unwrap()
.is_empty()
);
}
}