use alloc::collections::btree_map::BTreeMap;
use crate::Diagnostic;
use crate::Report;
use crate::report::{Finding, Location, Severity};
use mpeg_ts::ts::{TS_PACKET_SIZE, TsPacket};
const CLOCK_27MHZ: u64 = 27_000_000;
const PCR_MODULUS_27MHZ: u64 = (1u64 << 33) * 300;
const PCR_REPETITION_LIMIT_MS: u64 = 100;
const PCR_REPETITION_LIMIT_TICKS: u64 = CLOCK_27MHZ * PCR_REPETITION_LIMIT_MS / 1000;
const PCR_WARNING_LIMIT_MS: u64 = 40;
const PCR_WARNING_LIMIT_TICKS: u64 = CLOCK_27MHZ * PCR_WARNING_LIMIT_MS / 1000;
const PCR_JUMP_LIMIT_TICKS: u64 = PCR_REPETITION_LIMIT_TICKS;
#[derive(Debug, Clone)]
struct PcrState {
last_pcr: u64,
initialised: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct PcrCheck;
impl Diagnostic for PcrCheck {
fn run(&self, ts: &[u8], report: &mut Report) {
let n_packets = ts.len() / TS_PACKET_SIZE;
let mut pcr_states: BTreeMap<u16, PcrState> = BTreeMap::new();
for i in 0..n_packets {
let offset = i * TS_PACKET_SIZE;
let raw = &ts[offset..offset + TS_PACKET_SIZE];
let Ok(pkt) = TsPacket::parse(raw) else {
continue;
};
let pid = pkt.header.pid;
if !pkt.header.has_adaptation {
continue;
}
let af = match pkt.adaptation_field() {
Some(Ok(a)) => a,
_ => continue,
};
let pcr = match af.pcr {
Some(p) => p.as_27mhz(),
None => continue,
};
let discontinuity = af.discontinuity_indicator;
let state = pcr_states.entry(pid).or_insert(PcrState {
last_pcr: 0,
initialised: false,
});
if !state.initialised {
state.last_pcr = pcr;
state.initialised = true;
continue;
}
let last_pcr = state.last_pcr;
if discontinuity {
state.last_pcr = pcr;
continue;
}
let delta = (pcr.wrapping_add(PCR_MODULUS_27MHZ) - last_pcr) % PCR_MODULUS_27MHZ;
let delta_ms = delta * 1000 / CLOCK_27MHZ;
if delta > PCR_REPETITION_LIMIT_TICKS {
report.push(Finding::new(
Severity::Error,
Location::new(i, pid),
"pcr-repetition",
alloc::format!(
"PCR repetition interval {delta_ms} ms exceeds limit {PCR_REPETITION_LIMIT_MS} ms on PID 0x{pid:04X}",
),
));
} else if delta > PCR_WARNING_LIMIT_TICKS {
report.push(Finding::new(
Severity::Warning,
Location::new(i, pid),
"pcr-repetition",
alloc::format!(
"PCR repetition interval {delta_ms} ms exceeds recommended {PCR_WARNING_LIMIT_MS} ms on PID 0x{pid:04X}",
),
));
}
if delta > PCR_JUMP_LIMIT_TICKS {
report.push(Finding::new(
Severity::Error,
Location::new(i, pid),
"pcr-discontinuity",
alloc::format!(
"PCR delta {delta_ms} ms exceeds limit {PCR_REPETITION_LIMIT_MS} ms on PID 0x{pid:04X} \
without discontinuity_indicator",
),
));
}
state.last_pcr = pcr;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::report::{Report, Severity};
use mpeg_ts::Pcr;
fn encode_pcr_27mhz(ticks: u64) -> [u8; 6] {
Pcr::from_27mhz(ticks).to_field_bytes()
}
fn make_pcr_packet(pid: u16, pcr_27mhz: u64, cc: u8, discontinuity: bool) -> Vec<u8> {
let mut pkt = vec![0x47u8; 188];
pkt[1] = ((pid >> 8) as u8) & 0x1F;
pkt[2] = (pid & 0xFF) as u8;
pkt[3] = 0x30 | (cc & 0x0F);
let pcr_bytes = encode_pcr_27mhz(pcr_27mhz);
let af_len = 1 + 6; pkt[4] = af_len as u8;
let flags = if discontinuity {
0x80 | 0x10 } else {
0x10 };
pkt[5] = flags;
pkt[6..6 + 6].copy_from_slice(&pcr_bytes);
pkt
}
#[test]
fn single_pcr_no_findings() {
let pid = 0x0100u16;
let ts = make_pcr_packet(pid, 0, 0, false);
let mut report = Report::new();
PcrCheck.run(&ts, &mut report);
assert!(report.is_empty(), "unexpected findings: {report:?}");
}
#[test]
fn two_pcrs_within_limit_no_findings() {
let pid = 0x0100u16;
let mut ts = Vec::new();
ts.extend_from_slice(&make_pcr_packet(pid, 0, 0, false));
ts.extend_from_slice(&make_pcr_packet(pid, CLOCK_27MHZ * 30 / 1000, 1, false));
let mut report = Report::new();
PcrCheck.run(&ts, &mut report);
assert!(
report.is_empty(),
"expected no findings, got {:?}",
report.findings()
);
}
#[test]
fn pcr_repetition_over_100ms_flags_error() {
let pid = 0x0100u16;
let mut ts = Vec::new();
ts.extend_from_slice(&make_pcr_packet(pid, 0, 0, false));
ts.extend_from_slice(&make_pcr_packet(pid, CLOCK_27MHZ * 150 / 1000, 1, false));
let mut report = Report::new();
PcrCheck.run(&ts, &mut report);
let rep: Vec<_> = report
.findings()
.iter()
.filter(|f| f.rule_id == "pcr-repetition")
.collect();
assert_eq!(rep.len(), 1);
assert_eq!(rep[0].severity, Severity::Error);
}
#[test]
fn pcr_repetition_over_40ms_flags_warning() {
let pid = 0x0100u16;
let mut ts = Vec::new();
ts.extend_from_slice(&make_pcr_packet(pid, 0, 0, false));
ts.extend_from_slice(&make_pcr_packet(pid, CLOCK_27MHZ * 60 / 1000, 1, false));
let mut report = Report::new();
PcrCheck.run(&ts, &mut report);
let rep: Vec<_> = report
.findings()
.iter()
.filter(|f| f.rule_id == "pcr-repetition")
.collect();
assert_eq!(rep.len(), 1);
assert_eq!(rep[0].severity, Severity::Warning);
}
#[test]
fn pcr_jump_without_discontinuity_flags_error() {
let pid = 0x0100u16;
let mut ts = Vec::new();
ts.extend_from_slice(&make_pcr_packet(pid, 0, 0, false));
ts.extend_from_slice(&make_pcr_packet(pid, CLOCK_27MHZ * 10, 1, false));
let mut report = Report::new();
PcrCheck.run(&ts, &mut report);
let disc: Vec<_> = report
.findings()
.iter()
.filter(|f| f.rule_id == "pcr-discontinuity")
.collect();
assert_eq!(disc.len(), 1);
assert_eq!(disc[0].severity, Severity::Error);
assert!(disc[0].message.contains("without discontinuity_indicator"));
}
#[test]
fn pcr_jump_with_discontinuity_not_flagged() {
let pid = 0x0100u16;
let mut ts = Vec::new();
ts.extend_from_slice(&make_pcr_packet(pid, 0, 0, false));
ts.extend_from_slice(&make_pcr_packet(pid, CLOCK_27MHZ * 10, 1, true));
let mut report = Report::new();
PcrCheck.run(&ts, &mut report);
let disc: Vec<_> = report
.findings()
.iter()
.filter(|f| f.rule_id == "pcr-discontinuity")
.collect();
assert!(
disc.is_empty(),
"signalled discontinuity should not be flagged: {disc:?}"
);
}
#[test]
fn pcr_wrap_around_not_flagged() {
let pid = 0x0100u16;
let mut ts = Vec::new();
let start = PCR_MODULUS_27MHZ - CLOCK_27MHZ * 30 / 1000;
ts.extend_from_slice(&make_pcr_packet(pid, start, 0, false));
let after = CLOCK_27MHZ * 5 / 1000;
ts.extend_from_slice(&make_pcr_packet(pid, after, 1, false));
let mut report = Report::new();
PcrCheck.run(&ts, &mut report);
assert!(
report.is_empty(),
"PCR wrap should not be flagged: {:?}",
report.findings()
);
}
#[test]
fn discontinuity_resets_baseline() {
let pid = 0x0100u16;
let mut ts = Vec::new();
ts.extend_from_slice(&make_pcr_packet(pid, 0, 0, false));
ts.extend_from_slice(&make_pcr_packet(pid, CLOCK_27MHZ * 10, 1, true));
ts.extend_from_slice(&make_pcr_packet(
pid,
CLOCK_27MHZ * 10 + CLOCK_27MHZ * 30 / 1000,
2,
false,
));
let mut report = Report::new();
PcrCheck.run(&ts, &mut report);
let disc: Vec<_> = report
.findings()
.iter()
.filter(|f| f.rule_id == "pcr-discontinuity")
.collect();
assert!(
disc.is_empty(),
"post-discontinuity PCRs should be clean: {disc:?}"
);
let rep: Vec<_> = report
.findings()
.iter()
.filter(|f| f.rule_id == "pcr-repetition")
.collect();
assert!(
rep.is_empty(),
"post-discontinuity PCR repetition should be clean: {rep:?}"
);
}
}