dvb_conformance/lib.rs
1//! ETSI TR 101 290 v1.4.1 transport-stream conformance monitor.
2//!
3//! Implements the **first-priority** (Table 5.0a, indicators 1.1–1.6),
4//! **second-priority** (Table 5.0b, indicators 2.1–2.3b, 2.5–2.6), and
5//! **third-priority** (Table 5.0c, indicators 3.1–3.10) indicator sets —
6//! see `docs/tr_101_290.md` for the full spec transcription and the
7//! crate-coverage mapping.
8//!
9//! ## T-STD buffer model (indicators 3.3, 3.9, 3.10)
10//!
11//! A partial ISO/IEC 13818-1 T-STD buffer model (see `src/tstd.rs`) drives
12//! the buffer-model indicators:
13//!
14//! - **3.3 `BufferError`**: TBsys overflow detection (512-byte buffer at
15//! 1 Mbit/s drain, fed at PSI section completion). TBn overflow is deferred
16//! — it requires the coded bitrate `Rxn` from descriptors.
17//! - **3.9 `EmptyBufferError`**: TBn (per-PID) and TBsys (global) empty at
18//! least once per second. MBn empty check is deferred.
19//! - **3.10 `Data_delay_error`**: Data delay > 1 s through TBn and TBsys.
20//! Still-picture 60 s threshold is tracked but not yet differentiated.
21//! - **2.4 `PcrAccuracyError`**: not implemented — requires hardware arrival
22//! timestamps with ±500 ns resolution. The variant exists for documentation
23//! completeness only.
24//!
25//! MBn/EBn/Bn/Bsys buffer modelling is deferred — it requires codec-level
26//! buffer sizes from descriptors (multiplex_buffer_descriptor,
27//! smoothing_buffer_descriptor) not yet parsed by the monitor.
28//!
29//! # Caller-supplied time
30//!
31//! [`ConformanceMonitor::feed`] takes a [`core::time::Duration`] timestamp
32//! alongside each TS packet. All presence/absence timeout checks (1.3.a, 1.5.a,
33//! 1.6, 2.3a, 2.3b, 2.5, 3.2) are evaluated against this clock. The caller
34//! must ensure that timestamps are **monotonic non-decreasing** across calls;
35//! the monitor does not enforce this but non-monotonic timestamps will produce
36//! spurious events.
37//!
38//! # References
39//!
40//! - ETSI TR 101 290 v1.4.1 (2020-06), §5.2.1, Table 5.0a
41//! - ETSI TR 101 290 v1.4.1 (2020-06), §5.2.2, Table 5.0b
42//! - ETSI TR 101 290 v1.4.1 (2020-06), §5.2.3, Table 5.0c
43//! - ISO/IEC 13818-1 (MPEG-2 Systems)
44
45#![cfg_attr(not(feature = "std"), no_std)]
46#![cfg_attr(docsrs, feature(doc_cfg))]
47// Runnable examples, embedded so they render on docs.rs and stay in sync with
48// the actual `examples/*.rs` files (shown, not compiled).
49#![doc = "\n# Examples\n"]
50#![doc = "Two runnable examples ship with this crate (`cargo run -p dvb-conformance --example <name>`).\n"]
51#![doc = "\n## `monitor_stream`\n\n```rust,ignore"]
52#![doc = include_str!("../examples/monitor_stream.rs")]
53#![doc = "```\n\n## `priority_breakdown`\n\n```rust,ignore"]
54#![doc = include_str!("../examples/priority_breakdown.rs")]
55#![doc = "```"]
56extern crate alloc;
57
58mod tstd;
59
60use alloc::collections::BTreeMap;
61use alloc::format;
62use alloc::string::String;
63use alloc::vec::Vec;
64use core::time::Duration;
65
66use tstd::TstdModel;
67
68use broadcast_common::Parse;
69use dvb_si::tables::eit::EitSection;
70use dvb_si::tables::pat::{PatSection, TABLE_ID as PAT_TABLE_ID};
71use dvb_si::tables::pmt::PmtSection;
72use mpeg_ts::section::Section;
73use mpeg_ts::ts::{SectionReassembler, TsPacket};
74
75// ── Named PID constants ─────────────────────────────────────────────────────
76
77/// PID 0x0000 — Program Association Table (ISO/IEC 13818-1 §2.4.4.3).
78const PID_PAT: u16 = 0x0000;
79/// PID 0x0001 — Conditional Access Table (ISO/IEC 13818-1 §2.4.4.5).
80const PID_CAT: u16 = 0x0001;
81/// PID 0x0010 — Network Information Table (EN 300 468 §5.2.1).
82const PID_NIT: u16 = 0x0010;
83/// PID 0x0011 — SDT/BAT (EN 300 468 §5.2.2 / §5.2.3).
84const PID_SDT_BAT: u16 = 0x0011;
85/// PID 0x0012 — Event Information Table (EN 300 468 §5.2.4).
86const PID_EIT: u16 = 0x0012;
87/// PID 0x0013 — Running Status Table (EN 300 468 §5.2.8).
88const PID_RST: u16 = 0x0013;
89/// PID 0x0014 — TDT/TOT (EN 300 468 §5.2.5 / §5.2.6).
90const PID_TDT_TOT: u16 = 0x0014;
91/// PID 0x1FFF — Null/padding packets (ISO/IEC 13818-1 §2.4.3.3).
92const PID_NULL: u16 = 0x1FFF;
93
94/// Sync byte value (ISO/IEC 13818-1 §2.4.3.3).
95const SYNC_BYTE: u8 = 0x47;
96
97/// Well-known SI/PSI PIDs on which CRC-checked long-form sections appear.
98const SI_PIDS: [u16; 7] = [
99 PID_PAT,
100 PID_CAT,
101 PID_NIT,
102 PID_SDT_BAT,
103 PID_EIT,
104 PID_RST,
105 PID_TDT_TOT,
106];
107
108/// Reserved-for-future-use PID range (ISO/IEC 13818-1 Table 2-3) — exempt
109/// from Unreferenced_PID (TR 101 290 v1.4.1 Table 5.0c indicator 3.4).
110const RESERVED_PID_MIN: u16 = 0x0002;
111/// Upper (inclusive) bound of the reserved PID range.
112const RESERVED_PID_MAX: u16 = 0x000F;
113
114// ── Default timing constants ────────────────────────────────────────────────
115
116/// TR 101 290 v1.4.1 Table 5.0a note 3 / TS 101 154 §4.1.7 — PAT maximum
117/// interval (0.5 s per Table 5.0a row 1.3.a; TS 101 154 recommends ≤ 100 ms).
118const DEFAULT_PAT_MAX_INTERVAL_MS: u64 = 500;
119
120/// TR 101 290 v1.4.1 Table 5.0a row 1.5.a / note 3 — PMT maximum interval.
121const DEFAULT_PMT_MAX_INTERVAL_MS: u64 = 500;
122
123/// TR 101 290 v1.4.1 §5.2.1 accompanying text (1.6) — PID_error period.
124const DEFAULT_PID_ERROR_PERIOD_SECS: u64 = 5;
125
126/// TR 101 290 v1.4.1 §5.2.1 accompanying text (1.1) — sync acquisition
127/// threshold: five consecutive correct sync bytes.
128const DEFAULT_SYNC_ACQUIRE_PACKETS: u8 = 5;
129
130/// TR 101 290 v1.4.1 §5.2.1 accompanying text (1.1) — sync loss threshold:
131/// two or more consecutive corrupted sync bytes.
132const DEFAULT_SYNC_LOSS_PACKETS: u8 = 2;
133
134/// TR 101 290 v1.4.1 Table 5.0b indicator 2.3a / note 2 — PCR maximum
135/// repetition interval (100 ms; note 2 removed the 40 ms limit).
136const DEFAULT_PCR_REPETITION_LIMIT_MS: u64 = 100;
137
138/// TR 101 290 v1.4.1 Table 5.0b indicator 2.3b — PCR discontinuity indicator
139/// maximum interval (100 ms).
140const DEFAULT_PCR_DISCONTINUITY_LIMIT_MS: u64 = 100;
141
142/// TR 101 290 v1.4.1 Table 5.0b indicator 2.5 / note 3 — PTS maximum
143/// repetition interval (700 ms; not applied to still pictures).
144const DEFAULT_PTS_REPETITION_LIMIT_MS: u64 = 700;
145
146/// TR 101 290 v1.4.1 Table 5.0c indicator 3.2 — NIT_actual maximum repetition
147/// interval (10 s; EN 300 468 §5.2.1).
148const DEFAULT_SI_NIT_INTERVAL_SECS: u64 = 10;
149
150/// TR 101 290 v1.4.1 Table 5.0c indicator 3.2 — SDT_actual maximum repetition
151/// interval (2 s; EN 300 468 §5.2.2).
152const DEFAULT_SI_SDT_INTERVAL_SECS: u64 = 2;
153
154/// TR 101 290 v1.4.1 Table 5.0c indicator 3.2 — EIT P/F actual maximum
155/// repetition interval (2 s; EN 300 468 §5.2.4).
156const DEFAULT_SI_EIT_PF_INTERVAL_SECS: u64 = 2;
157
158/// TR 101 290 v1.4.1 Table 5.0c indicator 3.2 — TDT maximum repetition
159/// interval (30 s; EN 300 468 §5.2.5).
160const DEFAULT_SI_TDT_INTERVAL_SECS: u64 = 30;
161
162/// TR 101 290 v1.4.1 Table 5.0c indicator 3.4 / note 1 — Unreferenced_PID
163/// persistence threshold (0,5 s). Transitions shorter than this (e.g. a PID
164/// seen briefly before its PMT arrives) are not errors.
165const DEFAULT_UNREFERENCED_PID_PERIOD_MS: u64 = 500;
166
167// ── 25 ms minimum-gap constants (Table 5.0c rows 3.1.a / 3.2 / 3.5.a / 3.6.a / 3.7 / 3.8)
168
169/// TR 101 290 v1.4.1 Table 5.0c — minimum interval between sections of the
170/// same `(table_id, section_number)` on the same PID (25 ms).
171const DEFAULT_SI_MIN_GAP_MS: u64 = 25;
172
173/// TR 101 290 v1.4.1 Table 5.0c indicator 3.1.b / 3.5.b / 3.6.b — maximum
174/// interval between `_other` sections with the same `section_number` on the
175/// same PID (10 s per ETSI TR 101 211 §4.4).
176const DEFAULT_SI_OTHER_INTERVAL_SECS: u64 = 10;
177
178// ── PCR / PES constants ─────────────────────────────────────────────────────
179
180/// PCR modulus on the 27 MHz clock: `2^33 × 300` (33-bit base × 300 ticks).
181/// ISO/IEC 13818-1 §2.4.3.5 — PCR wraps modulo this value.
182const PCR_MODULUS_27MHZ: u64 = (1u64 << 33) * 300;
183
184/// 27 MHz clock rate (ticks per second).
185const CLOCK_27MHZ: u64 = 27_000_000;
186
187/// PES start-code prefix byte 0 (ISO/IEC 13818-1 §2.4.3.6, Table 2-21).
188const PES_PREFIX_0: u8 = 0x00;
189/// PES start-code prefix byte 1.
190const PES_PREFIX_1: u8 = 0x00;
191/// PES start-code prefix byte 2.
192const PES_PREFIX_2: u8 = 0x01;
193
194/// Offset of the PES header `marker_bits + flags` byte relative to the PES
195/// packet start (byte 6: `'10' + PES_scrambling_control + …`).
196const PES_FLAGS_OFFSET: usize = 6;
197
198/// Mask for the `PTS_DTS_flags` field within the PES header byte at offset 7
199/// (bits `[7:6]` — `0b10` means PTS present, `0b11` means PTS+DTS).
200const PES_PTS_DTS_FLAGS_MASK: u8 = 0b1100_0000;
201
202/// Value indicating PTS is present in `PTS_DTS_flags` (bit 7 set).
203const PES_PTS_PRESENT: u8 = 0b1000_0000;
204
205/// CAT `table_id` value (ISO/IEC 13818-1 §2.4.4.5).
206const CAT_TABLE_ID: u8 = dvb_si::table_id::TableId::Cat as u8;
207
208/// NIT_actual `table_id` (EN 300 468 §5.2.1, table_id 0x40).
209const NIT_ACTUAL_TABLE_ID: u8 = dvb_si::table_id::TableId::NetworkInformationActual as u8;
210
211/// SDT_actual `table_id` (EN 300 468 §5.2.2, table_id 0x42).
212const SDT_ACTUAL_TABLE_ID: u8 = dvb_si::table_id::TableId::ServiceDescriptionActual as u8;
213
214/// EIT P/F actual `table_id` (EN 300 468 §5.2.4, table_id 0x4E).
215const EIT_PF_ACTUAL_TABLE_ID: u8 = dvb_si::table_id::TableId::EventInformationPfActual as u8;
216
217/// TDT `table_id` (EN 300 468 §5.2.5, table_id 0x70).
218const TDT_TABLE_ID: u8 = dvb_si::table_id::TableId::TimeAndDate as u8;
219
220/// NIT_other `table_id` (EN 300 468 §5.2.1, table_id 0x41).
221const NIT_OTHER_TABLE_ID: u8 = dvb_si::table_id::TableId::NetworkInformationOther as u8;
222
223/// SDT_other `table_id` (EN 300 468 §5.2.3, table_id 0x46).
224const SDT_OTHER_TABLE_ID: u8 = dvb_si::table_id::TableId::ServiceDescriptionOther as u8;
225
226/// BAT `table_id` (EN 300 468 §5.2.2, table_id 0x4A — allowed alongside SDT
227/// on PID 0x0011).
228const BAT_TABLE_ID: u8 = dvb_si::table_id::TableId::BouquetAssociation as u8;
229
230/// EIT P/F other `table_id` (EN 300 468 §5.2.4, table_id 0x4F).
231const EIT_PF_OTHER_TABLE_ID: u8 = dvb_si::table_id::TableId::EventInformationPfOther as u8;
232
233/// RST `table_id` (EN 300 468 §5.2.8, table_id 0x71).
234const RST_TABLE_ID: u8 = dvb_si::table_id::TableId::RunningStatus as u8;
235
236/// Stuffing table (`ST`) `table_id` — allowed filler on every SI PID covered
237/// by Table 5.0c (EN 300 468 §5.2.9, table_id 0x72).
238const STUFFING_TABLE_ID: u8 = dvb_si::table_id::TableId::Stuffing as u8;
239
240/// TOT `table_id` (EN 300 468 §5.2.6, table_id 0x73 — allowed alongside TDT
241/// on PID 0x0014).
242const TOT_TABLE_ID: u8 = dvb_si::table_id::TableId::TimeOffset as u8;
243
244// ── Public types ─────────────────────────────────────────────────────────────
245
246/// Severity tier per TR 101 290 §5.2 (Tables 5.0a/5.0b/5.0c).
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248#[cfg_attr(feature = "serde", derive(serde::Serialize))]
249#[non_exhaustive]
250pub enum Priority {
251 /// Table 5.0a — necessary for de-codability.
252 First,
253 /// Table 5.0b — recommended for continuous or periodic monitoring.
254 Second,
255 /// Table 5.0c — application-dependant monitoring.
256 Third,
257}
258
259impl Priority {
260 /// Human-readable spec label (TR 101 290 §5.2, Tables 5.0a/5.0b/5.0c).
261 #[must_use]
262 pub fn name(&self) -> &'static str {
263 match self {
264 Self::First => "first priority",
265 Self::Second => "second priority",
266 Self::Third => "third priority",
267 }
268 }
269}
270broadcast_common::impl_spec_display!(Priority);
271
272/// A TR 101 290 measurement indicator.
273///
274/// `#[non_exhaustive]` — additional Priority-3 variants may be added later.
275#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
276#[cfg_attr(feature = "serde", derive(serde::Serialize))]
277#[non_exhaustive]
278pub enum Indicator {
279 // ── Priority 1 (Table 5.0a) ──────────────────────────────────────────
280 /// TR 101 290 v1.4.1 Table 5.0a indicator 1.1 — loss of synchronisation
281 /// with hysteresis.
282 TsSyncLoss,
283 /// TR 101 290 v1.4.1 Table 5.0a indicator 1.2 — sync_byte not equal 0x47.
284 SyncByteError,
285 /// TR 101 290 v1.4.1 Table 5.0a indicator 1.3.a — PAT_error_2.
286 PatError2,
287 /// TR 101 290 v1.4.1 Table 5.0a indicator 1.4 — Continuity_count_error.
288 ContinuityCountError,
289 /// TR 101 290 v1.4.1 Table 5.0a indicator 1.5.a — PMT_error_2.
290 PmtError2,
291 /// TR 101 290 v1.4.1 Table 5.0a indicator 1.6 — PID_error.
292 PidError,
293
294 // ── Priority 2 (Table 5.0b) ──────────────────────────────────────────
295 /// TR 101 290 v1.4.1 Table 5.0b indicator 2.1 — Transport_error.
296 TransportError,
297 /// TR 101 290 v1.4.1 Table 5.0b indicator 2.2 — CRC_error.
298 CrcError,
299 /// TR 101 290 v1.4.1 Table 5.0b indicator 2.3a — PCR_repetition_error.
300 PcrRepetitionError,
301 /// TR 101 290 v1.4.1 Table 5.0b indicator 2.3b —
302 /// PCR_discontinuity_indicator_error.
303 PcrDiscontinuityError,
304 /// TR 101 290 v1.4.1 Table 5.0b indicator 2.5 — PTS_error.
305 PtsError,
306 /// TR 101 290 v1.4.1 Table 5.0b indicator 2.6 — CAT_error.
307 CatError,
308
309 // ── Priority 3 (Table 5.0c) ──────────────────────────────────────────
310 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.1 — NIT_error (bad table_id
311 /// on PID 0x0010 + NIT_actual absence).
312 NitError,
313 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.2 — SI_repetition_error
314 /// (maximum interval dimension).
315 SiRepetitionError,
316 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.4 — Unreferenced_PID: a PID
317 /// persists longer than the presence threshold without being referenced
318 /// by the PAT/CAT/a PMT or one of the well-known SI PIDs.
319 UnreferencedPid,
320 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.5 — SDT_error (bad table_id
321 /// on PID 0x0011 + SDT_actual absence).
322 SdtError,
323 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.6 — EIT_error (bad table_id
324 /// on PID 0x0012 + EIT P/F actual absence).
325 EitError,
326 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.7 — RST_error (bad table_id
327 /// on PID 0x0013).
328 RstError,
329 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.8 — TDT_error (bad table_id
330 /// on PID 0x0014 + TDT absence).
331 TdtError,
332
333 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.6.c — EIT_PF_error: if either
334 /// section ('0' P or '1' F) of an EIT P/F sub-table is present, both
335 /// should exist.
336 EitPfError,
337
338 /// TR 101 290 v1.4.1 Table 5.0c — NIT_other_error (3.1.b): interval between
339 /// sections with the same `section_number` and `table_id = 0x41` on
340 /// PID 0x0010 longer than 10 s.
341 NitOtherError,
342 /// TR 101 290 v1.4.1 Table 5.0c — SDT_other_error (3.5.b): interval between
343 /// sections with the same `section_number` and `table_id = 0x46` on
344 /// PID 0x0011 longer than 10 s.
345 SdtOtherError,
346 /// TR 101 290 v1.4.1 Table 5.0c — EIT_other_error (3.6.b): interval between
347 /// EIT P/F other sections with the same `section_number` and
348 /// `table_id = 0x4F` on PID 0x0012 longer than 10 s.
349 EitOtherError,
350
351 /// TR 101 290 v1.4.1 Table 5.0c — min-gap violation: two sections of the
352 /// same `(table_id, section_number)` occurred closer together than the
353 /// specified minimum interval (25 ms per Table 5.0c rows 3.1.a, 3.2,
354 /// 3.5.a, 3.6.a, 3.7, 3.8).
355 SiMinGapError,
356
357 // ── Priority 2 (Table 5.0b) — T-STD ──────────────────────────────────
358 /// TR 101 290 v1.4.1 Table 5.0b indicator 2.4 — PCR_accuracy_error.
359 ///
360 /// **Not implemented**: requires hardware arrival timing with ±500 ns
361 /// resolution (ISO/IEC 13818-1 §2.4.2.2). A packet-index-derived arrival
362 /// estimate cannot honestly resolve 500 ns, and a false positive is worse
363 /// than a gap (c.f. the withdrawn `PtsCheck`). This variant exists for
364 /// documentation completeness only and is never emitted by the monitor.
365 PcrAccuracyError,
366
367 // ── Priority 3 (Table 5.0c) — T-STD ──────────────────────────────────
368 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.3 — Buffer_error.
369 ///
370 /// Currently checks `TB_buffering_error` (overflow of transport buffer
371 /// TBn, 512 bytes per ISO/IEC 13818-1 §2.4.2.3) and
372 /// `TBsys_buffering_error` (overflow of TBsys, 512 bytes). The remaining
373 /// sub-checks — `MB_buffering_error`, `EB_buffering_error`,
374 /// `B_buffering_error`, `Bsys_buffering_error` — require codec-dependent
375 /// buffer sizes from descriptors (multiplex_buffer_descriptor,
376 /// smoothing_buffer_descriptor) and are deferred until the monitor
377 /// parses those descriptors.
378 BufferError,
379
380 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.9 — Empty_buffer_error.
381 ///
382 /// Checks that TBn (transport buffer for each elementary stream) and
383 /// TBsys (system-information transport buffer) are empty at least once
384 /// per second. The MBn check (multiplexing buffer, leak method) is
385 /// deferred — it requires the full MBn buffer model with leak-rate
386 /// parameters from descriptors.
387 EmptyBufferError,
388
389 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.10 — Data_delay_error.
390 ///
391 /// Checks that data delay through the T-STD transport buffers (TBn,
392 /// TBsys) does not exceed 1 second (60 s for still-picture video).
393 /// The full end-to-end delay through MBn/EBn/Bn is not modelled
394 /// (deferred — see `Buffer_error` doc).
395 DataDelayError,
396}
397
398impl Indicator {
399 /// The priority tier this indicator belongs to.
400 #[must_use]
401 pub fn priority(self) -> Priority {
402 match self {
403 Self::TsSyncLoss
404 | Self::SyncByteError
405 | Self::PatError2
406 | Self::ContinuityCountError
407 | Self::PmtError2
408 | Self::PidError => Priority::First,
409 Self::TransportError
410 | Self::CrcError
411 | Self::PcrRepetitionError
412 | Self::PcrDiscontinuityError
413 | Self::PtsError
414 | Self::CatError
415 | Self::PcrAccuracyError => Priority::Second,
416 Self::NitError
417 | Self::SiRepetitionError
418 | Self::UnreferencedPid
419 | Self::SdtError
420 | Self::EitError
421 | Self::RstError
422 | Self::TdtError
423 | Self::BufferError
424 | Self::EmptyBufferError
425 | Self::DataDelayError
426 | Self::EitPfError
427 | Self::NitOtherError
428 | Self::SdtOtherError
429 | Self::EitOtherError
430 | Self::SiMinGapError => Priority::Third,
431 }
432 }
433
434 /// Verbatim indicator name from the TR 101 290 tables.
435 #[must_use]
436 pub fn name(self) -> &'static str {
437 match self {
438 Self::TsSyncLoss => "TS_sync_loss",
439 Self::SyncByteError => "Sync_byte_error",
440 Self::PatError2 => "PAT_error_2",
441 Self::ContinuityCountError => "Continuity_count_error",
442 Self::PmtError2 => "PMT_error_2",
443 Self::PidError => "PID_error",
444 Self::TransportError => "Transport_error",
445 Self::CrcError => "CRC_error",
446 Self::PcrRepetitionError => "PCR_repetition_error",
447 Self::PcrDiscontinuityError => "PCR_discontinuity_indicator_error",
448 Self::PtsError => "PTS_error",
449 Self::CatError => "CAT_error",
450 Self::NitError => "NIT_error",
451 Self::SiRepetitionError => "SI_repetition_error",
452 Self::UnreferencedPid => "Unreferenced_PID",
453 Self::SdtError => "SDT_error",
454 Self::EitError => "EIT_error",
455 Self::RstError => "RST_error",
456 Self::TdtError => "TDT_error",
457 Self::PcrAccuracyError => "PCR_accuracy_error",
458 Self::BufferError => "Buffer_error",
459 Self::EmptyBufferError => "Empty_buffer_error",
460 Self::DataDelayError => "Data_delay_error",
461 Self::EitPfError => "EIT_PF_error",
462 Self::NitOtherError => "NIT_other_error",
463 Self::SdtOtherError => "SDT_other_error",
464 Self::EitOtherError => "EIT_other_error",
465 Self::SiMinGapError => "SI_min_gap_error",
466 }
467 }
468
469 /// Clause citation from the spec.
470 #[must_use]
471 pub fn clause(self) -> &'static str {
472 match self {
473 Self::TsSyncLoss => "TR 101 290 v1.4.1 Table 5.0a indicator 1.1",
474 Self::SyncByteError => "TR 101 290 v1.4.1 Table 5.0a indicator 1.2",
475 Self::PatError2 => "TR 101 290 v1.4.1 Table 5.0a indicator 1.3.a",
476 Self::ContinuityCountError => "TR 101 290 v1.4.1 Table 5.0a indicator 1.4",
477 Self::PmtError2 => "TR 101 290 v1.4.1 Table 5.0a indicator 1.5.a",
478 Self::PidError => "TR 101 290 v1.4.1 Table 5.0a indicator 1.6",
479 Self::TransportError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.1",
480 Self::CrcError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.2",
481 Self::PcrRepetitionError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.3a",
482 Self::PcrDiscontinuityError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.3b",
483 Self::PtsError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.5",
484 Self::CatError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.6",
485 Self::NitError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.1",
486 Self::SiRepetitionError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.2",
487 Self::UnreferencedPid => "TR 101 290 v1.4.1 Table 5.0c indicator 3.4",
488 Self::SdtError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.5",
489 Self::EitError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.6",
490 Self::RstError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.7",
491 Self::TdtError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.8",
492 Self::PcrAccuracyError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.4",
493 Self::BufferError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.3",
494 Self::EmptyBufferError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.9",
495 Self::DataDelayError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.10",
496 Self::EitPfError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.6.c",
497 Self::NitOtherError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.1.b",
498 Self::SdtOtherError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.5.b",
499 Self::EitOtherError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.6.b",
500 Self::SiMinGapError => {
501 "TR 101 290 v1.4.1 Table 5.0c (25 ms minimum-gap dimension of 3.1.a / 3.2 / 3.5.a / 3.6.a / 3.7 / 3.8)"
502 }
503 }
504 }
505}
506broadcast_common::impl_spec_display!(Indicator);
507
508/// One raised conformance error.
509#[derive(Debug, Clone, PartialEq, Eq)]
510#[cfg_attr(feature = "serde", derive(serde::Serialize))]
511#[non_exhaustive]
512pub struct ConformanceEvent {
513 /// The indicator that was raised.
514 pub indicator: Indicator,
515 /// Priority tier of the indicator.
516 pub priority: Priority,
517 /// PID the error concerns, when applicable.
518 pub pid: Option<u16>,
519 /// Caller timestamp of the packet that raised it.
520 pub at: Duration,
521 /// Human-readable specifics (e.g. "expected cc=5, got 7").
522 pub detail: String,
523}
524
525/// Diagnostic counters.
526#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
527#[cfg_attr(feature = "serde", derive(serde::Serialize))]
528#[non_exhaustive]
529pub struct Stats {
530 /// Total TS packets fed.
531 pub packets: u64,
532 /// Total conformance events raised.
533 pub events: u64,
534 /// Whether the monitor is currently in sync.
535 pub in_sync: bool,
536}
537
538/// Configurable hysteresis and timeout parameters.
539#[derive(Debug, Clone)]
540#[non_exhaustive]
541pub struct Config {
542 /// Maximum interval between PAT sections (Table 5.0a 1.3.a / note 3).
543 /// Default: 500 ms.
544 pub pat_max_interval: Duration,
545 /// Maximum interval between PMT sections per program_map_PID (1.5.a).
546 /// Default: 500 ms.
547 pub pmt_max_interval: Duration,
548 /// Period after which a referenced PID is considered absent (1.6).
549 /// Default: 5 s.
550 pub pid_error_period: Duration,
551 /// Consecutive good sync bytes to acquire sync (1.1).
552 /// Default: 5.
553 pub sync_acquire_packets: u8,
554 /// Consecutive bad sync bytes to declare sync loss (1.1).
555 /// Default: 2.
556 pub sync_loss_packets: u8,
557 /// Maximum interval between consecutive PCR values on a single PID
558 /// (Table 5.0b 2.3a / note 2). Default: 100 ms.
559 pub pcr_repetition_limit: Duration,
560 /// Maximum legal PCR delta (in time) without a signalled discontinuity
561 /// (Table 5.0b 2.3b). Default: 100 ms.
562 pub pcr_discontinuity_limit: Duration,
563 /// Maximum interval between consecutive PTS values on an elementary-stream
564 /// PID (Table 5.0b 2.5 / note 3). Default: 700 ms.
565 pub pts_repetition_limit: Duration,
566 /// Maximum repetition interval for NIT_actual sections (Table 5.0c 3.2 /
567 /// EN 300 468 §5.2.1). Default: 10 s.
568 pub si_nit_interval: Duration,
569 /// Maximum repetition interval for SDT_actual sections (Table 5.0c 3.2 /
570 /// EN 300 468 §5.2.2). Default: 2 s.
571 pub si_sdt_interval: Duration,
572 /// Maximum repetition interval for EIT P/F actual sections (Table 5.0c
573 /// 3.2 / EN 300 468 §5.2.4). Default: 2 s.
574 pub si_eit_pf_interval: Duration,
575 /// Maximum repetition interval for TDT sections (Table 5.0c 3.2 /
576 /// EN 300 468 §5.2.5). Default: 30 s.
577 pub si_tdt_interval: Duration,
578 /// Persistence threshold before an unreferenced PID is flagged
579 /// (Table 5.0c 3.4 / note 1). Default: 500 ms.
580 pub unreferenced_pid_period: Duration,
581 /// Minimum allowed interval between sections of the same
582 /// `(table_id, section_number)` on the same PID
583 /// (Table 5.0c rows 3.1.a / 3.2 / 3.5.a / 3.6.a / 3.7 / 3.8).
584 /// Default: 25 ms.
585 pub si_min_gap: Duration,
586 /// Maximum interval between `_other` sections with the same
587 /// `section_number` (Table 5.0c indicators 3.1.b / 3.5.b / 3.6.b).
588 /// Default: 10 s per ETSI TR 101 211 §4.4.
589 pub si_other_interval: Duration,
590}
591
592impl Default for Config {
593 fn default() -> Self {
594 Self {
595 pat_max_interval: Duration::from_millis(DEFAULT_PAT_MAX_INTERVAL_MS),
596 pmt_max_interval: Duration::from_millis(DEFAULT_PMT_MAX_INTERVAL_MS),
597 pid_error_period: Duration::from_secs(DEFAULT_PID_ERROR_PERIOD_SECS),
598 sync_acquire_packets: DEFAULT_SYNC_ACQUIRE_PACKETS,
599 sync_loss_packets: DEFAULT_SYNC_LOSS_PACKETS,
600 pcr_repetition_limit: Duration::from_millis(DEFAULT_PCR_REPETITION_LIMIT_MS),
601 pcr_discontinuity_limit: Duration::from_millis(DEFAULT_PCR_DISCONTINUITY_LIMIT_MS),
602 pts_repetition_limit: Duration::from_millis(DEFAULT_PTS_REPETITION_LIMIT_MS),
603 si_nit_interval: Duration::from_secs(DEFAULT_SI_NIT_INTERVAL_SECS),
604 si_sdt_interval: Duration::from_secs(DEFAULT_SI_SDT_INTERVAL_SECS),
605 si_eit_pf_interval: Duration::from_secs(DEFAULT_SI_EIT_PF_INTERVAL_SECS),
606 si_tdt_interval: Duration::from_secs(DEFAULT_SI_TDT_INTERVAL_SECS),
607 unreferenced_pid_period: Duration::from_millis(DEFAULT_UNREFERENCED_PID_PERIOD_MS),
608 si_min_gap: Duration::from_millis(DEFAULT_SI_MIN_GAP_MS),
609 si_other_interval: Duration::from_secs(DEFAULT_SI_OTHER_INTERVAL_SECS),
610 }
611 }
612}
613
614// ── Internal per-PID state ───────────────────────────────────────────────────
615
616/// Per-PID continuity-counter tracking state.
617struct CcState {
618 last_cc: u8,
619 had_payload: bool,
620 dup_used: bool,
621 initialised: bool,
622 /// Raw bytes of the last payload-bearing packet seen on this PID. Used
623 /// with [`broadcast_common::ts_dup::check_duplicate`] to test the
624 /// H.222.0 §2.4.3.3 "legal duplicate" byte-identity rule (that function
625 /// owns locating and exempting the PCR field). Empty until the first
626 /// packet is recorded.
627 last_packet: Vec<u8>,
628}
629
630/// Timer state for a presence/absence check (shared by 1.3.a, 1.5.a, 1.6).
631struct PresenceTimer {
632 last_seen: Duration,
633 reported: bool,
634}
635
636/// State tracked for each program_map_PID signalled by the PAT.
637struct PmtTracking {
638 timer: PresenceTimer,
639 reassembler: SectionReassembler,
640}
641
642/// State tracked for each elementary-stream PID referenced by a PMT.
643struct EsTracking {
644 timer: PresenceTimer,
645}
646
647/// Per-PID PCR tracking state (indicators 2.3a, 2.3b).
648struct PcrState {
649 last_pcr_27mhz: u64,
650 last_pcr_time: Duration,
651 initialised: bool,
652}
653
654/// Per-PID PTS tracking state (indicator 2.5).
655struct PtsState {
656 last_pts_time: Duration,
657 armed: bool,
658}
659
660/// Per-PID section reassembly state for the well-known SI/PSI PIDs.
661struct SiReassembly {
662 reassembler: SectionReassembler,
663}
664
665/// Timer state for an SI table repetition-interval check (indicator 3.2).
666/// Lazily armed — only starts checking after the first section of that
667/// table_id is seen.
668struct SiRepetitionTimer {
669 last_seen: Duration,
670 reported: bool,
671 armed: bool,
672}
673
674/// Tracking state for a candidate Unreferenced_PID (indicator 3.4): the time
675/// this PID was first observed while NOT part of the referenced set.
676struct UnreferencedPidTracking {
677 first_seen: Duration,
678 reported: bool,
679}
680
681/// Per-`(table_id, section_number)` last-seen tracking for the 25 ms
682/// minimum-gap dimension (indicators 3.1.a / 3.2 / 3.5.a / 3.6.a / 3.7 / 3.8).
683/// Keyed on the well-known PID + `(table_id, section_number)` so a dense
684/// multi-section table does not produce false positives.
685#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
686struct MinGapKey {
687 pid: u16,
688 table_id: u8,
689 section_number: u8,
690}
691
692/// State for a single `_other` section_number repetition check (indicators
693/// 3.1.b / 3.5.b / 3.6.b). Lazily armed — only checks after the second
694/// occurrence of the section is seen (so a stream that legitimately carries no
695/// `_other` table is never flagged).
696struct OtherRepetitionState {
697 /// How many times this `(table_id, section_number)` has been seen.
698 occurrences: u64,
699 /// Time of the most recent section.
700 last_seen: Duration,
701 /// Whether the absence has already been reported for this key.
702 reported: bool,
703}
704
705/// Per-sub-table EIT P/F pairing state (indicator 3.6.c).
706/// A sub-table is identified by `(service_id, transport_stream_id,
707/// original_network_id)`.
708#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
709struct EitSubTableKey {
710 service_id: u16,
711 transport_stream_id: u16,
712 original_network_id: u16,
713}
714
715/// Tracking for whether section '0' (present) and/or section '1' (following)
716/// have been seen for a given EIT P/F sub-table.
717struct EitPfPairState {
718 has_section_0: bool,
719 has_section_1: bool,
720 /// True once the pairing error has been emitted for this sub-table.
721 reported: bool,
722 /// Timestamp when the first incomplete observation was made (one section
723 /// seen without the other). The error fires only after the EIT P/F
724 /// repetition interval (2 s) has elapsed since this timestamp, giving the
725 /// partner section time to arrive.
726 first_incomplete_at: Option<Duration>,
727}
728
729// ── ConformanceMonitor ───────────────────────────────────────────────────────
730
731/// ETSI TR 101 290 transport-stream conformance monitor.
732///
733/// Feed one TS packet at a time via [`feed`](Self::feed); each call returns
734/// the events raised by that packet. The monitor is synchronous and
735/// single-threaded — no interior mutability, no async.
736pub struct ConformanceMonitor {
737 config: Config,
738 events: Vec<ConformanceEvent>,
739 stats: Stats,
740
741 // Sync hysteresis state machine (1.1)
742 in_sync: bool,
743 good_run: u8,
744 bad_run: u8,
745
746 // Per-PID continuity counter (1.4)
747 cc_states: BTreeMap<u16, CcState>,
748
749 // PAT section reassembly + timing (1.3.a)
750 pat_reassembler: SectionReassembler,
751 pat_timer: PresenceTimer,
752
753 // PMT section reassembly + timing per program_map_PID (1.5.a)
754 pmt_trackings: BTreeMap<u16, PmtTracking>,
755
756 // Referenced ES PID timing (1.6)
757 es_trackings: BTreeMap<u16, EsTracking>,
758
759 // Well-known SI/PSI section reassembly + CRC checking (2.2)
760 si_reassemblies: BTreeMap<u16, SiReassembly>,
761
762 // Per-PID PCR tracking (2.3a, 2.3b)
763 pcr_states: BTreeMap<u16, PcrState>,
764
765 // Per-PID PTS tracking (2.5)
766 pts_states: BTreeMap<u16, PtsState>,
767
768 // CAT tracking (2.6)
769 cat_seen: bool,
770 scrambled_without_cat_reported: bool,
771
772 // SI repetition-interval timers keyed by table_id (3.2)
773 si_timers: BTreeMap<u8, SiRepetitionTimer>,
774
775 // Unreferenced_PID candidate tracking (3.4)
776 unreferenced_pid_timers: BTreeMap<u16, UnreferencedPidTracking>,
777
778 // Per-`(table_id, section_number)` min-gap tracking
779 // (25 ms minimum-gap dimension of 3.1.a / 3.2 / 3.5.a / 3.6.a / 3.7 / 3.8)
780 min_gap_timers: BTreeMap<MinGapKey, Duration>,
781
782 // Per-section_number `_other` repetition tracking (3.1.b / 3.5.b / 3.6.b)
783 other_repetition_timers: BTreeMap<(u8, u8), OtherRepetitionState>,
784
785 // EIT P/F pairing tracking per sub-table (3.6.c)
786 eit_pf_pairs: BTreeMap<EitSubTableKey, EitPfPairState>,
787
788 // T-STD buffer model (3.3, 3.9, 3.10)
789 tstd: TstdModel,
790}
791
792impl ConformanceMonitor {
793 /// Create a monitor with default configuration.
794 pub fn new() -> Self {
795 Self::with_config(Config::default())
796 }
797
798 /// Create a monitor with the given configuration.
799 pub fn with_config(config: Config) -> Self {
800 let mut si_reassemblies = BTreeMap::new();
801 for &pid in &SI_PIDS {
802 si_reassemblies.insert(
803 pid,
804 SiReassembly {
805 reassembler: SectionReassembler::default(),
806 },
807 );
808 }
809 Self {
810 config,
811 events: Vec::new(),
812 stats: Stats {
813 packets: 0,
814 events: 0,
815 in_sync: false,
816 },
817 in_sync: false,
818 good_run: 0,
819 bad_run: 0,
820 cc_states: BTreeMap::new(),
821 pat_reassembler: SectionReassembler::default(),
822 pat_timer: PresenceTimer {
823 last_seen: Duration::ZERO,
824 reported: false,
825 },
826 pmt_trackings: BTreeMap::new(),
827 es_trackings: BTreeMap::new(),
828 si_reassemblies,
829 pcr_states: BTreeMap::new(),
830 pts_states: BTreeMap::new(),
831 cat_seen: false,
832 scrambled_without_cat_reported: false,
833 si_timers: BTreeMap::new(),
834 unreferenced_pid_timers: BTreeMap::new(),
835 min_gap_timers: BTreeMap::new(),
836 other_repetition_timers: BTreeMap::new(),
837 eit_pf_pairs: BTreeMap::new(),
838 tstd: TstdModel::new(Duration::ZERO),
839 }
840 }
841
842 /// Feed ONE TS packet (any length; 188 expected) with its caller-supplied
843 /// arrival time `t`.
844 ///
845 /// `t` must be monotonic non-decreasing across calls (documented but not
846 /// enforced). Returns the events raised by this packet.
847 pub fn feed(&mut self, ts_packet: &[u8], t: Duration) -> &[ConformanceEvent] {
848 self.events.clear();
849 self.stats.packets += 1;
850
851 // ── Step 2: Sync byte check (1.2) ─────────────────────────────────
852 let sync_ok = !ts_packet.is_empty() && ts_packet[0] == SYNC_BYTE;
853 if !sync_ok {
854 self.emit(Indicator::SyncByteError, None, t, "sync_byte != 0x47");
855 }
856
857 // ── Step 3: Sync hysteresis state machine (1.1) ──────────────────
858 if sync_ok {
859 self.good_run = self.good_run.saturating_add(1);
860 self.bad_run = 0;
861 if !self.in_sync && self.good_run >= self.config.sync_acquire_packets {
862 self.in_sync = true;
863 }
864 } else {
865 self.bad_run = self.bad_run.saturating_add(1);
866 self.good_run = 0;
867 if self.in_sync && self.bad_run >= self.config.sync_loss_packets {
868 self.in_sync = false;
869 self.emit(
870 Indicator::TsSyncLoss,
871 None,
872 t,
873 "sync lost after hysteresis threshold",
874 );
875 }
876 }
877
878 // Per the doc: "If indicator 1.1 is activated then all other
879 // indicators are invalid." While not in sync, suppress all other
880 // indicators.
881 if !self.in_sync {
882 return &self.events;
883 }
884
885 // ── Step 4: Parse TS packet ───────────────────────────────────────
886 let packet = match TsPacket::parse(ts_packet) {
887 Ok(p) => p,
888 Err(_) => return &self.events,
889 };
890 let header = &packet.header;
891 let pid = header.pid;
892
893 // ── 2.1 Transport_error (Table 5.0b indicator 2.1) ──────────────
894 if header.tei {
895 self.emit(
896 Indicator::TransportError,
897 Some(pid),
898 t,
899 format!("transport_error_indicator set on PID 0x{pid:04X}"),
900 );
901 }
902
903 // ── Step 5: Continuity_count_error (1.4) ─────────────────────────
904 if pid != PID_NULL {
905 self.check_cc(
906 pid,
907 header.continuity_counter,
908 header.has_payload,
909 t,
910 ts_packet,
911 );
912 }
913
914 // ── Step 7: PAT_error_2 — scrambling check (1.3.a) ──────────────
915 if pid == PID_PAT && header.scrambling != 0 {
916 self.emit(
917 Indicator::PatError2,
918 Some(PID_PAT),
919 t,
920 format!(
921 "scrambling_control_field != 00 on PID 0x0000 (got {})",
922 header.scrambling
923 ),
924 );
925 }
926
927 // ── Step 8: PMT_error_2 — scrambling check (1.5.a) ──────────────
928 if self.pmt_trackings.contains_key(&pid) && header.scrambling != 0 {
929 self.emit(
930 Indicator::PmtError2,
931 Some(pid),
932 t,
933 format!("scrambling_control_field != 00 on program_map_PID 0x{pid:04X}"),
934 );
935 }
936
937 // ── 2.6 CAT_error — scrambled packet with no CAT (Table 5.0b 2.6)
938 //
939 // At stream start, scrambled packets may arrive before a CAT section
940 // has been acquired; this check fires once in that case. It re-arms
941 // (see `check_cat_table_id`) when a CAT later appears, so the error is
942 // re-detectable after a CAT section is seen.
943 if header.scrambling != 0 && !self.cat_seen && !self.scrambled_without_cat_reported {
944 self.scrambled_without_cat_reported = true;
945 self.emit(
946 Indicator::CatError,
947 Some(pid),
948 t,
949 format!("scrambled packet on PID 0x{pid:04X} but no CAT seen on PID 0x0001"),
950 );
951 }
952
953 // ── Step 6: Section reassembly — PAT ─────────────────────────────
954 if pid == PID_PAT && header.has_payload {
955 if let Some(payload) = packet.payload {
956 self.pat_reassembler.feed(payload, header.pusi);
957 }
958 self.pat_timer.last_seen = t;
959 self.pat_timer.reported = false;
960 while let Some(section_bytes) = self.pat_reassembler.pop_section() {
961 self.check_crc_and_process_pat(§ion_bytes, pid, t);
962 }
963 }
964
965 // ── Step 6b: Section reassembly — PMT PIDs ───────────────────────
966 if self.pmt_trackings.contains_key(&pid) && header.has_payload {
967 if let Some(payload) = packet.payload
968 && let Some(tracking) = self.pmt_trackings.get_mut(&pid)
969 {
970 tracking.reassembler.feed(payload, header.pusi);
971 }
972 let sections: Vec<_> = if let Some(tracking) = self.pmt_trackings.get_mut(&pid) {
973 tracking.timer.last_seen = t;
974 tracking.timer.reported = false;
975 core::iter::from_fn(|| tracking.reassembler.pop_section()).collect()
976 } else {
977 Vec::new()
978 };
979 for section_bytes in §ions {
980 self.check_crc_and_process_pmt(section_bytes, pid, t);
981 }
982 }
983
984 // ── Step 6c: Section reassembly — well-known SI/PSI PIDs (2.2) ───
985 // PAT and PMT PIDs are handled above (they have separate reassembly
986 // for P1 logic). Only process the non-PAT, non-PMT SI PIDs here.
987 if pid != PID_PAT
988 && !self.pmt_trackings.contains_key(&pid)
989 && self.si_reassemblies.contains_key(&pid)
990 && header.has_payload
991 {
992 if let Some(payload) = packet.payload
993 && let Some(si_ra) = self.si_reassemblies.get_mut(&pid)
994 {
995 si_ra.reassembler.feed(payload, header.pusi);
996 }
997 let sections: Vec<_> = if let Some(si_ra) = self.si_reassemblies.get_mut(&pid) {
998 core::iter::from_fn(|| si_ra.reassembler.pop_section()).collect()
999 } else {
1000 Vec::new()
1001 };
1002 for section_bytes in §ions {
1003 self.check_crc_for_si(section_bytes, pid, t);
1004 self.check_cat_table_id(section_bytes, pid, t);
1005 self.check_nit_table_id(section_bytes, pid, t);
1006 self.check_sdt_table_id(section_bytes, pid, t);
1007 self.check_eit_table_id(section_bytes, pid, t);
1008 self.check_rst_table_id(section_bytes, pid, t);
1009 self.check_tdt_table_id(section_bytes, pid, t);
1010 self.update_si_repetition(section_bytes, pid, t);
1011 self.check_si_min_gap(section_bytes, pid, t);
1012 self.check_si_other_section(section_bytes, pid, t);
1013 self.track_eit_pf_pair(section_bytes, t);
1014 }
1015 }
1016 // Also CRC-check completed PAT/PMT sections via the si_reassemblies
1017 // map (these share the same PID). PAT and PMT already have their own
1018 // reassemblers above — the si_reassemblies entries for those PIDs are
1019 // not fed again. CRC checking for PAT/PMT is done inside
1020 // check_crc_and_process_pat / check_crc_and_process_pmt.
1021
1022 // ── Step 9: PID_error — update last_seen for referenced PIDs ─────
1023 if let Some(tracking) = self.es_trackings.get_mut(&pid) {
1024 tracking.timer.last_seen = t;
1025 tracking.timer.reported = false;
1026 }
1027
1028 // ── 2.3a / 2.3b: PCR checks (Table 5.0b indicators 2.3a, 2.3b) ──
1029 if let Some(Ok(af)) = packet.adaptation_field()
1030 && let Some(pcr) = af.pcr
1031 {
1032 self.check_pcr(pid, pcr.as_27mhz(), af.discontinuity_indicator, t);
1033 }
1034
1035 // ── 2.5: PTS check (Table 5.0b indicator 2.5) ───────────────────
1036 if header.pusi
1037 && header.scrambling == 0
1038 && self.es_trackings.contains_key(&pid)
1039 && header.has_payload
1040 && let Some(payload) = packet.payload
1041 {
1042 self.check_pts(pid, payload, t);
1043 }
1044
1045 // ── 3.4: Unreferenced_PID bookkeeping ───────────────────────────
1046 if pid != PID_NULL {
1047 self.track_unreferenced_pid(pid, t);
1048 }
1049
1050 // ── T-STD buffer model (3.3, 3.9, 3.10) ─────────────────────────
1051 // ts_packet is a raw 188-byte (or larger) TS packet.
1052 self.process_tstd(header, ts_packet, t);
1053
1054 // ── Presence-timeout evaluation (1.3.a, 1.5.a, 1.6, 3.4) ────────
1055 self.check_presence_timeouts(t);
1056
1057 // ── EIT P/F pairing evaluation (3.6.c) ──────────────────────────
1058 self.check_eit_pf_pairs(t);
1059
1060 &self.events
1061 }
1062
1063 /// Diagnostic counters.
1064 pub fn stats(&self) -> Stats {
1065 Stats {
1066 in_sync: self.in_sync,
1067 ..self.stats
1068 }
1069 }
1070
1071 // ── Internal helpers ──────────────────────────────────────────────────
1072
1073 fn emit(
1074 &mut self,
1075 indicator: Indicator,
1076 pid: Option<u16>,
1077 at: Duration,
1078 detail: impl Into<String>,
1079 ) {
1080 let event = ConformanceEvent {
1081 indicator,
1082 priority: indicator.priority(),
1083 pid,
1084 at,
1085 detail: detail.into(),
1086 };
1087 self.stats.events += 1;
1088 self.events.push(event);
1089 }
1090
1091 /// Continuity_count_error (1.4) check.
1092 fn check_cc(&mut self, pid: u16, cc: u8, has_payload: bool, t: Duration, raw: &[u8]) {
1093 // Check for discontinuity_indicator in the adaptation field BEFORE
1094 // mutating cc_states (avoids holding the entry borrow across self.emit).
1095 let discontinuity = if raw.len() >= 5 {
1096 let b3 = raw[3];
1097 let has_adaptation = (b3 & 0x20) != 0;
1098 if has_adaptation {
1099 let af_len = raw[4] as usize;
1100 if af_len > 0 && raw.len() > 5 {
1101 (raw[5] & 0x80) != 0
1102 } else {
1103 false
1104 }
1105 } else {
1106 false
1107 }
1108 } else {
1109 false
1110 };
1111
1112 // Compute what we need from the existing state, then decide.
1113 let (expected, is_duplicate, should_emit_dup, should_emit_cc) = {
1114 let state = self.cc_states.entry(pid).or_insert_with(|| CcState {
1115 last_cc: cc,
1116 had_payload: has_payload,
1117 dup_used: false,
1118 initialised: false,
1119 last_packet: Vec::new(),
1120 });
1121
1122 if !state.initialised {
1123 state.last_cc = cc;
1124 state.had_payload = has_payload;
1125 state.dup_used = false;
1126 state.initialised = true;
1127 state.last_packet = raw.to_vec();
1128 return;
1129 }
1130
1131 if discontinuity {
1132 // Will update state below — just signal no emit.
1133 (0u8, false, false, false)
1134 } else {
1135 // ISO/IEC 13818-1:2023 §2.4.3.3: a legal duplicate requires
1136 // the same continuity_counter on a payload-bearing packet
1137 // AND byte-identical content (PCR excepted), AND at most
1138 // one such repeat in a row ("two, and only two"). Comparing
1139 // only `cc == state.last_cc` (as this code used to) would
1140 // accept ANY packet repeating the previous CC, silently
1141 // swallowing genuine continuity faults (issue #956).
1142 use broadcast_common::ts_dup::{DuplicateVerdict, check_duplicate};
1143
1144 let verdict = check_duplicate(&state.last_packet, raw, state.dup_used);
1145 let is_duplicate = !matches!(verdict, DuplicateVerdict::NotDuplicate);
1146 let mut should_emit_dup = false;
1147 let mut should_emit_cc = false;
1148
1149 match verdict {
1150 DuplicateVerdict::Legal => {}
1151 DuplicateVerdict::IllegalThirdRepeat => {
1152 should_emit_dup = true;
1153 }
1154 DuplicateVerdict::NotDuplicate => {
1155 state.dup_used = false;
1156 let expected = if has_payload {
1157 (state.last_cc.wrapping_add(1)) & 0x0F
1158 } else {
1159 state.last_cc
1160 };
1161 if cc != expected {
1162 should_emit_cc = true;
1163 }
1164 }
1165 // `DuplicateVerdict` is `#[non_exhaustive]`; there is no
1166 // fourth variant today, but a future addition must not
1167 // silently fall through as "not a duplicate".
1168 _ => unreachable!("unhandled DuplicateVerdict variant"),
1169 }
1170
1171 (
1172 if has_payload {
1173 (state.last_cc.wrapping_add(1)) & 0x0F
1174 } else {
1175 state.last_cc
1176 },
1177 is_duplicate,
1178 should_emit_dup,
1179 should_emit_cc,
1180 )
1181 }
1182 };
1183
1184 // Now emit events without holding a borrow on cc_states.
1185 if should_emit_dup {
1186 self.emit(
1187 Indicator::ContinuityCountError,
1188 Some(pid),
1189 t,
1190 format!("second consecutive duplicate on PID 0x{pid:04X} (cc={cc})"),
1191 );
1192 }
1193 if should_emit_cc {
1194 self.emit(
1195 Indicator::ContinuityCountError,
1196 Some(pid),
1197 t,
1198 format!("expected cc={expected}, got {cc} on PID 0x{pid:04X}"),
1199 );
1200 }
1201
1202 // Finally, update state.
1203 let state = self.cc_states.get_mut(&pid).unwrap();
1204 if discontinuity {
1205 state.last_cc = cc;
1206 state.had_payload = has_payload;
1207 state.dup_used = false;
1208 state.last_packet = raw.to_vec();
1209 } else if is_duplicate {
1210 // A duplicate (legal or an illegal third-plus repeat) is, by
1211 // construction, byte-identical (PCR excepted) to what's already
1212 // stored, so `last_cc`/`last_packet` are left in place — only
1213 // `dup_used` moves, so a further repeat is still recognised as
1214 // (at least) the second one.
1215 state.dup_used = true;
1216 } else {
1217 state.dup_used = false;
1218 state.last_cc = cc;
1219 state.had_payload = has_payload;
1220 state.last_packet = raw.to_vec();
1221 }
1222 }
1223
1224 /// CRC-check a completed section and, if on PID_PAT, process it.
1225 fn check_crc_and_process_pat(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1226 // 2.2: CRC check on PAT section.
1227 self.check_crc_for_section(section_bytes, pid, t);
1228
1229 self.process_pat_section(section_bytes, t);
1230 }
1231
1232 /// CRC-check a completed section and, if on a PMT PID, process it.
1233 fn check_crc_and_process_pmt(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1234 // 2.2: CRC check on PMT section.
1235 self.check_crc_for_section(section_bytes, pid, t);
1236
1237 self.process_pmt_section(section_bytes, pid, t);
1238 }
1239
1240 /// TR 101 290 v1.4.1 Table 5.0b indicator 2.2 — CRC_error.
1241 ///
1242 /// On any tracked PID, if a completed long-form section has a CRC
1243 /// mismatch, emit `CrcError`. Also feeds section bytes into TBsys
1244 /// for T-STD Buffer_error tracking (indicator 3.3).
1245 fn check_crc_for_section(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1246 let section = match Section::parse(section_bytes) {
1247 Ok(s) => s,
1248 Err(_) => return,
1249 };
1250
1251 // Feed section bytes into TBsys for T-STD buffer tracking
1252 // (ISO/IEC 13818-1 §2.4.2.3: PSI sections pass through TBsys
1253 // before entering Bsys). TBsys has 512 bytes capacity and drains
1254 // at 1 Mbit/s. Overflow fires Buffer_error (3.3).
1255 self.feed_tbsys_section_bytes(section_bytes, pid, t);
1256
1257 // validate_crc returns Ok for short-form sections (no CRC to check).
1258 if let Err(mpeg_ts::error::Error::CrcMismatch { .. }) = section.validate_crc(section_bytes)
1259 {
1260 self.emit(
1261 Indicator::CrcError,
1262 Some(pid),
1263 t,
1264 format!(
1265 "CRC-32 mismatch on PID 0x{:04X} (table_id 0x{:02X})",
1266 pid, section.table_id
1267 ),
1268 );
1269 }
1270 }
1271
1272 /// Feed PSI section bytes into TBsys and check for overflow.
1273 ///
1274 /// TBsys (ISO/IEC 13818-1 §2.4.2.3): 512-byte transport buffer for system
1275 /// information. Receives PSI section bytes from the well-known SI/PSI PIDs.
1276 /// Drains at 1 Mbit/s (125 000 bytes/s). Overflow fires Buffer_error (3.3).
1277 fn feed_tbsys_section_bytes(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1278 let section_len = section_bytes.len() as u64;
1279
1280 // Drain TBsys to current time using the fixed 1 Mbit/s leak rate.
1281 self.tstd.drain_tb_sys(t);
1282
1283 // Check if adding these section bytes would overflow TBsys.
1284 // TBsys has 512 bytes capacity at 1 Mbit/s drain.
1285 if self.tstd.tb_sys.would_overflow(section_len, t) {
1286 self.emit(
1287 Indicator::BufferError,
1288 Some(pid),
1289 t,
1290 format!(
1291 "TBsys overflow on PID 0x{pid:04X}: section {section_len} bytes exceeds {} byte capacity at 1 Mbit/s drain",
1292 tstd::TB_SYS_SIZE,
1293 ),
1294 );
1295 }
1296
1297 // Feed the section bytes into TBsys regardless (the buffer
1298 // model tracks occupancy for empty-interval and delay checks).
1299 let _overflow = self.tstd.tb_sys.feed(section_len, t);
1300
1301 // Indicator 3.9: TBsys empty at least once per second?
1302 if self.tstd.tb_sys.check_empty_interval(t) && !self.tstd.tb_sys_empty_reported {
1303 self.tstd.tb_sys_empty_reported = true;
1304 self.emit(
1305 Indicator::EmptyBufferError,
1306 Some(pid),
1307 t,
1308 format!(
1309 "TBsys not empty in the last {} s",
1310 tstd::TB_SYS_EMPTY_INTERVAL_SECS
1311 ),
1312 );
1313 }
1314
1315 // Indicator 3.10: TBsys data delay > 1 s?
1316 if let Some(delay) = self.tstd.tb_sys.delay_secs(t)
1317 && delay > tstd::DATA_DELAY_LIMIT_SECS as f64
1318 {
1319 self.emit(
1320 Indicator::DataDelayError,
1321 Some(pid),
1322 t,
1323 format!(
1324 "TBsys data delay {delay:.2} s exceeds {} s on PID 0x{pid:04X}",
1325 tstd::DATA_DELAY_LIMIT_SECS,
1326 ),
1327 );
1328 }
1329 }
1330
1331 /// CRC-check for SI PIDs that are not PAT/PMT (handled via si_reassemblies).
1332 fn check_crc_for_si(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1333 self.check_crc_for_section(section_bytes, pid, t);
1334 }
1335
1336 /// TR 101 290 v1.4.1 Table 5.0b indicator 2.6 — CAT_error, condition 1:
1337 /// section with `table_id != 0x01` on PID_CAT.
1338 fn check_cat_table_id(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1339 if pid != PID_CAT {
1340 return;
1341 }
1342 let section = match Section::parse(section_bytes) {
1343 Ok(s) => s,
1344 Err(_) => return,
1345 };
1346 if section.table_id == CAT_TABLE_ID {
1347 // Valid CAT section — mark as seen.
1348 self.cat_seen = true;
1349 // Re-arm the "scrambled without CAT" check so that if a CAT was
1350 // previously absent and then appears, the check resets.
1351 self.scrambled_without_cat_reported = false;
1352 } else {
1353 self.emit(
1354 Indicator::CatError,
1355 Some(PID_CAT),
1356 t,
1357 format!(
1358 "section with table_id 0x{:02X} on PID 0x0001 (expected 0x01 for CAT)",
1359 section.table_id
1360 ),
1361 );
1362 }
1363 }
1364
1365 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.1 — NIT_error, bad-table_id
1366 /// dimension. `docs/tr_101_290.md` clause 3.1: allowed table_ids on
1367 /// PID 0x0010 are NIT_actual (0x40), NIT_other (0x41), stuffing/ST
1368 /// (0x72). The absence dimension (NIT_actual missing for the
1369 /// EN 300 468 §5.1.4 repetition interval) is raised from the shared 3.2
1370 /// SI timer in `check_presence_timeouts`.
1371 fn check_nit_table_id(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1372 if pid != PID_NIT {
1373 return;
1374 }
1375 let section = match Section::parse(section_bytes) {
1376 Ok(s) => s,
1377 Err(_) => return,
1378 };
1379 let allowed = section.table_id == NIT_ACTUAL_TABLE_ID
1380 || section.table_id == NIT_OTHER_TABLE_ID
1381 || section.table_id == STUFFING_TABLE_ID;
1382 if !allowed {
1383 self.emit(
1384 Indicator::NitError,
1385 Some(PID_NIT),
1386 t,
1387 format!(
1388 "section with table_id 0x{:02X} on PID 0x0010 (expected NIT_actual/NIT_other/ST)",
1389 section.table_id
1390 ),
1391 );
1392 }
1393 }
1394
1395 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.5 — SDT_error, bad-table_id
1396 /// dimension. `docs/tr_101_290.md` clause 3.5: allowed table_ids on
1397 /// PID 0x0011 are SDT_actual (0x42), SDT_other (0x46), BAT (0x4A),
1398 /// stuffing/ST (0x72). The absence dimension (SDT_actual missing) is
1399 /// raised from the shared 3.2 SI timer in `check_presence_timeouts`.
1400 fn check_sdt_table_id(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1401 if pid != PID_SDT_BAT {
1402 return;
1403 }
1404 let section = match Section::parse(section_bytes) {
1405 Ok(s) => s,
1406 Err(_) => return,
1407 };
1408 let allowed = section.table_id == SDT_ACTUAL_TABLE_ID
1409 || section.table_id == SDT_OTHER_TABLE_ID
1410 || section.table_id == BAT_TABLE_ID
1411 || section.table_id == STUFFING_TABLE_ID;
1412 if !allowed {
1413 self.emit(
1414 Indicator::SdtError,
1415 Some(PID_SDT_BAT),
1416 t,
1417 format!(
1418 "section with table_id 0x{:02X} on PID 0x0011 (expected SDT_actual/SDT_other/BAT/ST)",
1419 section.table_id
1420 ),
1421 );
1422 }
1423 }
1424
1425 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.6 — EIT_error, bad-table_id
1426 /// dimension. `docs/tr_101_290.md` clause 3.6: allowed table_ids on
1427 /// PID 0x0012 are EIT P/F actual (0x4E), EIT P/F other (0x4F), EIT
1428 /// schedule actual (0x50..=0x5F), EIT schedule other (0x60..=0x6F),
1429 /// stuffing/ST (0x72). The absence dimension (EIT P/F actual missing) is
1430 /// raised from the shared 3.2 SI timer in `check_presence_timeouts`.
1431 fn check_eit_table_id(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1432 if pid != PID_EIT {
1433 return;
1434 }
1435 let section = match Section::parse(section_bytes) {
1436 Ok(s) => s,
1437 Err(_) => return,
1438 };
1439 let table_id = section.table_id;
1440 let allowed = table_id == EIT_PF_ACTUAL_TABLE_ID
1441 || table_id == EIT_PF_OTHER_TABLE_ID
1442 || (dvb_si::tables::eit::TABLE_ID_SCHEDULE_ACTUAL_FIRST
1443 ..=dvb_si::tables::eit::TABLE_ID_SCHEDULE_ACTUAL_LAST)
1444 .contains(&table_id)
1445 || (dvb_si::tables::eit::TABLE_ID_SCHEDULE_OTHER_FIRST
1446 ..=dvb_si::tables::eit::TABLE_ID_SCHEDULE_OTHER_LAST)
1447 .contains(&table_id)
1448 || table_id == STUFFING_TABLE_ID;
1449 if !allowed {
1450 self.emit(
1451 Indicator::EitError,
1452 Some(PID_EIT),
1453 t,
1454 format!(
1455 "section with table_id 0x{table_id:02X} on PID 0x0012 (expected EIT P/F or schedule range or ST)"
1456 ),
1457 );
1458 }
1459 }
1460
1461 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.7 — RST_error, bad-table_id
1462 /// dimension. `docs/tr_101_290.md` clause 3.7: allowed table_ids on
1463 /// PID 0x0013 are RST (0x71), stuffing/ST (0x72). RST has no documented
1464 /// absence threshold in Table 5.0c, so there is no presence dimension.
1465 fn check_rst_table_id(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1466 if pid != PID_RST {
1467 return;
1468 }
1469 let section = match Section::parse(section_bytes) {
1470 Ok(s) => s,
1471 Err(_) => return,
1472 };
1473 let allowed = section.table_id == RST_TABLE_ID || section.table_id == STUFFING_TABLE_ID;
1474 if !allowed {
1475 self.emit(
1476 Indicator::RstError,
1477 Some(PID_RST),
1478 t,
1479 format!(
1480 "section with table_id 0x{:02X} on PID 0x0013 (expected RST/ST)",
1481 section.table_id
1482 ),
1483 );
1484 }
1485 }
1486
1487 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.8 — TDT_error, bad-table_id
1488 /// dimension. `docs/tr_101_290.md` clause 3.8: allowed table_ids on
1489 /// PID 0x0014 are TDT (0x70), stuffing/ST (0x72), TOT (0x73). The
1490 /// absence dimension (TDT missing) is raised from the shared 3.2 SI
1491 /// timer in `check_presence_timeouts`.
1492 fn check_tdt_table_id(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1493 if pid != PID_TDT_TOT {
1494 return;
1495 }
1496 let section = match Section::parse(section_bytes) {
1497 Ok(s) => s,
1498 Err(_) => return,
1499 };
1500 let allowed = section.table_id == TDT_TABLE_ID
1501 || section.table_id == TOT_TABLE_ID
1502 || section.table_id == STUFFING_TABLE_ID;
1503 if !allowed {
1504 self.emit(
1505 Indicator::TdtError,
1506 Some(PID_TDT_TOT),
1507 t,
1508 format!(
1509 "section with table_id 0x{:02X} on PID 0x0014 (expected TDT/TOT/ST)",
1510 section.table_id
1511 ),
1512 );
1513 }
1514 }
1515
1516 /// Whether `pid` is (currently) part of the TR 101 290 v1.4.1 Table 5.0c
1517 /// indicator 3.4 referenced set: PAT, CAT, well-known SI PIDs (NIT/
1518 /// SDT-BAT/EIT/RST/TDT-TOT), the null PID, the reserved-for-future-use
1519 /// range, PMT_PIDs referenced by the PAT, and ES/PCR PIDs referenced by
1520 /// a PMT.
1521 ///
1522 /// Not tracked: CAT-referenced EMM PIDs (this monitor does not decode CA
1523 /// descriptors) and PIDs "user defined as private data streams" (the
1524 /// spec's own carve-out — not distinguishable from the wire alone). Both
1525 /// are documented limitations; see the crate `//!` and README.
1526 fn is_referenced_or_reserved_pid(&self, pid: u16) -> bool {
1527 pid == PID_PAT
1528 || pid == PID_CAT
1529 || pid == PID_NIT
1530 || pid == PID_SDT_BAT
1531 || pid == PID_EIT
1532 || pid == PID_RST
1533 || pid == PID_TDT_TOT
1534 || pid == PID_NULL
1535 || (RESERVED_PID_MIN..=RESERVED_PID_MAX).contains(&pid)
1536 || self.pmt_trackings.contains_key(&pid)
1537 || self.es_trackings.contains_key(&pid)
1538 }
1539
1540 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.4 — Unreferenced_PID
1541 /// bookkeeping. Records the first-seen time of a PID that is not
1542 /// (currently) part of the referenced set. If the PID is already
1543 /// referenced, drops any stale tracking entry. Absence-timeout
1544 /// evaluation happens in `check_presence_timeouts`; a PID that later
1545 /// becomes referenced (see `process_pat_section` / `process_pmt_section`)
1546 /// has its entry removed there.
1547 fn track_unreferenced_pid(&mut self, pid: u16, t: Duration) {
1548 if self.is_referenced_or_reserved_pid(pid) {
1549 self.unreferenced_pid_timers.remove(&pid);
1550 return;
1551 }
1552 self.unreferenced_pid_timers
1553 .entry(pid)
1554 .or_insert_with(|| UnreferencedPidTracking {
1555 first_seen: t,
1556 reported: false,
1557 });
1558 }
1559
1560 /// Process a completed section on PID_PAT.
1561 fn process_pat_section(&mut self, section_bytes: &[u8], t: Duration) {
1562 let section = match Section::parse(section_bytes) {
1563 Ok(s) => s,
1564 Err(_) => return,
1565 };
1566
1567 // 1.3.a: section with table_id other than 0x00 found on PID 0x0000.
1568 if section.table_id != PAT_TABLE_ID {
1569 self.emit(
1570 Indicator::PatError2,
1571 Some(PID_PAT),
1572 t,
1573 format!(
1574 "section with table_id 0x{:02X} on PID 0x0000 (expected 0x00)",
1575 section.table_id
1576 ),
1577 );
1578 return;
1579 }
1580
1581 // Parse the PAT proper.
1582 let pat = match PatSection::parse(section_bytes) {
1583 Ok(p) => p,
1584 Err(_) => return,
1585 };
1586
1587 // Discover program_map_PIDs and start tracking them.
1588 for entry in pat.programmes() {
1589 let pmt_pid = entry.pid;
1590 self.pmt_trackings
1591 .entry(pmt_pid)
1592 .or_insert_with(|| PmtTracking {
1593 timer: PresenceTimer {
1594 last_seen: t,
1595 reported: false,
1596 },
1597 reassembler: SectionReassembler::default(),
1598 });
1599 // 3.4: a program_map_PID is now referenced — it is no longer an
1600 // Unreferenced_PID candidate even if its own packets have not
1601 // been observed yet.
1602 self.unreferenced_pid_timers.remove(&pmt_pid);
1603 }
1604 }
1605
1606 /// Process a completed section on a program_map_PID.
1607 fn process_pmt_section(&mut self, section_bytes: &[u8], _pid: u16, t: Duration) {
1608 let section = match Section::parse(section_bytes) {
1609 Ok(s) => s,
1610 Err(_) => return,
1611 };
1612
1613 // 1.5.a only checks presence and scrambling of table_id 0x02 sections.
1614 // If table_id is not 0x02, skip — we don't emit PMT_error_2 for a
1615 // wrong table_id on a program_map_PID (that's not in the spec for
1616 // 1.5.a).
1617 let pmt_table_id: u8 = dvb_si::tables::pmt::TABLE_ID;
1618 if section.table_id != pmt_table_id {
1619 return;
1620 }
1621
1622 // Parse the PMT proper.
1623 let pmt = match PmtSection::parse(section_bytes) {
1624 Ok(p) => p,
1625 Err(_) => return,
1626 };
1627
1628 // Collect new ES PIDs to add.
1629 let mut new_es_pids: Vec<u16> = Vec::new();
1630 if pmt.pcr_pid != PID_NULL && !self.es_trackings.contains_key(&pmt.pcr_pid) {
1631 new_es_pids.push(pmt.pcr_pid);
1632 }
1633 for stream in &pmt.streams {
1634 let es_pid = stream.elementary_pid;
1635 if !self.es_trackings.contains_key(&es_pid) {
1636 new_es_pids.push(es_pid);
1637 }
1638 }
1639
1640 for es_pid in new_es_pids {
1641 self.es_trackings.insert(
1642 es_pid,
1643 EsTracking {
1644 timer: PresenceTimer {
1645 last_seen: t,
1646 reported: false,
1647 },
1648 },
1649 );
1650 // 3.4: an ES/PCR PID is now referenced by a PMT — no longer an
1651 // Unreferenced_PID candidate.
1652 self.unreferenced_pid_timers.remove(&es_pid);
1653 }
1654 }
1655
1656 /// TR 101 290 v1.4.1 Table 5.0b indicators 2.3a / 2.3b — PCR checks.
1657 fn check_pcr(&mut self, pid: u16, pcr_27mhz: u64, discontinuity: bool, t: Duration) {
1658 let state = self.pcr_states.entry(pid).or_insert_with(|| PcrState {
1659 last_pcr_27mhz: 0,
1660 last_pcr_time: Duration::ZERO,
1661 initialised: false,
1662 });
1663
1664 if !state.initialised {
1665 state.last_pcr_27mhz = pcr_27mhz;
1666 state.last_pcr_time = t;
1667 state.initialised = true;
1668 return;
1669 }
1670
1671 // Snapshot state for decision-making before any emit.
1672 let last_pcr_time = state.last_pcr_time;
1673 let last_pcr_27mhz = state.last_pcr_27mhz;
1674
1675 // 2.3a: PCR_repetition_error — interval between consecutive PCR
1676 // values exceeds the configured limit.
1677 let rep_interval = t.saturating_sub(last_pcr_time);
1678 let should_emit_rep = rep_interval > self.config.pcr_repetition_limit;
1679
1680 // 2.3b: PCR_discontinuity_indicator_error — PCR delta exceeds 100 ms
1681 // without a signalled discontinuity.
1682 let delta =
1683 (pcr_27mhz.wrapping_add(PCR_MODULUS_27MHZ) - last_pcr_27mhz) % PCR_MODULUS_27MHZ;
1684 let delta_ms = delta * 1000 / CLOCK_27MHZ;
1685 let limit_ms = self.config.pcr_discontinuity_limit.as_millis() as u64;
1686 let should_emit_disc = delta_ms > limit_ms && !discontinuity;
1687
1688 // Emit outside the HashMap borrow.
1689 if should_emit_rep {
1690 self.emit(
1691 Indicator::PcrRepetitionError,
1692 Some(pid),
1693 t,
1694 format!(
1695 "PCR interval {} ms exceeds limit {} ms on PID 0x{:04X}",
1696 rep_interval.as_millis(),
1697 self.config.pcr_repetition_limit.as_millis(),
1698 pid
1699 ),
1700 );
1701 }
1702 if should_emit_disc {
1703 self.emit(
1704 Indicator::PcrDiscontinuityError,
1705 Some(pid),
1706 t,
1707 format!(
1708 "PCR delta {delta_ms} ms exceeds limit {limit_ms} ms on PID 0x{pid:04X} without discontinuity_indicator"
1709 ),
1710 );
1711 }
1712
1713 // Update state.
1714 let state = self.pcr_states.get_mut(&pid).unwrap();
1715 state.last_pcr_27mhz = pcr_27mhz;
1716 state.last_pcr_time = t;
1717 }
1718
1719 /// TR 101 290 v1.4.1 Table 5.0b indicator 2.5 — PTS_error.
1720 ///
1721 /// Peeks the PES header on an elementary-stream PID for PTS_DTS_flags.
1722 /// Only checks PIDs that have been "armed" by seeing at least one PTS.
1723 fn check_pts(&mut self, pid: u16, payload: &[u8], t: Duration) {
1724 // PES start-code prefix: 00 00 01.
1725 if payload.len() < PES_FLAGS_OFFSET + 2 {
1726 return;
1727 }
1728 if payload[0] != PES_PREFIX_0 || payload[1] != PES_PREFIX_1 || payload[2] != PES_PREFIX_2 {
1729 return;
1730 }
1731
1732 // Byte 6: `'10' + flags` — the top two bits must be `10`.
1733 let flags_byte = payload[PES_FLAGS_OFFSET];
1734 if (flags_byte >> 6) != 0b10 {
1735 return;
1736 }
1737
1738 // Byte 7: PTS_DTS_flags in bits `[7:6]`.
1739 let pts_dts_flags = payload[PES_FLAGS_OFFSET + 1] & PES_PTS_DTS_FLAGS_MASK;
1740 let pts_present = (pts_dts_flags & PES_PTS_PRESENT) != 0;
1741 if !pts_present {
1742 return;
1743 }
1744
1745 let state = self.pts_states.entry(pid).or_insert_with(|| PtsState {
1746 last_pts_time: Duration::ZERO,
1747 armed: false,
1748 });
1749
1750 if !state.armed {
1751 // First PTS on this PID — arm the check, no error yet.
1752 state.last_pts_time = t;
1753 state.armed = true;
1754 return;
1755 }
1756
1757 // Snapshot state for decision-making before any emit.
1758 let last_pts_time = state.last_pts_time;
1759 let pts_interval = t.saturating_sub(last_pts_time);
1760 let should_emit = pts_interval > self.config.pts_repetition_limit;
1761
1762 if should_emit {
1763 self.emit(
1764 Indicator::PtsError,
1765 Some(pid),
1766 t,
1767 format!(
1768 "PTS interval {} ms exceeds limit {} ms on PID 0x{:04X}",
1769 pts_interval.as_millis(),
1770 self.config.pts_repetition_limit.as_millis(),
1771 pid
1772 ),
1773 );
1774 }
1775
1776 // Update state.
1777 let state = self.pts_states.get_mut(&pid).unwrap();
1778 state.last_pts_time = t;
1779 }
1780
1781 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.2 — update SI repetition timer
1782 /// when a completed section on a well-known SI PID matches one of the four
1783 /// tracked table_ids.
1784 fn update_si_repetition(&mut self, section_bytes: &[u8], _pid: u16, t: Duration) {
1785 let table_id = match Section::parse(section_bytes) {
1786 Ok(s) => s.table_id,
1787 Err(_) => return,
1788 };
1789
1790 let is_tracked = table_id == NIT_ACTUAL_TABLE_ID
1791 || table_id == SDT_ACTUAL_TABLE_ID
1792 || table_id == EIT_PF_ACTUAL_TABLE_ID
1793 || table_id == TDT_TABLE_ID;
1794
1795 if !is_tracked {
1796 return;
1797 }
1798
1799 let timer = self
1800 .si_timers
1801 .entry(table_id)
1802 .or_insert_with(|| SiRepetitionTimer {
1803 last_seen: Duration::ZERO,
1804 reported: false,
1805 armed: false,
1806 });
1807
1808 timer.last_seen = t;
1809 timer.reported = false;
1810 timer.armed = true;
1811 }
1812
1813 /// TR 101 290 v1.4.1 Table 5.0c — 25 ms minimum-gap check shared by
1814 /// 3.1.a / 3.2 / 3.5.a / 3.6.a / 3.7 / 3.8.
1815 ///
1816 /// Tracks last-seen time per `(table_id, section_number)`, not per
1817 /// `table_id` alone, so a dense legitimate multi-section table does not
1818 /// produce false positives. The key also includes `pid` so the same
1819 /// `(table_id, section_number)` pair arriving on different PIDs is not
1820 /// conflated.
1821 ///
1822 /// Called for every completed section on the well-known SI PIDs.
1823 fn check_si_min_gap(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1824 let table_id = match Section::parse(section_bytes) {
1825 Ok(s) => s.table_id,
1826 Err(_) => return,
1827 };
1828
1829 // Only the tracked table_ids participate in the min-gap check.
1830 let min_gap_table_ids = [
1831 NIT_ACTUAL_TABLE_ID,
1832 SDT_ACTUAL_TABLE_ID,
1833 EIT_PF_ACTUAL_TABLE_ID,
1834 RST_TABLE_ID,
1835 TDT_TABLE_ID,
1836 ];
1837 if !min_gap_table_ids.contains(&table_id) {
1838 return;
1839 }
1840
1841 let section_number = match Section::parse(section_bytes) {
1842 Ok(s) => s.section_number,
1843 Err(_) => return,
1844 };
1845
1846 let key = MinGapKey {
1847 pid,
1848 table_id,
1849 section_number,
1850 };
1851
1852 if let Some(&last_time) = self.min_gap_timers.get(&key) {
1853 let gap = t.saturating_sub(last_time);
1854 if gap < self.config.si_min_gap {
1855 self.emit(
1856 Indicator::SiMinGapError,
1857 Some(pid),
1858 t,
1859 format!(
1860 "table_id 0x{table_id:02X} section_number {section_number} on PID 0x{pid:04X}: gap {gap} µs < {limit} ms minimum",
1861 gap = gap.as_micros(),
1862 limit = self.config.si_min_gap.as_millis(),
1863 ),
1864 );
1865 }
1866 }
1867
1868 self.min_gap_timers.insert(key, t);
1869 }
1870
1871 /// TR 101 290 v1.4.1 Table 5.0c indicators 3.1.b / 3.5.b / 3.6.b —
1872 /// `_other` repetition interval check.
1873 ///
1874 /// Only checks once the sub-table's presence is **established** (observed
1875 /// at least twice). A stream that legitimately carries no `_other` table
1876 /// is never flagged.
1877 fn check_other_repetition(&mut self, table_id: u8, section_number: u8, pid: u16, t: Duration) {
1878 // Only the three `_other` table_ids.
1879 if table_id != NIT_OTHER_TABLE_ID
1880 && table_id != SDT_OTHER_TABLE_ID
1881 && table_id != EIT_PF_OTHER_TABLE_ID
1882 {
1883 return;
1884 }
1885
1886 let key = (table_id, section_number);
1887 let entry =
1888 self.other_repetition_timers
1889 .entry(key)
1890 .or_insert_with(|| OtherRepetitionState {
1891 occurrences: 0,
1892 last_seen: Duration::ZERO,
1893 reported: false,
1894 });
1895
1896 entry.occurrences += 1;
1897
1898 // Snapshot what we need before any emit.
1899 let should_report = entry.occurrences >= 2
1900 && !entry.reported
1901 && t.saturating_sub(entry.last_seen) > self.config.si_other_interval;
1902 let last_seen = entry.last_seen;
1903
1904 entry.last_seen = t;
1905
1906 if should_report {
1907 entry.reported = true;
1908
1909 let indicator = match table_id {
1910 NIT_OTHER_TABLE_ID => Indicator::NitOtherError,
1911 SDT_OTHER_TABLE_ID => Indicator::SdtOtherError,
1912 _ => Indicator::EitOtherError,
1913 };
1914 let (table_name, clause) = match table_id {
1915 NIT_OTHER_TABLE_ID => ("NIT_other (0x41)", "3.1.b"),
1916 SDT_OTHER_TABLE_ID => ("SDT_other (0x46)", "3.5.b"),
1917 _ => ("EIT_other (0x4F)", "3.6.b"),
1918 };
1919 let interval = t.saturating_sub(last_seen);
1920 let interval_ms = interval.as_millis();
1921 let limit = self.config.si_other_interval.as_secs();
1922 self.emit(
1923 indicator,
1924 Some(pid),
1925 t,
1926 format!(
1927 "{table_name} section_number {section_number}: interval {interval_ms} ms exceeds {limit} s ({clause})",
1928 ),
1929 );
1930 }
1931 }
1932
1933 /// TR 101 290 v1.4.1 Table 5.0c indicator 3.6.c — EIT_PF_error: track
1934 /// section-0/section-1 presence per sub-table.
1935 ///
1936 /// A sub-table is keyed by `(service_id, transport_stream_id,
1937 /// original_network_id)`. If either section exists, both should.
1938 fn track_eit_pf_pair(&mut self, section_bytes: &[u8], t: Duration) {
1939 let table_id = match Section::parse(section_bytes) {
1940 Ok(s) => s.table_id,
1941 Err(_) => return,
1942 };
1943
1944 // Only EIT P/F actual
1945 if table_id != EIT_PF_ACTUAL_TABLE_ID {
1946 return;
1947 }
1948
1949 let eit = match EitSection::parse(section_bytes) {
1950 Ok(e) => e,
1951 Err(_) => return,
1952 };
1953
1954 if eit.section_number > 1 {
1955 // Only sections '0' (present) and '1' (following) participate.
1956 return;
1957 }
1958
1959 let key = EitSubTableKey {
1960 service_id: eit.service_id,
1961 transport_stream_id: eit.transport_stream_id,
1962 original_network_id: eit.original_network_id,
1963 };
1964
1965 let entry = self
1966 .eit_pf_pairs
1967 .entry(key)
1968 .or_insert_with(|| EitPfPairState {
1969 has_section_0: false,
1970 has_section_1: false,
1971 reported: false,
1972 first_incomplete_at: None,
1973 });
1974
1975 if eit.section_number == 0 {
1976 entry.has_section_0 = true;
1977 } else {
1978 entry.has_section_1 = true;
1979 }
1980
1981 // If one section is present but the other is not, record the time
1982 // of this first incomplete observation (so the pairing check can
1983 // wait for the partner to arrive).
1984 if entry.first_incomplete_at.is_none() && (entry.has_section_0 != entry.has_section_1) {
1985 entry.first_incomplete_at = Some(t);
1986 }
1987
1988 // If both sections have now arrived, clear the incomplete marker.
1989 if entry.has_section_0 && entry.has_section_1 {
1990 entry.first_incomplete_at = None;
1991 }
1992 }
1993
1994 /// Evaluate EIT P/F pairing and emit EIT_PF_error for any sub-table where
1995 /// one section exists without the other for longer than the EIT P/F
1996 /// repetition interval (2 s per EN 300 468 §5.2.4).
1997 fn check_eit_pf_pairs(&mut self, t: Duration) {
1998 let limit = self.config.si_eit_pf_interval;
1999
2000 let to_report: Vec<EitSubTableKey> = self
2001 .eit_pf_pairs
2002 .iter()
2003 .filter_map(|(&key, state)| {
2004 if state.reported {
2005 return None;
2006 }
2007 match (state.has_section_0, state.has_section_1) {
2008 (true, false) | (false, true) => {
2009 // Only fire if the incomplete pair has persisted
2010 // longer than the EIT P/F repetition interval.
2011 if let Some(first_at) = state.first_incomplete_at {
2012 if t.saturating_sub(first_at) > limit {
2013 Some(key)
2014 } else {
2015 None
2016 }
2017 } else {
2018 None
2019 }
2020 }
2021 _ => None,
2022 }
2023 })
2024 .collect();
2025
2026 for key in to_report {
2027 if let Some(state) = self.eit_pf_pairs.get_mut(&key) {
2028 state.reported = true;
2029 }
2030 self.emit(
2031 Indicator::EitPfError,
2032 Some(PID_EIT),
2033 t,
2034 format!(
2035 "EIT P/F sub-table (service_id=0x{:04X} ts_id=0x{:04X} onw_id=0x{:04X}): one section present without the other",
2036 key.service_id, key.transport_stream_id, key.original_network_id,
2037 ),
2038 );
2039 }
2040 }
2041
2042 /// Dispatch `_other` repetition tracking for completed SI sections on
2043 /// the well-known PIDs. Only sections with `table_id` NIT_other (0x41),
2044 /// SDT_other (0x46), or EIT P/F other (0x4F) are tracked.
2045 fn check_si_other_section(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
2046 let section = match Section::parse(section_bytes) {
2047 Ok(s) => s,
2048 Err(_) => return,
2049 };
2050
2051 let table_id = section.table_id;
2052 if table_id != NIT_OTHER_TABLE_ID
2053 && table_id != SDT_OTHER_TABLE_ID
2054 && table_id != EIT_PF_OTHER_TABLE_ID
2055 {
2056 return;
2057 }
2058
2059 self.check_other_repetition(table_id, section.section_number, pid, t);
2060 }
2061
2062 /// T-STD buffer model processing: run the buffer simulation for one TS
2063 /// packet and evaluate indicators 3.3 (Buffer_error), 3.9
2064 /// (Empty_buffer_error), and 3.10 (Data_delay_error).
2065 ///
2066 /// # Buffer model
2067 ///
2068 /// - **TBn** (per-PID, 512 bytes): each payload PID gets a transport
2069 /// buffer. The full 188-byte packet (minus sync) enters the buffer
2070 /// instantaneously at the caller's timestamp `t`. The buffer drains at
2071 /// the stream's effective bitrate (estimated from accumulated bytes
2072 /// divided by elapsed wall-clock time, clamped to a floor of
2073 /// 125 kbit/s).
2074 /// - **TBsys** (global, 512 bytes, 1 Mbit/s leak): the PSI/SI PIDs (PAT,
2075 /// CAT, NIT, SDT/BAT, EIT, RST, TDT/TOT) feed into TBsys. When a
2076 /// completed section is assembled and passed to the SI/PSI decoder, the
2077 /// section bytes are removed from TBsys.
2078 ///
2079 /// # Known limitation
2080 ///
2081 /// The effective bitrate estimate from accumulated bytes / wall-clock time
2082 /// is inherently approximate when the caller's timestamps do not match the
2083 /// actual transport rate. The monitor documents this: the caller is
2084 /// responsible for supplying realistic timestamps.
2085 fn process_tstd(&mut self, header: &mpeg_ts::ts::TsHeader, ts_packet: &[u8], t: Duration) {
2086 let pid = header.pid;
2087
2088 // Periodic TBsys drain and empty-interval check. TBsys feeds at
2089 // section-completion, but we must drain it on every packet to
2090 // accurately model the 1 Mbit/s continuous drain rate.
2091 self.tstd.drain_tb_sys(t);
2092
2093 // ── TBsys: PSI/SI PIDs ──────────────────────────────────────────
2094 let is_si_pid = pid == PID_PAT
2095 || pid == PID_CAT
2096 || pid == PID_NIT
2097 || pid == PID_SDT_BAT
2098 || pid == PID_EIT
2099 || pid == PID_RST
2100 || pid == PID_TDT_TOT;
2101
2102 // Count the packet bytes for T-STD accounting.
2103 // The T-STD model uses the full 188-byte TS packet as the arrival
2104 // unit (ISO/IEC 13818-1 §2.4.2.3: the transport buffer receives TS
2105 // packets and strips the TS header + adaptation field before passing
2106 // bytes downstream; but for TBn overflow, the relevant input unit
2107 // is the packet's contribution to buffer occupancy).
2108 let packet_bytes = ts_packet.len() as u64;
2109
2110 if is_si_pid && header.has_payload {
2111 // TBsys is fed at section-completion time (see
2112 // `feed_tbsys_section_bytes` called from `check_crc_for_section`).
2113 // The per-packet path only drains TBsys and does empty/delay
2114 // checks, which are also done at section-completion.
2115 }
2116
2117 // ── TBn: per-PID buffers for all non-SI, non-null PIDs ──────────
2118 if pid == PID_NULL || is_si_pid {
2119 return;
2120 }
2121
2122 // Compute T-STD checks without holding a mutable borrow on self.tstd
2123 // across any self.emit() calls. We collect the events to emit, then
2124 // emit them after releasing the borrow.
2125 struct TstdCheck {
2126 buffer_overflow: bool,
2127 empty_interval: bool,
2128 delay_exceeded: bool,
2129 }
2130
2131 let check = {
2132 let entry = self.tstd.pid_buffers.entry(pid).or_insert_with(|| {
2133 // Initial leak rate conservatively high: the effective
2134 // bitrate will be estimated once we have timing data.
2135 tstd::PidStdState::new(1_000_000u64, t)
2136 });
2137
2138 // Accumulate bytes for effective bitrate estimation.
2139 entry.total_bytes += packet_bytes;
2140
2141 // Estimate the effective transport rate from accumulated bytes
2142 // and elapsed wall-clock time. Wait until we have at least
2143 // 1 ms of history before estimating; use a default of
2144 // ~6 Mbit/s (750 KB/s) until then.
2145 let leak = {
2146 let elapsed_us = t.saturating_sub(entry.first_seen).as_micros() as u64;
2147 if elapsed_us >= 1_000 {
2148 // bytes per second = total_bytes * 1_000_000 / elapsed_us
2149 (entry.total_bytes * 1_000_000 / elapsed_us).max(tstd::TB_LEAK_RATE_FLOOR)
2150 } else {
2151 // Default: generous initial rate (~40 Mbit/s).
2152 // This prevents false overflows during the first
2153 // millisecond before we have enough data to estimate
2154 // the effective bitrate. For a real ~38 Mbit/s stream
2155 // with 40 µs inter-packet, this is close enough to
2156 // prevent overflow during the convergence period.
2157 5_000_000u64
2158 }
2159 };
2160 entry.tb.set_leak_rate(leak);
2161
2162 // Drain the buffer TO the current time (i.e. drain up to the
2163 // arrival instant of this packet, before the packet data enters).
2164 entry.tb.drain_to(t);
2165
2166 let buffer_overflow = {
2167 // Drain, then feed (for occupancy tracking). We do NOT flag
2168 // TBn overflow as BufferError — accurate overflow detection
2169 // requires the coded bitrate Rxn from descriptors
2170 // (multiplex_buffer_descriptor). Without that, the
2171 // estimated leak rate is approximate and produces false
2172 // positives on clean streams. TBn occupancy is still
2173 // tracked for empty-interval and data-delay checks.
2174 entry.tb.drain_to(t);
2175 let _overflow = entry.tb.feed(packet_bytes, t);
2176 false
2177 };
2178 let empty_interval = entry.tb.check_empty_interval(t) && !entry.empty_reported;
2179 let delay_exceeded = if let Some(delay) = entry.tb.delay_secs(t) {
2180 delay > tstd::DATA_DELAY_LIMIT_SECS as f64 && !entry.delay_reported
2181 } else {
2182 false
2183 };
2184
2185 if empty_interval {
2186 entry.empty_reported = true;
2187 }
2188 if delay_exceeded {
2189 entry.delay_reported = true;
2190 }
2191 entry.last_packet_time = t;
2192
2193 TstdCheck {
2194 buffer_overflow,
2195 empty_interval,
2196 delay_exceeded,
2197 }
2198 };
2199
2200 // Emit events outside the borrow.
2201 if check.buffer_overflow {
2202 self.emit(
2203 Indicator::BufferError,
2204 Some(pid),
2205 t,
2206 format!(
2207 "TBn overflow on PID 0x{pid:04X}: {} byte capacity exceeded",
2208 tstd::TB_SIZE,
2209 ),
2210 );
2211 }
2212
2213 if check.empty_interval {
2214 self.emit(
2215 Indicator::EmptyBufferError,
2216 Some(pid),
2217 t,
2218 format!(
2219 "TBn not empty in the last {} s on PID 0x{pid:04X}",
2220 tstd::TB_EMPTY_INTERVAL_SECS,
2221 ),
2222 );
2223 }
2224
2225 if check.delay_exceeded {
2226 self.emit(
2227 Indicator::DataDelayError,
2228 Some(pid),
2229 t,
2230 format!(
2231 "TBn data delay exceeds {} s on PID 0x{pid:04X}",
2232 tstd::DATA_DELAY_LIMIT_SECS,
2233 ),
2234 );
2235 }
2236 }
2237
2238 /// Evaluate all presence/absence timeouts against the current time `t`.
2239 fn check_presence_timeouts(&mut self, t: Duration) {
2240 // 1.3.a: PAT presence timeout
2241 if t.saturating_sub(self.pat_timer.last_seen) > self.config.pat_max_interval
2242 && !self.pat_timer.reported
2243 {
2244 self.pat_timer.reported = true;
2245 self.emit(
2246 Indicator::PatError2,
2247 Some(PID_PAT),
2248 t,
2249 format!(
2250 "no PAT section within {} ms",
2251 self.config.pat_max_interval.as_millis()
2252 ),
2253 );
2254 }
2255
2256 // 1.5.a: PMT presence timeout per program_map_PID
2257 // Collect PIDs that need events, then emit outside the iteration.
2258 let pmt_timeouts: Vec<(u16, u64)> = self
2259 .pmt_trackings
2260 .iter()
2261 .filter_map(|(&pid, tracking)| {
2262 if t.saturating_sub(tracking.timer.last_seen) > self.config.pmt_max_interval
2263 && !tracking.timer.reported
2264 {
2265 Some((pid, self.config.pmt_max_interval.as_millis() as u64))
2266 } else {
2267 None
2268 }
2269 })
2270 .collect();
2271 for (pid, interval_ms) in pmt_timeouts {
2272 if let Some(tracking) = self.pmt_trackings.get_mut(&pid) {
2273 tracking.timer.reported = true;
2274 }
2275 self.emit(
2276 Indicator::PmtError2,
2277 Some(pid),
2278 t,
2279 format!("no PMT section on program_map_PID 0x{pid:04X} within {interval_ms} ms"),
2280 );
2281 }
2282
2283 // 1.6: PID_error — referenced PID absence
2284 let pid_timeouts: Vec<(u16, u64)> = self
2285 .es_trackings
2286 .iter()
2287 .filter_map(|(&pid, tracking)| {
2288 if t.saturating_sub(tracking.timer.last_seen) > self.config.pid_error_period
2289 && !tracking.timer.reported
2290 {
2291 Some((pid, self.config.pid_error_period.as_secs()))
2292 } else {
2293 None
2294 }
2295 })
2296 .collect();
2297 for (pid, period_secs) in pid_timeouts {
2298 if let Some(tracking) = self.es_trackings.get_mut(&pid) {
2299 tracking.timer.reported = true;
2300 }
2301 self.emit(
2302 Indicator::PidError,
2303 Some(pid),
2304 t,
2305 format!("referenced PID 0x{pid:04X} absent for > {period_secs} s"),
2306 );
2307 }
2308
2309 // 3.2: SI_repetition_error — maximum interval for tracked SI tables.
2310 // Collect table_ids that need events, then emit outside the iteration.
2311 let si_timeouts: Vec<(u8, u64, u16, u64)> = self
2312 .si_timers
2313 .iter()
2314 .filter_map(|(&table_id, timer)| {
2315 if !timer.armed || timer.reported {
2316 return None;
2317 }
2318 let (limit, pid) = match table_id {
2319 NIT_ACTUAL_TABLE_ID => (self.config.si_nit_interval, PID_NIT),
2320 SDT_ACTUAL_TABLE_ID => (self.config.si_sdt_interval, PID_SDT_BAT),
2321 EIT_PF_ACTUAL_TABLE_ID => (self.config.si_eit_pf_interval, PID_EIT),
2322 TDT_TABLE_ID => (self.config.si_tdt_interval, PID_TDT_TOT),
2323 _ => return None,
2324 };
2325 let interval = t.saturating_sub(timer.last_seen);
2326 if interval > limit {
2327 Some((
2328 table_id,
2329 interval.as_millis() as u64,
2330 pid,
2331 limit.as_millis() as u64,
2332 ))
2333 } else {
2334 None
2335 }
2336 })
2337 .collect();
2338 for (table_id, interval_ms, pid, limit_ms) in si_timeouts {
2339 if let Some(timer) = self.si_timers.get_mut(&table_id) {
2340 timer.reported = true;
2341 }
2342 // The four `_actual` table_ids share this ONE lazily-armed
2343 // timer: an absence past the interval is simultaneously
2344 // SI_repetition_error (3.2, the general repetition-rate
2345 // indicator, emitted unconditionally below) AND the presence
2346 // dimension of the corresponding table-specific indicator
2347 // (NIT_error 3.1 / SDT_error 3.5 / EIT_error 3.6 / TDT_error 3.8)
2348 // — the same underlying absence, per the spec's historical
2349 // combined indicator vs. the split `_actual` variants (see
2350 // docs/tr_101_290.md notes 2-4).
2351 let (table_name, group_indicator) = match table_id {
2352 NIT_ACTUAL_TABLE_ID => ("NIT_actual", Some(Indicator::NitError)),
2353 SDT_ACTUAL_TABLE_ID => ("SDT_actual", Some(Indicator::SdtError)),
2354 EIT_PF_ACTUAL_TABLE_ID => ("EIT_P/F_actual", Some(Indicator::EitError)),
2355 TDT_TABLE_ID => ("TDT", Some(Indicator::TdtError)),
2356 _ => ("unknown", None),
2357 };
2358 self.emit(
2359 Indicator::SiRepetitionError,
2360 Some(pid),
2361 t,
2362 format!("{table_name} repetition interval {interval_ms} ms exceeds {limit_ms} ms"),
2363 );
2364 if let Some(indicator) = group_indicator {
2365 self.emit(
2366 indicator,
2367 Some(pid),
2368 t,
2369 format!("no {table_name} section on PID 0x{pid:04X} within {limit_ms} ms"),
2370 );
2371 }
2372 }
2373
2374 // 3.4: Unreferenced_PID — a PID persisting beyond the presence
2375 // threshold without being referenced by the PAT/CAT/a PMT or one of
2376 // the well-known SI PIDs.
2377 let unref_timeouts: Vec<(u16, u64)> = self
2378 .unreferenced_pid_timers
2379 .iter()
2380 .filter_map(|(&pid, timer)| {
2381 if timer.reported {
2382 return None;
2383 }
2384 let elapsed = t.saturating_sub(timer.first_seen);
2385 if elapsed > self.config.unreferenced_pid_period {
2386 Some((pid, self.config.unreferenced_pid_period.as_millis() as u64))
2387 } else {
2388 None
2389 }
2390 })
2391 .collect();
2392 for (pid, period_ms) in unref_timeouts {
2393 if let Some(timer) = self.unreferenced_pid_timers.get_mut(&pid) {
2394 timer.reported = true;
2395 }
2396 self.emit(
2397 Indicator::UnreferencedPid,
2398 Some(pid),
2399 t,
2400 format!(
2401 "PID 0x{pid:04X} present for > {period_ms} ms without being referenced by PAT/CAT/a PMT or a well-known SI PID"
2402 ),
2403 );
2404 }
2405 }
2406}
2407
2408impl Default for ConformanceMonitor {
2409 fn default() -> Self {
2410 Self::new()
2411 }
2412}
2413
2414#[cfg(test)]
2415mod tests;