use alloc::collections::btree_map::BTreeMap;
use alloc::format;
use dvb_si::tables::pmt::StreamType;
use transmux::annexb::iter_annexb_nals;
use transmux::sps::decode_avc_sps;
use crate::Diagnostic;
use crate::Report;
use crate::diagnostics::codec_common::{
collect_pmt_streams, for_each_access_unit, pids_with_stream_type,
};
use crate::report::{Finding, Location, Severity};
const H264_NAL_TYPE_MASK: u8 = 0x1F;
const H264_NAL_SPS: u8 = 7;
#[derive(Debug, Clone, Copy)]
pub struct InterlaceCheck;
impl Diagnostic for InterlaceCheck {
fn run(&self, ts: &[u8], report: &mut Report) {
let declared = collect_pmt_streams(ts);
let avc_pids = pids_with_stream_type(&declared, StreamType::H264);
if avc_pids.is_empty() {
return;
}
let mut reported: BTreeMap<u16, bool> = avc_pids.iter().map(|&p| (p, false)).collect();
for_each_access_unit(
ts,
|pid| avc_pids.contains(&pid),
|payload, packet_index, pid| {
let Some(done) = reported.get(&pid).copied() else {
return;
};
if done {
return;
}
for nal in iter_annexb_nals(payload) {
if nal.is_empty() || nal[0] & H264_NAL_TYPE_MASK != H264_NAL_SPS {
continue;
}
let Ok(info) = decode_avc_sps(nal) else {
continue;
};
reported.insert(pid, true);
if !info.frame_mbs_only {
report.push(Finding::new(
Severity::Info,
Location::new(packet_index, pid),
"avc-interlaced-content",
format!(
"AVC SPS on PID 0x{pid:04X} has frame_mbs_only_flag=0 \
(interlaced coding tools enabled) — ITU-T H.264 §7.3.2.1.1"
),
));
}
break;
}
},
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostics::codec_common::tests::{build_pat_pmt_ts, build_pes, make_pes_packet};
const VIDEO_PID: u16 = 0x101;
#[test]
fn progressive_sps_not_flagged() {
let sps: &[u8] = &[
0x67, 0x64, 0x00, 0x0D, 0xAD, 0xC8, 0xBF, 0xFE, 0x03, 0xC1, 0x41, 0xF9,
];
let mut au = alloc::vec::Vec::new();
au.extend_from_slice(&[0x00, 0x00, 0x01]);
au.extend_from_slice(sps);
let mut ts = build_pat_pmt_ts(&[(VIDEO_PID, StreamType::H264)]);
ts.extend_from_slice(&make_pes_packet(VIDEO_PID, 0, &build_pes(0xE0, &au)));
let mut report = Report::new();
InterlaceCheck.run(&ts, &mut report);
assert!(
report
.findings()
.iter()
.all(|f| f.rule_id != "avc-interlaced-content"),
"progressive SPS must not be flagged, got {:?}",
report.findings()
);
}
const INTERLACED_SPS: &[u8] = &[0x67, 0x42, 0x00, 0x0A, 0xDA, 0x64, 0x80];
#[test]
fn baseline_interlaced_sps_decodes_as_expected() {
let info = decode_avc_sps(INTERLACED_SPS).expect("hand-built SPS must decode");
assert_eq!(info.profile_idc, 66);
assert!(!info.frame_mbs_only, "SPS must decode as interlaced");
}
#[test]
fn interlaced_sps_is_flagged() {
let mut au = alloc::vec::Vec::new();
au.extend_from_slice(&[0x00, 0x00, 0x01]);
au.extend_from_slice(INTERLACED_SPS);
let mut ts = build_pat_pmt_ts(&[(VIDEO_PID, StreamType::H264)]);
ts.extend_from_slice(&make_pes_packet(VIDEO_PID, 0, &build_pes(0xE0, &au)));
let mut report = Report::new();
InterlaceCheck.run(&ts, &mut report);
assert!(
report
.findings()
.iter()
.any(|f| f.rule_id == "avc-interlaced-content" && f.location.pid == VIDEO_PID),
"expected avc-interlaced-content, got {:?}",
report.findings()
);
}
}