use alloc::collections::btree_map::BTreeMap;
use alloc::vec::Vec;
use dvb_si::tables::pmt::StreamType;
use transmux::{iter_annexb_nals, parse_adts_header};
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};
#[derive(Debug, Default, Clone, Copy)]
struct Seen {
any_au: bool,
structured: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct CodecSignallingCheck;
impl Diagnostic for CodecSignallingCheck {
fn run(&self, ts: &[u8], report: &mut Report) {
let declared = collect_pmt_streams(ts);
let video_pids: Vec<u16> = pids_with_stream_type(&declared, StreamType::H264)
.into_iter()
.chain(pids_with_stream_type(&declared, StreamType::Hevc))
.collect();
let audio_pids = pids_with_stream_type(&declared, StreamType::AacAdts);
if video_pids.is_empty() && audio_pids.is_empty() {
return;
}
let mut video_seen: BTreeMap<u16, Seen> =
video_pids.iter().map(|&p| (p, Seen::default())).collect();
let mut audio_seen: BTreeMap<u16, Seen> =
audio_pids.iter().map(|&p| (p, Seen::default())).collect();
for_each_access_unit(
ts,
|pid| video_pids.contains(&pid) || audio_pids.contains(&pid),
|payload, _packet_index, pid| {
if let Some(seen) = video_seen.get_mut(&pid) {
seen.any_au = true;
if iter_annexb_nals(payload).next().is_some() {
seen.structured = true;
}
} else if let Some(seen) = audio_seen.get_mut(&pid) {
seen.any_au = true;
if has_adts_sync(payload) {
seen.structured = true;
}
}
},
);
for (&pid, seen) in &video_seen {
if seen.any_au && !seen.structured {
report.push(Finding::new(
Severity::Error,
Location::new(0, pid),
"codec-signalling-mismatch",
alloc::format!(
"PMT declares a NAL video codec on PID 0x{pid:04X} but its elementary \
stream never contains a single Annex B start code — the stream_type \
claim and the bitstream disagree (ISO/IEC 13818-1 Table 2-34)",
),
));
}
}
for (&pid, seen) in &audio_seen {
if seen.any_au && !seen.structured {
report.push(Finding::new(
Severity::Error,
Location::new(0, pid),
"codec-signalling-mismatch",
alloc::format!(
"PMT declares stream_type AAC-ADTS (0x0F) on PID 0x{pid:04X} but its \
elementary stream never contains a valid ADTS sync — the stream_type \
claim and the bitstream disagree (ISO/IEC 13818-1 Table 2-34)",
),
));
}
}
}
}
fn has_adts_sync(payload: &[u8]) -> bool {
const ADTS_MIN: usize = 7;
if payload.len() < ADTS_MIN {
return false;
}
(0..=payload.len() - ADTS_MIN).any(|off| {
payload[off] == 0xFF
&& (payload[off + 1] & 0xF0) == 0xF0
&& parse_adts_header(&payload[off..]).is_ok()
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostics::codec_common::tests::{build_pat_pmt_ts, build_pes, make_pes_packet};
use crate::report::Report;
#[test]
fn declared_pid_with_no_es_at_all_not_flagged() {
let ts = build_pat_pmt_ts(&[(0x101, StreamType::H264)]);
let mut report = Report::new();
CodecSignallingCheck.run(&ts, &mut report);
assert!(
report
.findings()
.iter()
.all(|f| f.rule_id != "codec-signalling-mismatch"),
"a declared PID with zero access units must not be flagged, got {:?}",
report.findings()
);
}
#[test]
fn declared_h264_with_non_nal_payload_is_flagged() {
const VIDEO_PID: u16 = 0x101;
let garbage: &[u8] = &[0xAA; 32]; let mut ts = build_pat_pmt_ts(&[(VIDEO_PID, StreamType::H264)]);
ts.extend_from_slice(&make_pes_packet(VIDEO_PID, 0, &build_pes(0xE0, garbage)));
let mut report = Report::new();
CodecSignallingCheck.run(&ts, &mut report);
assert!(
report
.findings()
.iter()
.any(|f| f.rule_id == "codec-signalling-mismatch" && f.location.pid == VIDEO_PID),
"expected codec-signalling-mismatch for a non-NAL H.264 PID, got {:?}",
report.findings()
);
}
#[test]
fn declared_h264_with_real_nal_payload_not_flagged() {
const VIDEO_PID: u16 = 0x101;
let sps: &[u8] = &[
0x67, 0x64, 0x00, 0x0D, 0xAD, 0xC8, 0xBF, 0xFE, 0x03, 0xC1, 0x41, 0xF9,
];
let pps: &[u8] = &[0x68, 0xCE, 0x38, 0x80];
let slice: &[u8] = &[0x65, 0x88, 0x84, 0x00];
let mut au = Vec::new();
for nal in [sps, pps, slice] {
au.extend_from_slice(&[0x00, 0x00, 0x01]);
au.extend_from_slice(nal);
}
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();
CodecSignallingCheck.run(&ts, &mut report);
assert!(
report
.findings()
.iter()
.all(|f| f.rule_id != "codec-signalling-mismatch"),
"a real NAL-structured AVC access unit must not be flagged, got {:?}",
report.findings()
);
}
#[test]
fn declared_aac_with_non_adts_payload_is_flagged() {
const AUDIO_PID: u16 = 0x102;
let garbage: &[u8] = &[0x00; 32];
let mut ts = build_pat_pmt_ts(&[(AUDIO_PID, StreamType::AacAdts)]);
ts.extend_from_slice(&make_pes_packet(AUDIO_PID, 0, &build_pes(0xC0, garbage)));
let mut report = Report::new();
CodecSignallingCheck.run(&ts, &mut report);
assert!(
report
.findings()
.iter()
.any(|f| f.rule_id == "codec-signalling-mismatch" && f.location.pid == AUDIO_PID),
"expected codec-signalling-mismatch for a non-ADTS AAC PID, got {:?}",
report.findings()
);
}
#[test]
fn declared_aac_with_real_adts_payload_not_flagged() {
const AUDIO_PID: u16 = 0x102;
let header = transmux::build_adts_header(1, 4, 2, 7); let mut ts = build_pat_pmt_ts(&[(AUDIO_PID, StreamType::AacAdts)]);
ts.extend_from_slice(&make_pes_packet(AUDIO_PID, 0, &build_pes(0xC0, &header)));
let mut report = Report::new();
CodecSignallingCheck.run(&ts, &mut report);
assert!(
report
.findings()
.iter()
.all(|f| f.rule_id != "codec-signalling-mismatch"),
"a real ADTS-framed AAC PID must not be flagged, got {:?}",
report.findings()
);
}
#[test]
fn real_captures_not_flagged() {
for rel in [
"ts/h264/baseline.ts",
"ts/hevc/main.ts",
"ts/h264_aac.ts",
"ts/france-pcr-discontinuity.ts",
"ts/m6-single.ts",
] {
let path = alloc::format!("{}/../fixtures/{rel}", env!("CARGO_MANIFEST_DIR"));
let ts = std::fs::read(&path).unwrap_or_else(|e| panic!("read fixture {path}: {e}"));
let mut report = Report::new();
CodecSignallingCheck.run(&ts, &mut report);
assert!(
report
.findings()
.iter()
.all(|f| f.rule_id != "codec-signalling-mismatch"),
"real capture {rel} must not be flagged, got {:?}",
report.findings()
);
}
}
}