use std::time::Duration;
use bytes::Bytes;
use hang::catalog::{AV1, VideoCodec, VideoConfig};
use moq_mux::codec::{annexb, h264, h265};
use moq_net::Timestamp;
use super::backend::{self, Backend, Codec};
use crate::{Error, Frame, Size};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum Kind {
#[default]
Auto,
Hardware,
Software,
Named(String),
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct Config {
pub kind: Kind,
pub latency_max: Option<Duration>,
pub resize: Option<Size>,
}
impl Config {
pub fn new() -> Self {
Self::default()
}
}
enum Conversion {
Passthrough,
LengthPrefixed { length_size: usize, keyframe_prefix: Bytes },
}
pub struct Decoder {
backend: Box<dyn Backend>,
conversion: Conversion,
got_keyframe: bool,
}
impl Decoder {
pub fn new(catalog: &VideoConfig, config: &Config) -> Result<Self, Error> {
let (codec, conversion) = match &catalog.codec {
VideoCodec::H264(h264) => {
let conversion = if h264.inline {
Conversion::Passthrough
} else {
let avcc = catalog.description.as_ref().ok_or_else(|| {
Error::Codec(anyhow::anyhow!("avc1 H.264 track is missing its avcC description"))
})?;
let params = h264::Avcc::parse(avcc).map_err(moq_mux::Error::from)?;
let keyframe_prefix = annexb::build_prefix(params.sps.iter().chain(params.pps.iter()));
Conversion::LengthPrefixed {
length_size: params.length_size,
keyframe_prefix,
}
};
(Codec::H264, conversion)
}
VideoCodec::H265(h265) => {
let conversion = if h265.in_band {
Conversion::Passthrough
} else {
let hvcc = catalog.description.as_ref().ok_or_else(|| {
Error::Codec(anyhow::anyhow!("hvc1 H.265 track is missing its hvcC description"))
})?;
let params = h265::Hvcc::parse(hvcc).map_err(moq_mux::Error::from)?;
let keyframe_prefix =
annexb::build_prefix(params.vps.iter().chain(params.sps.iter()).chain(params.pps.iter()));
Conversion::LengthPrefixed {
length_size: params.length_size,
keyframe_prefix,
}
};
(Codec::H265, conversion)
}
VideoCodec::AV1(av1) if is_supported_av1(av1) => (Codec::Av1, Conversion::Passthrough),
other => return Err(Error::UnsupportedCodec(other.to_string())),
};
let backend = backend::open(codec, config)?;
tracing::debug!(decoder = backend.name(), "opened video decoder");
Ok(Self {
backend,
conversion,
got_keyframe: false,
})
}
pub fn name(&self) -> &str {
self.backend.name()
}
pub fn decode(&mut self, payload: &Bytes, timestamp: Timestamp, keyframe: bool) -> Result<Vec<Frame>, Error> {
if !self.got_keyframe {
if !keyframe {
return Ok(Vec::new());
}
self.got_keyframe = true;
}
let access_unit = match &self.conversion {
Conversion::Passthrough => payload.clone(),
Conversion::LengthPrefixed {
length_size,
keyframe_prefix,
} => {
let prefix = keyframe.then(|| keyframe_prefix.as_ref());
annexb::from_length_prefixed(payload, *length_size, prefix).map_err(moq_mux::Error::from)?
}
};
self.backend.decode(access_unit, timestamp, keyframe)
}
}
fn is_supported_av1(av1: &AV1) -> bool {
av1.bitdepth == 8 && !av1.mono_chrome && av1.chroma_subsampling_x && av1.chroma_subsampling_y
}
#[cfg(test)]
mod tests {
use moq_net::Timestamp;
use super::backend::{self, Codec};
use crate::encode::{Config as EncodeConfig, Encoder, Kind as EncodeKind};
use crate::frame::I420;
use crate::{Frame, Surface};
fn gray_frame(index: u64) -> Frame {
let rgba = vec![0x80u8; 320 * 240 * 4];
let surface = Surface::rgba(&rgba, crate::Size::new(320, 240)).unwrap();
Frame::new(surface, Timestamp::from_micros(index * 33_333).unwrap())
}
fn assert_gray(i420: &I420, width: u32, height: u32) {
assert_eq!(i420.width, width);
assert_eq!(i420.height, height);
let luma = (width * height) as usize;
assert_eq!(i420.data.len(), luma * 3 / 2);
let avg = |plane: &[u8]| plane.iter().map(|&b| b as u32).sum::<u32>() / plane.len() as u32;
let y = avg(&i420.data[..luma]);
let u = avg(&i420.data[luma..luma + luma / 4]);
let v = avg(&i420.data[luma + luma / 4..]);
assert!((110..=140).contains(&y), "luma {y} off for a gray frame");
assert!((118..=138).contains(&u), "u {u} off for a gray frame");
assert!((118..=138).contains(&v), "v {v} off for a gray frame");
}
fn round_trip(mut encoder: Encoder, mut decoder: Box<dyn backend::Backend>, expect_name: &str) {
assert_eq!(decoder.name(), expect_name);
let mut decoded = Vec::new();
for i in 0..10u64 {
let keyframe = i == 0;
if keyframe {
encoder.keyframe();
}
for encoded in encoder.encode(&gray_frame(i)).unwrap() {
decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, keyframe).unwrap());
}
}
assert!(!decoded.is_empty(), "decoder produced no frames");
for out in &decoded {
assert_gray(&out.surface.to_i420().unwrap(), 320, 240);
}
let micros: Vec<u128> = decoded.iter().map(|d| d.timestamp.as_micros()).collect();
assert!(
micros.windows(2).all(|w| w[0] < w[1]),
"decoded timestamps not strictly increasing: {micros:?}"
);
assert!(
micros.iter().all(|&t| t % 33_333 == 0 && t < 333_330),
"decoded timestamp outside the fed set: {micros:?}"
);
}
fn decode_config(kind: super::Kind) -> super::Config {
super::Config {
kind,
..super::Config::new()
}
}
fn h264_software_encoder() -> Encoder {
Encoder::new(&EncodeConfig {
kind: EncodeKind::Software,
..EncodeConfig::new(320, 240, 30)
})
.expect("openh264 encoder")
}
#[test]
fn openh264_round_trip() {
let decoder = backend::open(Codec::H264, &decode_config(super::Kind::Software)).expect("openh264 decoder");
round_trip(h264_software_encoder(), decoder, "openh264");
}
#[test]
fn av1_is_supported_by_hardware_only() {
let catalog = hang::catalog::VideoConfig::new(hang::catalog::AV1::default());
let config = decode_config(super::Kind::Software);
let Err(err) = super::Decoder::new(&catalog, &config) else {
panic!("software AV1 decode unexpectedly opened");
};
assert!(matches!(err, crate::Error::NoDecoder(_)));
}
#[test]
fn av1_rejects_unsupported_catalog_shape() {
let av1 = hang::catalog::AV1 {
bitdepth: 10,
..hang::catalog::AV1::default()
};
let catalog = hang::catalog::VideoConfig::new(av1);
let config = decode_config(super::Kind::Auto);
let Err(err) = super::Decoder::new(&catalog, &config) else {
panic!("10-bit AV1 decode unexpectedly opened");
};
assert!(matches!(err, crate::Error::UnsupportedCodec(_)));
}
#[cfg(target_os = "macos")]
#[test]
fn videotoolbox_round_trip() {
let decoder = backend::open(Codec::H264, &decode_config(super::Kind::Named("videotoolbox".into())))
.expect("videotoolbox decoder");
round_trip(h264_software_encoder(), decoder, "videotoolbox");
}
#[cfg(target_os = "macos")]
fn decode_gray(count: u64) -> Vec<Frame> {
let mut encoder = h264_software_encoder();
let mut decoder = backend::open(Codec::H264, &decode_config(super::Kind::Named("videotoolbox".into())))
.expect("videotoolbox decoder");
let mut decoded = Vec::new();
for i in 0..count {
let keyframe = i == 0;
if keyframe {
encoder.keyframe();
}
for encoded in encoder.encode(&gray_frame(i)).unwrap() {
decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, keyframe).unwrap());
}
}
assert!(!decoded.is_empty(), "decoder produced no frames");
decoded
}
#[cfg(target_os = "macos")]
#[test]
fn videotoolbox_decode_stays_gpu_resident() {
for out in &decode_gray(3) {
assert!(
matches!(out.surface, Surface::PixelBuffer(_)),
"VideoToolbox decode downloaded to the CPU instead of keeping its surface"
);
}
}
#[cfg(target_os = "macos")]
#[test]
fn videotoolbox_resized_surface_reencodes_in_place() {
let decoded = decode_gray(3);
let resized: Vec<_> = decoded
.iter()
.map(|frame| frame.resize(crate::Size::new(160, 120)).unwrap())
.collect();
for frame in &resized {
assert_eq!(frame.size(), crate::Size::new(160, 120));
assert!(
matches!(frame.surface, Surface::PixelBuffer(_)),
"VideoToolbox resize downloaded to the CPU"
);
}
let encoder = Encoder::new(&EncodeConfig {
kind: EncodeKind::Named("videotoolbox".into()),
..EncodeConfig::new(160, 120, 30)
});
let Ok(mut encoder) = encoder else {
eprintln!("skipping: no VideoToolbox H.264 hardware encoder available");
return;
};
let mut packets = 0;
for (i, out) in resized.iter().enumerate() {
if i == 0 {
encoder.keyframe();
}
packets += encoder.encode(out).unwrap().len();
}
packets += encoder.finish().unwrap().len();
assert!(packets > 0, "re-encoding decoded surfaces produced no packets");
}
#[cfg(target_os = "macos")]
#[test]
fn videotoolbox_hevc_round_trip() {
let encoder = Encoder::new(&EncodeConfig {
kind: EncodeKind::Named("videotoolbox".into()),
codec: crate::encode::Codec::H265,
..EncodeConfig::new(320, 240, 30)
});
let Ok(encoder) = encoder else {
eprintln!("skipping: no VideoToolbox H.265 hardware encoder available");
return;
};
let decoder = backend::open(Codec::H265, &decode_config(super::Kind::Named("videotoolbox".into())))
.expect("videotoolbox H.265 decoder");
round_trip(encoder, decoder, "videotoolbox");
}
#[cfg(target_os = "windows")]
#[test]
fn mediafoundation_round_trip() {
let Ok(decoder) = backend::open(
Codec::H264,
&decode_config(super::Kind::Named("mediafoundation".into())),
) else {
eprintln!("skipping: no Media Foundation H.264 hardware decoder available");
return;
};
round_trip(h264_software_encoder(), decoder, "mediafoundation");
}
#[cfg(target_os = "windows")]
#[test]
fn mediafoundation_hevc_round_trip() {
let encoder = Encoder::new(&EncodeConfig {
kind: EncodeKind::Named("mediafoundation".into()),
codec: crate::encode::Codec::H265,
..EncodeConfig::new(320, 240, 30)
});
let Ok(encoder) = encoder else {
eprintln!("skipping: no Media Foundation H.265 hardware encoder available");
return;
};
let Ok(decoder) = backend::open(
Codec::H265,
&decode_config(super::Kind::Named("mediafoundation".into())),
) else {
eprintln!("skipping: no Media Foundation H.265 hardware decoder available");
return;
};
round_trip(encoder, decoder, "mediafoundation");
}
}