1mod export;
11mod import;
12mod split;
13mod sps;
14
15pub use export::*;
16pub use import::*;
17pub use split::*;
18
19use bytes::{Buf, BufMut, Bytes, BytesMut};
20use scuffle_h265::{NALUnitType, SpsNALUnit};
21use sps::Sps;
22
23pub(crate) fn hvc1_frame(
31 data: impl moq_net::IntoBytes,
32 length_size: usize,
33 pts: moq_net::Timestamp,
34) -> crate::Result<crate::container::Frame> {
35 let keyframe = hvc1_is_keyframe(data.as_ref(), length_size);
36 Ok(crate::container::Frame {
37 timestamp: pts,
38 payload: data.into_bytes(),
39 keyframe,
40 duration: None,
41 })
42}
43
44fn hvc1_is_keyframe(data: &[u8], length_size: usize) -> bool {
46 let Ok(nals) = crate::codec::annexb::length_prefixed_nals(data, length_size) else {
47 return false;
48 };
49 nals.map_while(std::result::Result::ok)
50 .any(|nal| nal.first().is_some_and(|header| is_irap(split::nal_unit_type(*header))))
51}
52
53pub(crate) fn is_irap(nal_type: NALUnitType) -> bool {
56 matches!(
57 nal_type,
58 NALUnitType::IdrWRadl
59 | NALUnitType::IdrNLp
60 | NALUnitType::BlaNLp
61 | NALUnitType::BlaWRadl
62 | NALUnitType::BlaWLp
63 | NALUnitType::CraNut
64 )
65}
66
67#[derive(Debug, Clone, thiserror::Error)]
69#[non_exhaustive]
70pub enum Error {
71 #[error("NAL unit is too short")]
72 NalTooShort,
73
74 #[error("{0} too large for hvcC length field ({1} > {max})", max = u16::MAX)]
75 NalTooLargeForHvcc(&'static str, usize),
76
77 #[error("too many {0} for hvcC ({1} > {max})", max = u16::MAX)]
78 TooManyNals(&'static str, usize),
79
80 #[error("NAL too large for 4-byte length prefix")]
81 NalTooLarge,
82
83 #[error("failed to parse SPS NAL unit")]
84 SpsParse,
85
86 #[error("missing level_idc in SPS")]
87 MissingLevelIdc,
88
89 #[error("forbidden zero bit is not zero")]
90 ForbiddenZeroBit,
91
92 #[error("not initialized")]
93 NotInitialized,
94
95 #[error("expected SPS before any frames")]
96 MissingSps,
97
98 #[error("missing timestamp")]
99 MissingTimestamp,
100
101 #[error("HEVCDecoderConfigurationRecord too short")]
102 HvccTooShort,
103
104 #[error("HEVCDecoderConfigurationRecord truncated")]
105 HvccTruncated,
106
107 #[error("hvc1 description for rendition {name:?} is missing VPS, SPS, or PPS (vps={vps}, sps={sps}, pps={pps})")]
108 MissingParamSets {
109 name: String,
110 vps: usize,
111 sps: usize,
112 pps: usize,
113 },
114
115 #[error("annexb: {0}")]
116 Annexb(#[from] crate::codec::annexb::Error),
117}
118
119pub type Result<T> = std::result::Result<T, Error>;
120
121#[derive(Debug, Clone)]
124#[non_exhaustive]
125pub struct Hvcc {
126 pub length_size: usize,
128 pub vps: Vec<Bytes>,
130 pub sps: Vec<Bytes>,
132 pub pps: Vec<Bytes>,
134}
135
136impl Hvcc {
137 pub fn parse(hvcc: &[u8]) -> Result<Self> {
140 if hvcc.len() < 23 {
141 return Err(Error::HvccTooShort);
142 }
143 let length_size = (hvcc[21] & 0x3) as usize + 1;
144 let num_arrays = hvcc[22] as usize;
145
146 let mut vps = Vec::new();
147 let mut sps = Vec::new();
148 let mut pps = Vec::new();
149 let mut pos: usize = 23;
150
151 for _ in 0..num_arrays {
152 let after_hdr = pos.checked_add(3).ok_or(Error::HvccTruncated)?;
153 if hvcc.len() < after_hdr {
154 return Err(Error::HvccTruncated);
155 }
156 let nal_type = hvcc[pos] & 0x3f;
157 let num_nalus = u16::from_be_bytes([hvcc[pos + 1], hvcc[pos + 2]]) as usize;
158 pos = after_hdr;
159
160 for _ in 0..num_nalus {
161 let after_len = pos.checked_add(2).ok_or(Error::HvccTruncated)?;
162 if hvcc.len() < after_len {
163 return Err(Error::HvccTruncated);
164 }
165 let len = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize;
166 let after_nal = after_len.checked_add(len).ok_or(Error::HvccTruncated)?;
167 if hvcc.len() < after_nal {
168 return Err(Error::HvccTruncated);
169 }
170 let bytes = Bytes::copy_from_slice(&hvcc[after_len..after_nal]);
171 pos = after_nal;
172
173 match NALUnitType::from(nal_type) {
174 NALUnitType::VpsNut => vps.push(bytes),
175 NALUnitType::SpsNut => sps.push(bytes),
176 NALUnitType::PpsNut => pps.push(bytes),
177 _ => {}
178 }
179 }
180 }
181
182 Ok(Self {
183 length_size,
184 vps,
185 sps,
186 pps,
187 })
188 }
189}
190
191pub(crate) fn config_from_hvcc(hvcc: &[u8]) -> Result<hang::catalog::VideoConfig> {
200 let params = Hvcc::parse(hvcc)?;
201 let sps_nal = params.sps.first().ok_or(Error::MissingSps)?;
202 let sps = Sps::parse(sps_nal)?.head;
203
204 let mut config = hang::catalog::VideoConfig::new(hang::catalog::H265 {
205 in_band: false,
206 profile_space: sps.profile_space,
207 profile_idc: sps.profile_idc,
208 profile_compatibility_flags: sps.profile_compatibility_flags,
209 tier_flag: sps.tier_flag,
210 level_idc: sps.level_idc,
211 constraint_flags: sps.constraint_flags,
212 });
213 config.coded_width = Some(sps.width);
214 config.coded_height = Some(sps.height);
215 config.description = Some(Bytes::copy_from_slice(hvcc));
216 Ok(config)
217}
218
219pub(crate) fn sps_reorder(nal: &[u8]) -> Option<crate::codec::video::Reorder> {
225 let sps = SpsNALUnit::parse(&mut &nal[..]).ok()?;
226 let highest = sps.rbsp.sps_max_sub_layers_minus1 as usize;
227 let reorder = &sps.rbsp.sub_layer_ordering_info.sps_max_num_reorder_pics;
228 let depth = *reorder.get(highest).or(reorder.last())?;
229 Some(crate::codec::video::Reorder {
230 depth: u32::try_from(depth).ok()?,
231 period: sps_period(&sps.rbsp),
232 })
233}
234
235pub(crate) fn sps_period(sps: &scuffle_h265::SpsRbsp) -> Option<(u64, u64)> {
239 let highest = sps.sps_max_sub_layers_minus1 as usize;
240 let timing = sps.vui_parameters.as_ref()?.vui_timing_info.as_ref()?;
241 let sub_layers = &timing.hrd_parameters.as_ref()?.sub_layers;
242 let elemental = sub_layers
243 .get(highest)
244 .or(sub_layers.last())?
245 .elemental_duration_in_tc_minus1?;
246 let units = elemental
247 .checked_add(1)?
248 .checked_mul(u64::from(timing.num_units_in_tick.get()))?;
249 Some((units, u64::from(timing.time_scale.get())))
250}
251
252pub struct Hvc1 {
259 hvcc: Option<Bytes>,
260 vps: Vec<Bytes>,
262 sps: Vec<Bytes>,
264 pps: Vec<Bytes>,
266}
267
268impl Default for Hvc1 {
269 fn default() -> Self {
270 Self::new()
271 }
272}
273
274impl Hvc1 {
275 pub fn new() -> Self {
277 Self {
278 hvcc: None,
279 vps: Vec::new(),
280 sps: Vec::new(),
281 pps: Vec::new(),
282 }
283 }
284
285 pub fn hvcc(&self) -> Option<&Bytes> {
287 self.hvcc.as_ref()
288 }
289
290 pub fn transform(&mut self, payload: Bytes) -> Result<Option<Bytes>> {
298 let mut buf = payload.clone();
299 let mut nal_iter = crate::codec::annexb::NalIterator::new(&mut buf);
300
301 let mut out = BytesMut::with_capacity(payload.remaining());
302 let mut frame_vps: Vec<Bytes> = Vec::new();
303 let mut frame_sps: Vec<Bytes> = Vec::new();
304 let mut frame_pps: Vec<Bytes> = Vec::new();
305 let mut emitted_any_slice = false;
306
307 loop {
308 let nal = match nal_iter.next() {
309 Some(Ok(n)) => n,
310 Some(Err(e)) => return Err(e.into()),
311 None => break,
312 };
313 if process_nal(&nal, &mut out, &mut frame_vps, &mut frame_sps, &mut frame_pps)? {
314 emitted_any_slice = true;
315 }
316 }
317
318 if let Some(nal) = nal_iter.flush()?
319 && process_nal(&nal, &mut out, &mut frame_vps, &mut frame_sps, &mut frame_pps)?
320 {
321 emitted_any_slice = true;
322 }
323
324 let mut changed = false;
329 if !frame_vps.is_empty() && frame_vps != self.vps {
330 self.vps = frame_vps;
331 changed = true;
332 }
333 if !frame_sps.is_empty() && frame_sps != self.sps {
334 self.sps = frame_sps;
335 changed = true;
336 }
337 if !frame_pps.is_empty() && frame_pps != self.pps {
338 self.pps = frame_pps;
339 changed = true;
340 }
341 if changed {
342 self.rebuild_hvcc()?;
343 }
344
345 if !emitted_any_slice {
346 return Ok(None);
347 }
348
349 Ok(Some(out.freeze()))
350 }
351
352 fn rebuild_hvcc(&mut self) -> Result<()> {
353 if self.vps.is_empty() || self.sps.is_empty() || self.pps.is_empty() {
354 return Ok(());
355 }
356 self.hvcc = Some(build_hvcc(&self.vps, &self.sps, &self.pps)?);
357 Ok(())
358 }
359}
360
361fn process_nal(
365 nal: &Bytes,
366 out: &mut BytesMut,
367 frame_vps: &mut Vec<Bytes>,
368 frame_sps: &mut Vec<Bytes>,
369 frame_pps: &mut Vec<Bytes>,
370) -> Result<bool> {
371 if nal.is_empty() {
372 return Ok(false);
373 }
374 match NALUnitType::from((nal[0] >> 1) & 0x3f) {
376 NALUnitType::VpsNut => {
377 crate::codec::annexb::push_distinct(frame_vps, nal);
378 Ok(false)
379 }
380 NALUnitType::SpsNut => {
381 crate::codec::annexb::push_distinct(frame_sps, nal);
382 Ok(false)
383 }
384 NALUnitType::PpsNut => {
385 crate::codec::annexb::push_distinct(frame_pps, nal);
386 Ok(false)
387 }
388 _ => {
389 let len = u32::try_from(nal.len()).map_err(|_| Error::NalTooLarge)?;
390 out.extend_from_slice(&len.to_be_bytes());
391 out.extend_from_slice(nal);
392 Ok(true)
393 }
394 }
395}
396
397pub(crate) fn build_hvcc(vps_nals: &[Bytes], sps_nals: &[Bytes], pps_nals: &[Bytes]) -> Result<Bytes> {
402 let first_sps = sps_nals.first().ok_or(Error::MissingSps)?;
403 for (label, nals) in [("VPS", vps_nals), ("SPS", sps_nals), ("PPS", pps_nals)] {
404 if nals.len() > u16::MAX as usize {
405 return Err(Error::TooManyNals(label, nals.len()));
406 }
407 for nal in nals {
408 if nal.len() > u16::MAX as usize {
409 return Err(Error::NalTooLargeForHvcc(label, nal.len()));
410 }
411 }
412 }
413
414 let sps = Sps::parse(first_sps)?.head;
415 let num_temporal_layers = sps.max_sub_layers_minus1 + 1;
416
417 let params_len: usize = vps_nals
418 .iter()
419 .chain(sps_nals)
420 .chain(pps_nals)
421 .map(|n| 2 + n.len())
422 .sum();
423 let mut out = BytesMut::with_capacity(23 + 3 * 3 + params_len);
424 out.put_u8(1); out.put_u8(((sps.profile_space & 0x3) << 6) | ((sps.tier_flag as u8) << 5) | (sps.profile_idc & 0x1f));
426 out.put_slice(&sps.profile_compatibility_flags);
427 out.put_slice(&sps.constraint_flags);
428 out.put_u8(sps.level_idc);
429 out.put_u16(0xf000); out.put_u8(0xfc); out.put_u8(0xfc | (sps.chroma_format_idc & 0x3));
432 out.put_u8(0xf8 | (sps.bit_depth_luma_minus8 & 0x7));
433 out.put_u8(0xf8 | (sps.bit_depth_chroma_minus8 & 0x7));
434 out.put_u16(0); out.put_u8(((num_temporal_layers & 0x7) << 3) | ((sps.temporal_id_nesting_flag as u8) << 2) | 0x3);
436 out.put_u8(3); for (nal_type, nals) in [
439 (u8::from(NALUnitType::VpsNut), vps_nals),
440 (u8::from(NALUnitType::SpsNut), sps_nals),
441 (u8::from(NALUnitType::PpsNut), pps_nals),
442 ] {
443 out.put_u8(0x80 | (nal_type & 0x3f)); out.put_u16(nals.len() as u16); for nal in nals {
446 out.put_u16(nal.len() as u16);
447 out.put_slice(nal);
448 }
449 }
450
451 Ok(out.freeze())
452}
453
454pub(crate) fn hvcc_params(hvcc: &[u8]) -> anyhow::Result<(usize, Vec<Bytes>)> {
459 anyhow::ensure!(hvcc.len() >= 23, "HEVCDecoderConfigurationRecord too short");
460 let length_size = (hvcc[21] & 0x03) as usize + 1;
461 let num_arrays = hvcc[22];
462
463 let mut params = Vec::new();
464 let mut pos = 23;
465 for _ in 0..num_arrays {
466 anyhow::ensure!(hvcc.len() >= pos + 3, "truncated hvcC NAL array header");
468 pos += 1;
469 let num_nalus = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]);
470 pos += 2;
471 for _ in 0..num_nalus {
472 anyhow::ensure!(hvcc.len() >= pos + 2, "truncated hvcC NAL length");
473 let len = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize;
474 pos += 2;
475 anyhow::ensure!(hvcc.len() >= pos + len, "hvcC NAL exceeds buffer");
476 params.push(Bytes::copy_from_slice(&hvcc[pos..pos + len]));
477 pos += len;
478 }
479 }
480
481 Ok((length_size, params))
482}
483
484pub(crate) fn pack_constraint_flags(profile: &scuffle_h265::Profile) -> [u8; 6] {
486 let mut flags = [0u8; 6];
487 flags[0] = ((profile.progressive_source_flag as u8) << 7)
488 | ((profile.interlaced_source_flag as u8) << 6)
489 | ((profile.non_packed_constraint_flag as u8) << 5)
490 | ((profile.frame_only_constraint_flag as u8) << 4);
491 flags
492}
493
494#[cfg(test)]
497pub(crate) mod fixtures {
498 use bytes::Bytes;
499
500 pub(crate) const VPS: &[u8] = &[
501 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00,
502 0x03, 0x00, 0x5d, 0x95, 0x98, 0x09,
503 ];
504 pub(crate) const SPS: &[u8] = &[
505 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x5d,
506 0xa0, 0x02, 0x80, 0x80, 0x2d, 0x16, 0x59, 0x59, 0xa4, 0x93, 0x2b, 0xc0, 0x5a, 0x02, 0x00, 0x00, 0x03, 0x00,
507 0x02, 0x00, 0x00, 0x03, 0x00, 0x3c, 0x10,
508 ];
509 pub(crate) const PPS: &[u8] = &[0x44, 0x01, 0xc1, 0x72, 0xb4, 0x62, 0x40];
510 pub(crate) const SPS_PYRAMID: &[u8] = &[
513 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x3c,
514 0xa0, 0x20, 0x81, 0x05, 0x96, 0x56, 0x59, 0x24, 0xca, 0xf0, 0x16, 0x80, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80,
515 0x00, 0x00, 0x0c, 0xb0, 0x0a, 0x48, 0x2f, 0x00, 0x07, 0xa1, 0x20, 0x00, 0xf4, 0x24, 0x40,
516 ];
517
518 pub(crate) const SPS_ZERO_COLOUR: &[u8] = &[
523 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00,
524 0x96, 0xa0, 0x03, 0xc0, 0x80, 0x11, 0x07, 0xcb, 0x8a, 0xad, 0x3b, 0xa2, 0x4b, 0xb9, 0x08, 0x00, 0x00, 0x03,
525 0x00, 0x20, 0x05, 0x26, 0x5c, 0x00, 0x33, 0x7f, 0x98, 0x01,
526 ];
527
528 pub(crate) const SPS_NVENC: &[u8] = &[
531 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78,
532 0xa0, 0x02, 0x80, 0x80, 0x2e, 0x1f, 0x13, 0x96, 0x5d, 0x29, 0x08, 0x46, 0x45, 0xd5, 0x0c, 0x04, 0x04, 0x00,
533 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf1, 0x80, 0x17, 0x79, 0x44, 0x00, 0x02, 0xdc, 0x6c, 0x00,
534 0x00, 0x2d, 0xc6, 0xc0, 0x40,
535 ];
536
537 pub(crate) const SPS_ANAMORPHIC: &[u8] = &[
539 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78,
540 0xa0, 0x02, 0xd0, 0x80, 0x11, 0x07, 0xcb, 0x96, 0x5d, 0x29, 0x08, 0x46, 0x45, 0xd5, 0x0c, 0x38, 0x04, 0x00,
541 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x79, 0x80, 0x17, 0x79, 0x44, 0x00, 0x02, 0xdc, 0x6c, 0x00,
542 0x00, 0xf4, 0x24, 0x04,
543 ];
544
545 pub(crate) fn hvcc() -> Bytes {
547 super::build_hvcc(
548 &[Bytes::from_static(VPS)],
549 &[Bytes::from_static(SPS)],
550 &[Bytes::from_static(PPS)],
551 )
552 .expect("real parameter sets must build an hvcC")
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559
560 fn length_prefixed(nals: &[&[u8]]) -> Vec<u8> {
562 let mut au = Vec::new();
563 for nal in nals {
564 au.extend_from_slice(&(nal.len() as u32).to_be_bytes());
565 au.extend_from_slice(nal);
566 }
567 au
568 }
569
570 fn ts() -> moq_net::Timestamp {
571 moq_net::Timestamp::from_micros(0).unwrap()
572 }
573
574 #[test]
575 fn sps_reorder_reads_the_highest_sub_layer() {
576 let reorder = sps_reorder(fixtures::SPS_PYRAMID).unwrap();
577 assert_eq!(reorder.depth, 2);
578 assert_eq!(reorder.period, Some((1, 25)));
579 assert_eq!(sps_reorder(&fixtures::SPS[..8]), None);
580 }
581
582 #[test]
586 fn hvc1_frame_keyframe() {
587 let sei: &[u8] = &[0x4e, 0x01, 0x05, 0xff]; let idr: &[u8] = &[0x26, 0x01, 0x80, 0xaa]; let au = length_prefixed(&[sei, idr]);
590
591 let frame = hvc1_frame(&au, 4, ts()).unwrap();
592 assert!(frame.keyframe);
593 assert_eq!(frame.payload, au);
594 }
595
596 #[test]
598 fn hvc1_frame_cra_keyframe() {
599 let cra: &[u8] = &[0x2a, 0x01, 0x80, 0x55]; let au = length_prefixed(&[cra]);
601
602 let frame = hvc1_frame(&au, 4, ts()).unwrap();
603 assert!(frame.keyframe);
604 }
605
606 #[test]
608 fn hvc1_frame_delta() {
609 let trail: &[u8] = &[0x02, 0x01, 0x80, 0x33]; let au = length_prefixed(&[trail]);
611
612 let frame = hvc1_frame(&au, 4, ts()).unwrap();
613 assert!(!frame.keyframe);
614 }
615
616 #[test]
620 fn hvc1_frame_tsa_delta_not_h264_idr() {
621 let tsa: &[u8] = &[0x05, 0x01, 0x80, 0x33]; let au = length_prefixed(&[tsa]);
623
624 let frame = hvc1_frame(&au, 4, ts()).unwrap();
625 assert!(!frame.keyframe);
626 }
627
628 #[test]
631 fn config_from_hvcc_resolves_real_sps() {
632 let hvcc = fixtures::hvcc();
633 let config = config_from_hvcc(&hvcc).unwrap();
634
635 let hang::catalog::VideoCodec::H265(h265) = &config.codec else {
636 panic!("expected H.265 codec")
637 };
638 assert!(!h265.in_band, "hvcC config is out-of-band");
639 assert_eq!(config.coded_width, Some(1280));
640 assert_eq!(config.coded_height, Some(720));
641 assert_eq!(config.description.as_deref(), Some(hvcc.as_ref()));
642 }
643
644 #[test]
648 fn hvcc_params_parses_vps_sps_pps() {
649 let vps = &[0x40, 0x01, 0x0c][..]; let sps = &[0x42, 0x01, 0x01, 0x60][..]; let pps = &[0x44, 0x01, 0xc0][..]; let mut hvcc = BytesMut::new();
654 hvcc.extend_from_slice(&[0u8; 21]); hvcc.put_u8(0xfc | 0x03); hvcc.put_u8(3); for (nal_type, nal) in [
658 (u8::from(NALUnitType::VpsNut), vps),
659 (u8::from(NALUnitType::SpsNut), sps),
660 (u8::from(NALUnitType::PpsNut), pps),
661 ] {
662 hvcc.put_u8(0x80 | (nal_type & 0x3f));
663 hvcc.put_u16(1); hvcc.put_u16(nal.len() as u16);
665 hvcc.put_slice(nal);
666 }
667
668 let (length_size, params) = hvcc_params(&hvcc).unwrap();
669 assert_eq!(length_size, 4);
670 assert_eq!(params.len(), 3);
671 assert_eq!(params[0].as_ref(), vps);
672 assert_eq!(params[1].as_ref(), sps);
673 assert_eq!(params[2].as_ref(), pps);
674 }
675
676 #[test]
679 fn hev1_config_tolerates_zeroed_colour_description() {
680 let mut annexb = Vec::new();
681 for nal in [fixtures::VPS, fixtures::SPS_ZERO_COLOUR, fixtures::PPS] {
682 annexb.extend_from_slice(&[0, 0, 0, 1]);
683 annexb.extend_from_slice(nal);
684 }
685 let config = config(&annexb).expect("the SPS head is well-formed");
686 let hang::catalog::VideoCodec::H265(h265) = &config.codec else {
687 panic!("expected H.265 codec")
688 };
689 assert!(h265.in_band);
690 assert_eq!(h265.to_string(), "hev1.1.6.L150");
691 assert_eq!((config.coded_width, config.coded_height), (Some(1920), Some(1080)));
692 }
693
694 #[test]
697 fn hvcc_tolerates_zeroed_colour_description() {
698 let hvcc = build_hvcc(
699 &[Bytes::from_static(fixtures::VPS)],
700 &[Bytes::from_static(fixtures::SPS_ZERO_COLOUR)],
701 &[Bytes::from_static(fixtures::PPS)],
702 )
703 .expect("the SPS head is well-formed");
704 assert_eq!(
705 &hvcc[..23],
706 &[
707 0x01, 0x01, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96, 0xf0, 0x00, 0xfc, 0xfd,
708 0xf8, 0xf8, 0x00, 0x00, 0x0f, 0x03,
709 ]
710 );
711 let config = config_from_hvcc(&hvcc).unwrap();
712 assert_eq!((config.coded_width, config.coded_height), (Some(1920), Some(1080)));
713 }
714
715 #[test]
718 fn bitstream_and_catalog_paths_agree() {
719 use std::str::FromStr;
720
721 let config = config_from_hvcc(&fixtures::hvcc()).unwrap();
722 let hang::catalog::VideoCodec::H265(h265) = &config.codec else {
723 panic!("expected H.265 codec")
724 };
725
726 assert_eq!(h265.profile_compatibility_flags, [0x60, 0, 0, 0]);
728
729 let encoded = h265.to_string();
730 assert_eq!(encoded, "hvc1.1.6.L93.90");
731
732 let parsed = hang::catalog::H265::from_str(&encoded).unwrap();
733 assert_eq!(parsed.profile_compatibility_flags, h265.profile_compatibility_flags);
734 }
735}