use alloc::vec::Vec;
use crate::annexb::{iter_annexb_nals, iter_length_prefixed_nals};
const AVC_NAL_TYPE_MASK: u8 = 0x1F;
const AVC_NAL_IDR: u8 = 5;
const AVC_NAL_SEI: u8 = 6;
const AVC_NAL_SPS: u8 = 7;
const AVC_SEI_PAYLOAD_TYPE_RECOVERY_POINT: u32 = 6;
const HEVC_NAL_TYPE_SHIFT: u8 = 1;
const HEVC_NAL_TYPE_MASK: u8 = 0x3F;
const HEVC_IRAP_FIRST: u8 = 16;
const HEVC_IRAP_LAST: u8 = 23;
const HEVC_NAL_SEI_PREFIX: u8 = 39;
const HEVC_NAL_SEI_SUFFIX: u8 = 40;
const VVC_NAL_TYPE_SHIFT: u8 = 3;
const VVC_NAL_TYPE_MASK: u8 = 0x1F;
const VVC_NAL_IDR_W_RADL: u8 = 7;
const VVC_NAL_IDR_N_LP: u8 = 8;
const VVC_NAL_CRA: u8 = 9;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum NalCodec {
Avc,
Hevc,
Vvc,
}
impl NalCodec {
pub fn name(&self) -> &'static str {
match self {
NalCodec::Avc => "AVC",
NalCodec::Hevc => "HEVC",
NalCodec::Vvc => "VVC",
}
}
fn header_len(self) -> usize {
match self {
NalCodec::Avc => 1,
NalCodec::Hevc | NalCodec::Vvc => 2,
}
}
}
broadcast_common::impl_spec_display!(NalCodec);
pub fn nal_unit_type(codec: NalCodec, nal: &[u8]) -> Option<u8> {
if nal.len() < codec.header_len() {
return None;
}
Some(match codec {
NalCodec::Avc => nal[0] & AVC_NAL_TYPE_MASK,
NalCodec::Hevc => (nal[0] >> HEVC_NAL_TYPE_SHIFT) & HEVC_NAL_TYPE_MASK,
NalCodec::Vvc => (nal[1] >> VVC_NAL_TYPE_SHIFT) & VVC_NAL_TYPE_MASK,
})
}
pub fn is_keyframe_nal(codec: NalCodec, nal: &[u8]) -> bool {
match nal_unit_type(codec, nal) {
None => false,
Some(t) => match codec {
NalCodec::Avc => t == AVC_NAL_IDR,
NalCodec::Hevc => (HEVC_IRAP_FIRST..=HEVC_IRAP_LAST).contains(&t),
NalCodec::Vvc => t == VVC_NAL_IDR_W_RADL || t == VVC_NAL_IDR_N_LP || t == VVC_NAL_CRA,
},
}
}
pub fn access_unit_is_keyframe(codec: NalCodec, au: &[u8], length_prefixed: bool) -> bool {
if length_prefixed {
match iter_length_prefixed_nals(au) {
Ok(nals) => nals.iter().any(|nal| is_keyframe_nal(codec, nal)),
Err(_) => false,
}
} else {
iter_annexb_nals(au).any(|nal| is_keyframe_nal(codec, nal))
}
}
struct EbspBytes<'a> {
nal: &'a [u8],
pos: usize,
zero_run: u8,
}
impl<'a> EbspBytes<'a> {
fn new(nal: &'a [u8]) -> Self {
Self {
nal,
pos: 0,
zero_run: 0,
}
}
}
impl Iterator for EbspBytes<'_> {
type Item = u8;
fn next(&mut self) -> Option<u8> {
while self.pos < self.nal.len() {
let b = self.nal[self.pos];
self.pos += 1;
if self.zero_run >= 2 && b == 0x03 {
self.zero_run = 0;
continue;
}
self.zero_run = if b == 0 { self.zero_run + 1 } else { 0 };
return Some(b);
}
None
}
}
fn read_sei_varint(bytes: &mut impl Iterator<Item = u8>) -> Option<u32> {
let mut value: u32 = 0;
loop {
let b = bytes.next()?;
value += u32::from(b);
if b != 0xFF {
return Some(value);
}
}
}
pub fn recovery_point_sei(nal: &[u8]) -> bool {
if nal_unit_type(NalCodec::Avc, nal) != Some(AVC_NAL_SEI) {
return false;
}
let mut bytes = EbspBytes::new(&nal[NalCodec::Avc.header_len()..]).peekable();
loop {
let Some(payload_type) = read_sei_varint(&mut bytes) else {
return false;
};
let Some(payload_size) = read_sei_varint(&mut bytes) else {
return false;
};
if payload_type == AVC_SEI_PAYLOAD_TYPE_RECOVERY_POINT {
return true;
}
for _ in 0..payload_size {
if bytes.next().is_none() {
return false;
}
}
if bytes.peek().is_none() {
return false;
}
}
}
pub fn access_unit_is_rap(codec: NalCodec, au: &[u8], length_prefixed: bool) -> bool {
match codec {
NalCodec::Hevc | NalCodec::Vvc => access_unit_is_keyframe(codec, au, length_prefixed),
NalCodec::Avc => {
let is_rap_nal = |nal: &[u8]| {
nal_unit_type(NalCodec::Avc, nal) == Some(AVC_NAL_SPS)
|| is_keyframe_nal(NalCodec::Avc, nal)
|| recovery_point_sei(nal)
};
if length_prefixed {
match iter_length_prefixed_nals(au) {
Ok(nals) => nals.iter().any(|nal| is_rap_nal(nal)),
Err(_) => false,
}
} else {
iter_annexb_nals(au).any(is_rap_nal)
}
}
}
}
const SEI_PAYLOAD_TYPE_USER_DATA_REGISTERED_ITU_T_T35: u32 = 4;
const ITU_T_T35_COUNTRY_CODE_USA: u8 = 0xB5;
const ATSC_T35_PROVIDER_CODE: u16 = 0x0031;
const ATSC_USER_IDENTIFIER_GA94: u32 = 0x4741_3934;
const ATSC_USER_DATA_TYPE_CODE_CC_DATA: u8 = 0x03;
const ATSC_T35_HEADER_LEN: usize = 8;
fn is_sei_nal(codec: NalCodec, nal: &[u8]) -> bool {
match codec {
NalCodec::Avc => nal_unit_type(codec, nal) == Some(AVC_NAL_SEI),
NalCodec::Hevc => matches!(
nal_unit_type(codec, nal),
Some(HEVC_NAL_SEI_PREFIX) | Some(HEVC_NAL_SEI_SUFFIX)
),
NalCodec::Vvc => false,
}
}
fn append_if_atsc_cc_data(payload: &[u8], out: &mut Vec<u8>) {
if payload.len() < ATSC_T35_HEADER_LEN {
return;
}
if payload[0] != ITU_T_T35_COUNTRY_CODE_USA {
return;
}
let provider_code = u16::from_be_bytes([payload[1], payload[2]]);
if provider_code != ATSC_T35_PROVIDER_CODE {
return;
}
let user_identifier = u32::from_be_bytes([payload[3], payload[4], payload[5], payload[6]]);
if user_identifier != ATSC_USER_IDENTIFIER_GA94 {
return;
}
if payload[7] != ATSC_USER_DATA_TYPE_CODE_CC_DATA {
return;
}
let cc = &payload[ATSC_T35_HEADER_LEN..];
const CC_DATA_HEADER_LEN: usize = 2;
const CC_TRIPLET_LEN: usize = 3;
const CC_DATA_MARKER_LEN: usize = 1;
const CC_COUNT_MASK: u8 = 0x1F;
if cc.len() < CC_DATA_HEADER_LEN {
return;
}
let cc_count = usize::from(cc[0] & CC_COUNT_MASK);
let total = CC_DATA_HEADER_LEN + cc_count * CC_TRIPLET_LEN + CC_DATA_MARKER_LEN;
if cc.len() < total {
return;
}
out.extend_from_slice(&cc[..total]);
}
fn append_atsc_cc_data_from_sei_nal(codec: NalCodec, nal: &[u8], out: &mut Vec<u8>) {
if !is_sei_nal(codec, nal) {
return;
}
let mut bytes = EbspBytes::new(&nal[codec.header_len()..]).peekable();
loop {
let Some(payload_type) = read_sei_varint(&mut bytes) else {
return;
};
let Some(payload_size) = read_sei_varint(&mut bytes) else {
return;
};
let payload_size = payload_size as usize;
if payload_type == SEI_PAYLOAD_TYPE_USER_DATA_REGISTERED_ITU_T_T35 {
let payload: Vec<u8> = (&mut bytes).take(payload_size).collect();
if payload.len() != payload_size {
return;
}
append_if_atsc_cc_data(&payload, out);
} else {
for _ in 0..payload_size {
if bytes.next().is_none() {
return;
}
}
}
if bytes.peek().is_none() {
return;
}
}
}
pub fn caption_cc_data(codec: NalCodec, au: &[u8], length_prefixed: bool) -> Vec<u8> {
let mut out = Vec::new();
if length_prefixed {
if let Ok(nals) = iter_length_prefixed_nals(au) {
for nal in nals {
append_atsc_cc_data_from_sei_nal(codec, nal, &mut out);
}
}
} else {
for nal in iter_annexb_nals(au) {
append_atsc_cc_data_from_sei_nal(codec, nal, &mut out);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn avc_nal_type_extraction() {
assert_eq!(nal_unit_type(NalCodec::Avc, &[0x67, 0x42]), Some(7));
assert_eq!(nal_unit_type(NalCodec::Avc, &[0x65, 0x88]), Some(5));
assert!(is_keyframe_nal(NalCodec::Avc, &[0x65, 0x88]));
assert!(!is_keyframe_nal(NalCodec::Avc, &[0x67, 0x42])); assert!(!is_keyframe_nal(NalCodec::Avc, &[0x41, 0x9a]));
assert_eq!(nal_unit_type(NalCodec::Avc, &[]), None);
assert!(!is_keyframe_nal(NalCodec::Avc, &[]));
}
#[test]
fn hevc_nal_type_extraction() {
assert_eq!(nal_unit_type(NalCodec::Hevc, &[0x42, 0x01]), Some(33));
assert!(!is_keyframe_nal(NalCodec::Hevc, &[0x42, 0x01]));
assert_eq!(nal_unit_type(NalCodec::Hevc, &[0x26, 0x01]), Some(19));
assert_eq!(nal_unit_type(NalCodec::Hevc, &[0x28, 0x01]), Some(20));
assert!(is_keyframe_nal(NalCodec::Hevc, &[0x26, 0x01]));
assert!(is_keyframe_nal(NalCodec::Hevc, &[0x28, 0x01]));
assert_eq!(nal_unit_type(NalCodec::Hevc, &[0x02, 0x01]), Some(1));
assert!(!is_keyframe_nal(NalCodec::Hevc, &[0x02, 0x01]));
assert_eq!(nal_unit_type(NalCodec::Hevc, &[0x26]), None);
}
#[test]
fn vvc_nal_type_extraction() {
assert_eq!(nal_unit_type(NalCodec::Vvc, &[0x00, 0x78]), Some(15));
assert!(!is_keyframe_nal(NalCodec::Vvc, &[0x00, 0x78]));
assert_eq!(nal_unit_type(NalCodec::Vvc, &[0x00, 0x38]), Some(7));
assert_eq!(nal_unit_type(NalCodec::Vvc, &[0x00, 0x40]), Some(8));
assert_eq!(nal_unit_type(NalCodec::Vvc, &[0x00, 0x48]), Some(9));
assert!(is_keyframe_nal(NalCodec::Vvc, &[0x00, 0x38]));
assert!(is_keyframe_nal(NalCodec::Vvc, &[0x00, 0x40]));
assert!(is_keyframe_nal(NalCodec::Vvc, &[0x00, 0x48]));
assert!(!is_keyframe_nal(NalCodec::Vvc, &[0x00, 0x00]));
}
#[test]
fn access_unit_annexb_and_length_prefixed_agree() {
let non_kf = [0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x00, 0x01, 0x41, 0x9a];
assert!(!access_unit_is_keyframe(NalCodec::Avc, &non_kf, false));
let kf = [0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x00, 0x01, 0x65, 0x88];
assert!(access_unit_is_keyframe(NalCodec::Avc, &kf, false));
let lp_non_kf = crate::annexb::annexb_to_length_prefixed(&non_kf);
let lp_kf = crate::annexb::annexb_to_length_prefixed(&kf);
assert!(!access_unit_is_keyframe(NalCodec::Avc, &lp_non_kf, true));
assert!(access_unit_is_keyframe(NalCodec::Avc, &lp_kf, true));
}
#[test]
fn malformed_length_prefixed_is_not_keyframe() {
let lp = [0x00, 0x00, 0x00, 0x63, 0x65, 0x88];
assert!(!access_unit_is_keyframe(NalCodec::Avc, &lp, true));
}
#[test]
fn nal_codec_display() {
assert_eq!(NalCodec::Avc.to_string(), "AVC");
assert_eq!(NalCodec::Hevc.to_string(), "HEVC");
assert_eq!(NalCodec::Vvc.to_string(), "VVC");
}
#[test]
fn recovery_point_sei_matches_payload_type_6() {
let nal = [0x06, 0x06, 0x00];
assert!(recovery_point_sei(&nal));
}
#[test]
fn other_sei_payload_type_is_not_recovery_point() {
let nal = [0x06, 0x01, 0x00];
assert!(!recovery_point_sei(&nal));
}
#[test]
fn recovery_point_sei_found_among_multiple_messages() {
let nal = [0x06, 0x00, 0x01, 0xAA, 0x06, 0x00];
assert!(recovery_point_sei(&nal));
}
#[test]
fn recovery_point_sei_rejects_non_sei_nal() {
let nal = [0x67, 0x42, 0x00];
assert!(!recovery_point_sei(&nal));
let short_sei = [0x06];
assert!(!recovery_point_sei(&short_sei));
}
#[test]
fn recovery_point_sei_unescapes_emulation_prevention() {
let nal = [
0x06, 0x00, 0x02, 0x00, 0x00,
0x03, 0x06, 0x00, ];
assert!(recovery_point_sei(&nal));
}
#[test]
fn access_unit_is_rap_recognises_open_gop_signals() {
let sps_led = [
0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x0A, 0x00, 0x00, 0x01, 0x68, 0xCE, 0x3C, 0x80, 0x00, 0x00, 0x01, 0x41, 0x9A, ];
assert!(access_unit_is_rap(NalCodec::Avc, &sps_led, false));
assert!(!access_unit_is_keyframe(NalCodec::Avc, &sps_led, false));
let sei_led = [
0x00, 0x00, 0x01, 0x06, 0x06, 0x00, 0x80, 0x00, 0x00, 0x01, 0x41, 0x9A, ];
assert!(access_unit_is_rap(NalCodec::Avc, &sei_led, false));
assert!(!access_unit_is_keyframe(NalCodec::Avc, &sei_led, false));
let plain = [0x00, 0x00, 0x01, 0x41, 0x9A];
assert!(!access_unit_is_rap(NalCodec::Avc, &plain, false));
let idr = [0x00, 0x00, 0x01, 0x65, 0x88];
assert!(access_unit_is_rap(NalCodec::Avc, &idr, false));
assert!(access_unit_is_keyframe(NalCodec::Avc, &idr, false));
}
#[test]
fn access_unit_is_rap_hevc_matches_keyframe_helper() {
let cra = [0x00, 0x00, 0x01, 0x2A, 0x01]; assert!(access_unit_is_keyframe(NalCodec::Hevc, &cra, false));
assert!(access_unit_is_rap(NalCodec::Hevc, &cra, false));
let trail = [0x00, 0x00, 0x01, 0x02, 0x01]; assert!(!access_unit_is_keyframe(NalCodec::Hevc, &trail, false));
assert!(!access_unit_is_rap(NalCodec::Hevc, &trail, false));
}
#[rustfmt::skip]
const REAL_A53_SEI_NAL: [u8; 82] = [
0x00, 0x00, 0x01, 0x06, 0x04, 0x47, 0xb5, 0x00, 0x31, 0x47, 0x41, 0x39,
0x34, 0x03, 0xd4, 0xff, 0xfc, 0x80, 0x80, 0xfd, 0x80, 0x80, 0xfa, 0x00,
0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00,
0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00,
0x00, 0xfa, 0x47, 0x01, 0xe1, 0x12, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa,
0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa,
0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xff, 0x80,
];
#[rustfmt::skip]
const REAL_A53_CC_DATA: [u8; 63] = [
0xd4, 0xff, 0xfc, 0x80, 0x80, 0xfd, 0x80, 0x80, 0xfa, 0x00, 0x00, 0xfa,
0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa,
0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa,
0x47, 0x01, 0xe1, 0x12, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00,
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00,
0xfa, 0x00, 0x00,
];
#[test]
fn caption_cc_data_extracts_real_atsc_a53_sei() {
let extracted = caption_cc_data(NalCodec::Avc, &REAL_A53_SEI_NAL, false);
assert_eq!(extracted, REAL_A53_CC_DATA);
}
#[test]
fn caption_cc_data_finds_sei_alongside_other_nals() {
let mut au = alloc::vec::Vec::new();
au.extend_from_slice(&[0x00, 0x00, 0x01, 0x41, 0x9a]); au.extend_from_slice(&REAL_A53_SEI_NAL);
au.extend_from_slice(&[0x00, 0x00, 0x01, 0x41, 0x9b]); let extracted = caption_cc_data(NalCodec::Avc, &au, false);
assert_eq!(extracted, REAL_A53_CC_DATA);
}
#[test]
fn caption_cc_data_length_prefixed_matches_annexb() {
let lp = crate::annexb::annexb_to_length_prefixed(&REAL_A53_SEI_NAL);
let extracted = caption_cc_data(NalCodec::Avc, &lp, true);
assert_eq!(extracted, REAL_A53_CC_DATA);
}
#[test]
fn caption_cc_data_ignores_non_caption_sei() {
let recovery_point = [0x00, 0x00, 0x01, 0x06, 0x06, 0x00];
assert!(caption_cc_data(NalCodec::Avc, &recovery_point, false).is_empty());
let pic_timing = [0x00, 0x00, 0x01, 0x06, 0x01, 0x02, 0xAA, 0xBB];
assert!(caption_cc_data(NalCodec::Avc, &pic_timing, false).is_empty());
let wrong_signature = [
0x00, 0x00, 0x01, 0x06, 0x04, 0x08, 0xB5, 0x00, 0x99, b'X', b'X', b'X', b'X', 0x03, ];
assert!(caption_cc_data(NalCodec::Avc, &wrong_signature, false).is_empty());
}
#[test]
fn caption_cc_data_rejects_declared_cc_count_overrunning_payload() {
let truncated = [
0x00, 0x00, 0x01, 0x06, 0x04, 0x0A, 0xB5, 0x00, 0x31, 0x47, 0x41, 0x39, 0x34, 0x03, 0x9F, 0xFF, ];
assert!(caption_cc_data(NalCodec::Avc, &truncated, false).is_empty());
}
#[test]
fn caption_cc_data_hevc_prefix_and_suffix_sei() {
let t35_and_cc_data = &REAL_A53_SEI_NAL[6..]; for (label, nal_type) in [
("prefix", HEVC_NAL_SEI_PREFIX),
("suffix", HEVC_NAL_SEI_SUFFIX),
] {
let mut au = alloc::vec::Vec::new();
au.extend_from_slice(&[0x00, 0x00, 0x01]);
au.push(nal_type << HEVC_NAL_TYPE_SHIFT); au.push(0x01); au.push(0x04); au.push(0x47); au.extend_from_slice(t35_and_cc_data);
let extracted = caption_cc_data(NalCodec::Hevc, &au, false);
assert_eq!(extracted, REAL_A53_CC_DATA, "HEVC {label} SEI");
}
}
#[test]
fn caption_cc_data_vvc_is_always_empty() {
let extracted = caption_cc_data(NalCodec::Vvc, &REAL_A53_SEI_NAL, false);
assert!(extracted.is_empty());
}
#[test]
fn caption_cc_data_no_panic_on_arbitrary_and_truncated_bytes() {
for codec in [NalCodec::Avc, NalCodec::Hevc, NalCodec::Vvc] {
let _ = caption_cc_data(codec, &[], false);
let _ = caption_cc_data(codec, &[], true);
let _ = caption_cc_data(codec, &[0x06], false);
let _ = caption_cc_data(codec, &[0xFF; 32], false);
let _ = caption_cc_data(codec, &[0x00, 0x00, 0x01, 0x06, 0xFF, 0xFF, 0xFF], false);
for cut in 0..=REAL_A53_SEI_NAL.len() {
let _ = caption_cc_data(codec, &REAL_A53_SEI_NAL[..cut], false);
}
let bad_lp = [0x00, 0x00, 0x00, 0x63, 0x06, 0x04];
let _ = caption_cc_data(codec, &bad_lp, true);
}
}
}