1use std::time::Duration;
12
13use bytes::Bytes;
14use hang::catalog::{AV1, VideoCodec, VideoConfig};
15use moq_mux::codec::{annexb, h264, h265};
16use moq_net::Timestamp;
17
18use super::Frame;
19use super::backend::{self, Backend, Codec};
20use crate::{Error, Size};
21
22#[derive(Clone, Debug, Default, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum Kind {
27 #[default]
29 Auto,
30 Hardware,
32 Software,
34 Named(String),
37}
38
39#[derive(Clone, Debug, Default)]
44#[non_exhaustive]
45pub struct Config {
46 pub kind: Kind,
48 pub latency_max: Option<Duration>,
52 pub resize: Option<Size>,
58}
59
60impl Config {
61 pub fn new() -> Self {
63 Self::default()
64 }
65}
66
67enum Conversion {
69 Passthrough,
72 LengthPrefixed { length_size: usize, keyframe_prefix: Bytes },
76}
77
78pub struct Decoder {
86 backend: Box<dyn Backend>,
87 conversion: Conversion,
88 got_keyframe: bool,
89}
90
91impl Decoder {
92 pub fn new(catalog: &VideoConfig, config: &Config) -> Result<Self, Error> {
95 let (codec, conversion) = match &catalog.codec {
96 VideoCodec::H264(h264) => {
97 let conversion = if h264.inline {
98 Conversion::Passthrough
99 } else {
100 let avcc = catalog.description.as_ref().ok_or_else(|| {
101 Error::Codec(anyhow::anyhow!("avc1 H.264 track is missing its avcC description"))
102 })?;
103 let params = h264::Avcc::parse(avcc).map_err(moq_mux::Error::from)?;
104 let keyframe_prefix = annexb::build_prefix(params.sps.iter().chain(params.pps.iter()));
105 Conversion::LengthPrefixed {
106 length_size: params.length_size,
107 keyframe_prefix,
108 }
109 };
110 (Codec::H264, conversion)
111 }
112 VideoCodec::H265(h265) => {
113 let conversion = if h265.in_band {
114 Conversion::Passthrough
115 } else {
116 let hvcc = catalog.description.as_ref().ok_or_else(|| {
117 Error::Codec(anyhow::anyhow!("hvc1 H.265 track is missing its hvcC description"))
118 })?;
119 let params = h265::Hvcc::parse(hvcc).map_err(moq_mux::Error::from)?;
120 let keyframe_prefix =
121 annexb::build_prefix(params.vps.iter().chain(params.sps.iter()).chain(params.pps.iter()));
122 Conversion::LengthPrefixed {
123 length_size: params.length_size,
124 keyframe_prefix,
125 }
126 };
127 (Codec::H265, conversion)
128 }
129 VideoCodec::AV1(av1) if is_supported_av1(av1) => (Codec::Av1, Conversion::Passthrough),
130 other => return Err(Error::UnsupportedCodec(other.to_string())),
131 };
132
133 let backend = backend::open(codec, config)?;
134 tracing::debug!(decoder = backend.name(), "opened video decoder");
135 Ok(Self {
136 backend,
137 conversion,
138 got_keyframe: false,
139 })
140 }
141
142 pub fn name(&self) -> &str {
144 self.backend.name()
145 }
146
147 pub fn decode(&mut self, payload: &Bytes, timestamp: Timestamp, keyframe: bool) -> Result<Vec<Frame>, Error> {
153 if !self.got_keyframe {
156 if !keyframe {
157 return Ok(Vec::new());
158 }
159 self.got_keyframe = true;
160 }
161
162 let access_unit = match &self.conversion {
163 Conversion::Passthrough => payload.clone(),
165 Conversion::LengthPrefixed {
166 length_size,
167 keyframe_prefix,
168 } => {
169 let prefix = keyframe.then(|| keyframe_prefix.as_ref());
170 annexb::from_length_prefixed(payload, *length_size, prefix).map_err(moq_mux::Error::from)?
171 }
172 };
173
174 Ok(self
175 .backend
176 .decode(access_unit, timestamp, keyframe)?
177 .into_iter()
178 .map(|decoded| Frame {
179 timestamp: decoded.timestamp,
180 size: Size::new(decoded.frame.width(), decoded.frame.height()),
181 surface: decoded.frame,
182 })
183 .collect())
184 }
185}
186
187fn is_supported_av1(av1: &AV1) -> bool {
188 av1.bitdepth == 8 && !av1.mono_chrome && av1.chroma_subsampling_x && av1.chroma_subsampling_y
189}
190
191#[cfg(test)]
192mod tests {
193 use moq_net::Timestamp;
194
195 use super::backend::{self, Codec};
196 use crate::encode::{Config as EncodeConfig, Encoder, Kind as EncodeKind};
197 use crate::frame::I420;
198
199 fn gray_rgba(width: u32, height: u32) -> Vec<u8> {
201 vec![0x80u8; width as usize * height as usize * 4]
202 }
203
204 fn assert_gray(i420: &I420, width: u32, height: u32) {
209 assert_eq!(i420.width, width);
210 assert_eq!(i420.height, height);
211 let luma = (width * height) as usize;
212 assert_eq!(i420.data.len(), luma * 3 / 2);
214
215 let avg = |plane: &[u8]| plane.iter().map(|&b| b as u32).sum::<u32>() / plane.len() as u32;
216 let y = avg(&i420.data[..luma]);
217 let u = avg(&i420.data[luma..luma + luma / 4]);
218 let v = avg(&i420.data[luma + luma / 4..]);
219 assert!((110..=140).contains(&y), "luma {y} off for a gray frame");
220 assert!((118..=138).contains(&u), "u {u} off for a gray frame");
221 assert!((118..=138).contains(&v), "v {v} off for a gray frame");
222 }
223
224 fn round_trip(mut encoder: Encoder, mut decoder: Box<dyn backend::Backend>, expect_name: &str) {
228 assert_eq!(decoder.name(), expect_name);
229
230 let frame = gray_rgba(320, 240);
231 let mut decoded = Vec::new();
232 for i in 0..10u64 {
233 let keyframe = i == 0;
234 let timestamp = Timestamp::from_micros(i * 33_333).unwrap();
236 for packet in encoder
237 .encode_rgba(&frame, crate::Size::new(320, 240), keyframe)
238 .unwrap()
239 {
240 decoded.extend(decoder.decode(packet, timestamp, keyframe).unwrap());
241 }
242 }
243
244 assert!(!decoded.is_empty(), "decoder produced no frames");
245 for out in &decoded {
246 assert_gray(&out.frame.to_i420().unwrap(), 320, 240);
247 }
248
249 let micros: Vec<u128> = decoded.iter().map(|d| d.timestamp.as_micros()).collect();
253 assert!(
254 micros.windows(2).all(|w| w[0] < w[1]),
255 "decoded timestamps not strictly increasing: {micros:?}"
256 );
257 assert!(
258 micros.iter().all(|&t| t % 33_333 == 0 && t < 333_330),
259 "decoded timestamp outside the fed set: {micros:?}"
260 );
261 }
262
263 fn decode_config(kind: super::Kind) -> super::Config {
265 super::Config {
266 kind,
267 ..super::Config::new()
268 }
269 }
270
271 fn h264_software_encoder() -> Encoder {
273 Encoder::new(&EncodeConfig {
274 kind: EncodeKind::Software,
275 ..EncodeConfig::new(320, 240, 30)
276 })
277 .expect("openh264 encoder")
278 }
279
280 #[test]
281 fn openh264_round_trip() {
282 let decoder = backend::open(Codec::H264, &decode_config(super::Kind::Software)).expect("openh264 decoder");
283 round_trip(h264_software_encoder(), decoder, "openh264");
284 }
285
286 #[test]
287 fn av1_is_supported_by_hardware_only() {
288 let catalog = hang::catalog::VideoConfig::new(hang::catalog::AV1::default());
289 let config = decode_config(super::Kind::Software);
290 let Err(err) = super::Decoder::new(&catalog, &config) else {
291 panic!("software AV1 decode unexpectedly opened");
292 };
293 assert!(matches!(err, crate::Error::NoDecoder(_)));
294 }
295
296 #[test]
297 fn av1_rejects_unsupported_catalog_shape() {
298 let av1 = hang::catalog::AV1 {
299 bitdepth: 10,
300 ..hang::catalog::AV1::default()
301 };
302 let catalog = hang::catalog::VideoConfig::new(av1);
303 let config = decode_config(super::Kind::Auto);
304 let Err(err) = super::Decoder::new(&catalog, &config) else {
305 panic!("10-bit AV1 decode unexpectedly opened");
306 };
307 assert!(matches!(err, crate::Error::UnsupportedCodec(_)));
308 }
309
310 #[cfg(target_os = "macos")]
311 #[test]
312 fn videotoolbox_round_trip() {
313 let decoder = backend::open(Codec::H264, &decode_config(super::Kind::Named("videotoolbox".into())))
314 .expect("videotoolbox decoder");
315 round_trip(h264_software_encoder(), decoder, "videotoolbox");
316 }
317
318 #[cfg(target_os = "macos")]
321 fn decode_gray(count: u64) -> Vec<backend::Decoded> {
322 let mut encoder = h264_software_encoder();
323 let mut decoder = backend::open(Codec::H264, &decode_config(super::Kind::Named("videotoolbox".into())))
324 .expect("videotoolbox decoder");
325
326 let rgba = gray_rgba(320, 240);
327 let mut decoded = Vec::new();
328 for i in 0..count {
329 let keyframe = i == 0;
330 let timestamp = Timestamp::from_micros(i * 33_333).unwrap();
331 for packet in encoder
332 .encode_rgba(&rgba, crate::Size::new(320, 240), keyframe)
333 .unwrap()
334 {
335 decoded.extend(decoder.decode(packet, timestamp, keyframe).unwrap());
336 }
337 }
338
339 assert!(!decoded.is_empty(), "decoder produced no frames");
340 decoded
341 }
342
343 #[cfg(target_os = "macos")]
348 #[test]
349 fn videotoolbox_decode_stays_gpu_resident() {
350 for out in &decode_gray(3) {
351 assert!(
352 matches!(out.frame, crate::frame::Surface::PixelBuffer(_)),
353 "VideoToolbox decode downloaded to the CPU instead of keeping its surface"
354 );
355 }
356 }
357
358 #[cfg(target_os = "macos")]
362 #[test]
363 fn videotoolbox_surface_reencodes_in_place() {
364 let decoded = decode_gray(3);
365
366 let encoder = Encoder::new(&EncodeConfig {
367 kind: EncodeKind::Named("videotoolbox".into()),
368 ..EncodeConfig::new(320, 240, 30)
369 });
370 let Ok(mut encoder) = encoder else {
371 eprintln!("skipping: no VideoToolbox H.264 hardware encoder available");
372 return;
373 };
374
375 let mut packets = 0;
376 for (i, out) in decoded.iter().enumerate() {
377 packets += encoder.encode(&out.frame, i == 0).unwrap().len();
378 }
379 packets += encoder.finish().unwrap().len();
380
381 assert!(packets > 0, "re-encoding decoded surfaces produced no packets");
382 }
383
384 #[cfg(target_os = "macos")]
389 #[test]
390 fn videotoolbox_hevc_round_trip() {
391 let encoder = Encoder::new(&EncodeConfig {
392 kind: EncodeKind::Named("videotoolbox".into()),
393 codec: crate::encode::Codec::H265,
394 ..EncodeConfig::new(320, 240, 30)
395 });
396 let Ok(encoder) = encoder else {
397 eprintln!("skipping: no VideoToolbox H.265 hardware encoder available");
398 return;
399 };
400 let decoder = backend::open(Codec::H265, &decode_config(super::Kind::Named("videotoolbox".into())))
401 .expect("videotoolbox H.265 decoder");
402 round_trip(encoder, decoder, "videotoolbox");
403 }
404
405 #[cfg(target_os = "windows")]
406 #[test]
407 fn mediafoundation_round_trip() {
408 let Ok(decoder) = backend::open(
411 Codec::H264,
412 &decode_config(super::Kind::Named("mediafoundation".into())),
413 ) else {
414 eprintln!("skipping: no Media Foundation H.264 hardware decoder available");
415 return;
416 };
417 round_trip(h264_software_encoder(), decoder, "mediafoundation");
418 }
419
420 #[cfg(target_os = "windows")]
425 #[test]
426 fn mediafoundation_hevc_round_trip() {
427 let encoder = Encoder::new(&EncodeConfig {
428 kind: EncodeKind::Named("mediafoundation".into()),
429 codec: crate::encode::Codec::H265,
430 ..EncodeConfig::new(320, 240, 30)
431 });
432 let Ok(encoder) = encoder else {
433 eprintln!("skipping: no Media Foundation H.265 hardware encoder available");
434 return;
435 };
436 let Ok(decoder) = backend::open(
437 Codec::H265,
438 &decode_config(super::Kind::Named("mediafoundation".into())),
439 ) else {
440 eprintln!("skipping: no Media Foundation H.265 hardware decoder available");
441 return;
442 };
443 round_trip(encoder, decoder, "mediafoundation");
444 }
445}