1#![cfg_attr(not(feature = "std"), no_std)]
36#![cfg_attr(docsrs, feature(doc_cfg))]
37#![doc = "\n# Examples\n"]
40#![doc = "Two runnable examples ship with this crate (`cargo run -p dvb-conformance --example <name>`).\n"]
41#![doc = "\n## `monitor_stream`\n\n```rust,ignore"]
42#![doc = include_str!("../examples/monitor_stream.rs")]
43#![doc = "```\n\n## `priority_breakdown`\n\n```rust,ignore"]
44#![doc = include_str!("../examples/priority_breakdown.rs")]
45#![doc = "```"]
46extern crate alloc;
47
48use alloc::collections::BTreeMap;
49use alloc::format;
50use alloc::string::String;
51use alloc::vec::Vec;
52use core::time::Duration;
53
54use broadcast_common::Parse;
55use dvb_si::tables::pat::{PatSection, TABLE_ID as PAT_TABLE_ID};
56use dvb_si::tables::pmt::PmtSection;
57use mpeg_ts::section::Section;
58use mpeg_ts::ts::{SectionReassembler, TsPacket};
59
60const PID_PAT: u16 = 0x0000;
64const PID_CAT: u16 = 0x0001;
66const PID_NIT: u16 = 0x0010;
68const PID_SDT_BAT: u16 = 0x0011;
70const PID_EIT: u16 = 0x0012;
72const PID_RST: u16 = 0x0013;
74const PID_TDT_TOT: u16 = 0x0014;
76const PID_NULL: u16 = 0x1FFF;
78
79const SYNC_BYTE: u8 = 0x47;
81
82const SI_PIDS: [u16; 7] = [
84 PID_PAT,
85 PID_CAT,
86 PID_NIT,
87 PID_SDT_BAT,
88 PID_EIT,
89 PID_RST,
90 PID_TDT_TOT,
91];
92
93const RESERVED_PID_MIN: u16 = 0x0002;
96const RESERVED_PID_MAX: u16 = 0x000F;
98
99const DEFAULT_PAT_MAX_INTERVAL_MS: u64 = 500;
104
105const DEFAULT_PMT_MAX_INTERVAL_MS: u64 = 500;
107
108const DEFAULT_PID_ERROR_PERIOD_SECS: u64 = 5;
110
111const DEFAULT_SYNC_ACQUIRE_PACKETS: u8 = 5;
114
115const DEFAULT_SYNC_LOSS_PACKETS: u8 = 2;
118
119const DEFAULT_PCR_REPETITION_LIMIT_MS: u64 = 100;
122
123const DEFAULT_PCR_DISCONTINUITY_LIMIT_MS: u64 = 100;
126
127const DEFAULT_PTS_REPETITION_LIMIT_MS: u64 = 700;
130
131const DEFAULT_SI_NIT_INTERVAL_SECS: u64 = 10;
134
135const DEFAULT_SI_SDT_INTERVAL_SECS: u64 = 2;
138
139const DEFAULT_SI_EIT_PF_INTERVAL_SECS: u64 = 2;
142
143const DEFAULT_SI_TDT_INTERVAL_SECS: u64 = 30;
146
147const DEFAULT_UNREFERENCED_PID_PERIOD_MS: u64 = 500;
151
152const PCR_MODULUS_27MHZ: u64 = (1u64 << 33) * 300;
157
158const CLOCK_27MHZ: u64 = 27_000_000;
160
161const PES_PREFIX_0: u8 = 0x00;
163const PES_PREFIX_1: u8 = 0x00;
165const PES_PREFIX_2: u8 = 0x01;
167
168const PES_FLAGS_OFFSET: usize = 6;
171
172const PES_PTS_DTS_FLAGS_MASK: u8 = 0b1100_0000;
175
176const PES_PTS_PRESENT: u8 = 0b1000_0000;
178
179const CAT_TABLE_ID: u8 = dvb_si::table_id::TableId::Cat as u8;
181
182const NIT_ACTUAL_TABLE_ID: u8 = dvb_si::table_id::TableId::NetworkInformationActual as u8;
184
185const SDT_ACTUAL_TABLE_ID: u8 = dvb_si::table_id::TableId::ServiceDescriptionActual as u8;
187
188const EIT_PF_ACTUAL_TABLE_ID: u8 = dvb_si::table_id::TableId::EventInformationPfActual as u8;
190
191const TDT_TABLE_ID: u8 = dvb_si::table_id::TableId::TimeAndDate as u8;
193
194const NIT_OTHER_TABLE_ID: u8 = dvb_si::table_id::TableId::NetworkInformationOther as u8;
196
197const SDT_OTHER_TABLE_ID: u8 = dvb_si::table_id::TableId::ServiceDescriptionOther as u8;
199
200const BAT_TABLE_ID: u8 = dvb_si::table_id::TableId::BouquetAssociation as u8;
203
204const EIT_PF_OTHER_TABLE_ID: u8 = dvb_si::table_id::TableId::EventInformationPfOther as u8;
206
207const RST_TABLE_ID: u8 = dvb_si::table_id::TableId::RunningStatus as u8;
209
210const STUFFING_TABLE_ID: u8 = dvb_si::table_id::TableId::Stuffing as u8;
213
214const TOT_TABLE_ID: u8 = dvb_si::table_id::TableId::TimeOffset as u8;
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222#[cfg_attr(feature = "serde", derive(serde::Serialize))]
223#[non_exhaustive]
224pub enum Priority {
225 First,
227 Second,
229 Third,
231}
232
233impl Priority {
234 #[must_use]
236 pub fn name(&self) -> &'static str {
237 match self {
238 Self::First => "first priority",
239 Self::Second => "second priority",
240 Self::Third => "third priority",
241 }
242 }
243}
244broadcast_common::impl_spec_display!(Priority);
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
250#[cfg_attr(feature = "serde", derive(serde::Serialize))]
251#[non_exhaustive]
252pub enum Indicator {
253 TsSyncLoss,
257 SyncByteError,
259 PatError2,
261 ContinuityCountError,
263 PmtError2,
265 PidError,
267
268 TransportError,
271 CrcError,
273 PcrRepetitionError,
275 PcrDiscontinuityError,
278 PtsError,
280 CatError,
282
283 NitError,
288 SiRepetitionError,
291 UnreferencedPid,
295 SdtError,
299 EitError,
303 RstError,
306 TdtError,
309}
310
311impl Indicator {
312 #[must_use]
314 pub fn priority(self) -> Priority {
315 match self {
316 Self::TsSyncLoss
317 | Self::SyncByteError
318 | Self::PatError2
319 | Self::ContinuityCountError
320 | Self::PmtError2
321 | Self::PidError => Priority::First,
322 Self::TransportError
323 | Self::CrcError
324 | Self::PcrRepetitionError
325 | Self::PcrDiscontinuityError
326 | Self::PtsError
327 | Self::CatError => Priority::Second,
328 Self::NitError
329 | Self::SiRepetitionError
330 | Self::UnreferencedPid
331 | Self::SdtError
332 | Self::EitError
333 | Self::RstError
334 | Self::TdtError => Priority::Third,
335 }
336 }
337
338 #[must_use]
340 pub fn name(self) -> &'static str {
341 match self {
342 Self::TsSyncLoss => "TS_sync_loss",
343 Self::SyncByteError => "Sync_byte_error",
344 Self::PatError2 => "PAT_error_2",
345 Self::ContinuityCountError => "Continuity_count_error",
346 Self::PmtError2 => "PMT_error_2",
347 Self::PidError => "PID_error",
348 Self::TransportError => "Transport_error",
349 Self::CrcError => "CRC_error",
350 Self::PcrRepetitionError => "PCR_repetition_error",
351 Self::PcrDiscontinuityError => "PCR_discontinuity_indicator_error",
352 Self::PtsError => "PTS_error",
353 Self::CatError => "CAT_error",
354 Self::NitError => "NIT_error",
355 Self::SiRepetitionError => "SI_repetition_error",
356 Self::UnreferencedPid => "Unreferenced_PID",
357 Self::SdtError => "SDT_error",
358 Self::EitError => "EIT_error",
359 Self::RstError => "RST_error",
360 Self::TdtError => "TDT_error",
361 }
362 }
363
364 #[must_use]
366 pub fn clause(self) -> &'static str {
367 match self {
368 Self::TsSyncLoss => "TR 101 290 v1.4.1 Table 5.0a indicator 1.1",
369 Self::SyncByteError => "TR 101 290 v1.4.1 Table 5.0a indicator 1.2",
370 Self::PatError2 => "TR 101 290 v1.4.1 Table 5.0a indicator 1.3.a",
371 Self::ContinuityCountError => "TR 101 290 v1.4.1 Table 5.0a indicator 1.4",
372 Self::PmtError2 => "TR 101 290 v1.4.1 Table 5.0a indicator 1.5.a",
373 Self::PidError => "TR 101 290 v1.4.1 Table 5.0a indicator 1.6",
374 Self::TransportError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.1",
375 Self::CrcError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.2",
376 Self::PcrRepetitionError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.3a",
377 Self::PcrDiscontinuityError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.3b",
378 Self::PtsError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.5",
379 Self::CatError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.6",
380 Self::NitError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.1",
381 Self::SiRepetitionError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.2",
382 Self::UnreferencedPid => "TR 101 290 v1.4.1 Table 5.0c indicator 3.4",
383 Self::SdtError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.5",
384 Self::EitError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.6",
385 Self::RstError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.7",
386 Self::TdtError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.8",
387 }
388 }
389}
390broadcast_common::impl_spec_display!(Indicator);
391
392#[derive(Debug, Clone, PartialEq, Eq)]
394#[cfg_attr(feature = "serde", derive(serde::Serialize))]
395#[non_exhaustive]
396pub struct ConformanceEvent {
397 pub indicator: Indicator,
399 pub priority: Priority,
401 pub pid: Option<u16>,
403 pub at: Duration,
405 pub detail: String,
407}
408
409#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
411#[cfg_attr(feature = "serde", derive(serde::Serialize))]
412#[non_exhaustive]
413pub struct Stats {
414 pub packets: u64,
416 pub events: u64,
418 pub in_sync: bool,
420}
421
422#[derive(Debug, Clone)]
424#[non_exhaustive]
425pub struct Config {
426 pub pat_max_interval: Duration,
429 pub pmt_max_interval: Duration,
432 pub pid_error_period: Duration,
435 pub sync_acquire_packets: u8,
438 pub sync_loss_packets: u8,
441 pub pcr_repetition_limit: Duration,
444 pub pcr_discontinuity_limit: Duration,
447 pub pts_repetition_limit: Duration,
450 pub si_nit_interval: Duration,
453 pub si_sdt_interval: Duration,
456 pub si_eit_pf_interval: Duration,
459 pub si_tdt_interval: Duration,
462 pub unreferenced_pid_period: Duration,
465}
466
467impl Default for Config {
468 fn default() -> Self {
469 Self {
470 pat_max_interval: Duration::from_millis(DEFAULT_PAT_MAX_INTERVAL_MS),
471 pmt_max_interval: Duration::from_millis(DEFAULT_PMT_MAX_INTERVAL_MS),
472 pid_error_period: Duration::from_secs(DEFAULT_PID_ERROR_PERIOD_SECS),
473 sync_acquire_packets: DEFAULT_SYNC_ACQUIRE_PACKETS,
474 sync_loss_packets: DEFAULT_SYNC_LOSS_PACKETS,
475 pcr_repetition_limit: Duration::from_millis(DEFAULT_PCR_REPETITION_LIMIT_MS),
476 pcr_discontinuity_limit: Duration::from_millis(DEFAULT_PCR_DISCONTINUITY_LIMIT_MS),
477 pts_repetition_limit: Duration::from_millis(DEFAULT_PTS_REPETITION_LIMIT_MS),
478 si_nit_interval: Duration::from_secs(DEFAULT_SI_NIT_INTERVAL_SECS),
479 si_sdt_interval: Duration::from_secs(DEFAULT_SI_SDT_INTERVAL_SECS),
480 si_eit_pf_interval: Duration::from_secs(DEFAULT_SI_EIT_PF_INTERVAL_SECS),
481 si_tdt_interval: Duration::from_secs(DEFAULT_SI_TDT_INTERVAL_SECS),
482 unreferenced_pid_period: Duration::from_millis(DEFAULT_UNREFERENCED_PID_PERIOD_MS),
483 }
484 }
485}
486
487struct CcState {
491 last_cc: u8,
492 had_payload: bool,
493 dup_used: bool,
494 initialised: bool,
495}
496
497struct PresenceTimer {
499 last_seen: Duration,
500 reported: bool,
501}
502
503struct PmtTracking {
505 timer: PresenceTimer,
506 reassembler: SectionReassembler,
507}
508
509struct EsTracking {
511 timer: PresenceTimer,
512}
513
514struct PcrState {
516 last_pcr_27mhz: u64,
517 last_pcr_time: Duration,
518 initialised: bool,
519}
520
521struct PtsState {
523 last_pts_time: Duration,
524 armed: bool,
525}
526
527struct SiReassembly {
529 reassembler: SectionReassembler,
530}
531
532struct SiRepetitionTimer {
536 last_seen: Duration,
537 reported: bool,
538 armed: bool,
539}
540
541struct UnreferencedPidTracking {
544 first_seen: Duration,
545 reported: bool,
546}
547
548pub struct ConformanceMonitor {
556 config: Config,
557 events: Vec<ConformanceEvent>,
558 stats: Stats,
559
560 in_sync: bool,
562 good_run: u8,
563 bad_run: u8,
564
565 cc_states: BTreeMap<u16, CcState>,
567
568 pat_reassembler: SectionReassembler,
570 pat_timer: PresenceTimer,
571
572 pmt_trackings: BTreeMap<u16, PmtTracking>,
574
575 es_trackings: BTreeMap<u16, EsTracking>,
577
578 si_reassemblies: BTreeMap<u16, SiReassembly>,
580
581 pcr_states: BTreeMap<u16, PcrState>,
583
584 pts_states: BTreeMap<u16, PtsState>,
586
587 cat_seen: bool,
589 scrambled_without_cat_reported: bool,
590
591 si_timers: BTreeMap<u8, SiRepetitionTimer>,
593
594 unreferenced_pid_timers: BTreeMap<u16, UnreferencedPidTracking>,
596}
597
598impl ConformanceMonitor {
599 pub fn new() -> Self {
601 Self::with_config(Config::default())
602 }
603
604 pub fn with_config(config: Config) -> Self {
606 let mut si_reassemblies = BTreeMap::new();
607 for &pid in &SI_PIDS {
608 si_reassemblies.insert(
609 pid,
610 SiReassembly {
611 reassembler: SectionReassembler::default(),
612 },
613 );
614 }
615 Self {
616 config,
617 events: Vec::new(),
618 stats: Stats {
619 packets: 0,
620 events: 0,
621 in_sync: false,
622 },
623 in_sync: false,
624 good_run: 0,
625 bad_run: 0,
626 cc_states: BTreeMap::new(),
627 pat_reassembler: SectionReassembler::default(),
628 pat_timer: PresenceTimer {
629 last_seen: Duration::ZERO,
630 reported: false,
631 },
632 pmt_trackings: BTreeMap::new(),
633 es_trackings: BTreeMap::new(),
634 si_reassemblies,
635 pcr_states: BTreeMap::new(),
636 pts_states: BTreeMap::new(),
637 cat_seen: false,
638 scrambled_without_cat_reported: false,
639 si_timers: BTreeMap::new(),
640 unreferenced_pid_timers: BTreeMap::new(),
641 }
642 }
643
644 pub fn feed(&mut self, ts_packet: &[u8], t: Duration) -> &[ConformanceEvent] {
650 self.events.clear();
651 self.stats.packets += 1;
652
653 let sync_ok = !ts_packet.is_empty() && ts_packet[0] == SYNC_BYTE;
655 if !sync_ok {
656 self.emit(Indicator::SyncByteError, None, t, "sync_byte != 0x47");
657 }
658
659 if sync_ok {
661 self.good_run = self.good_run.saturating_add(1);
662 self.bad_run = 0;
663 if !self.in_sync && self.good_run >= self.config.sync_acquire_packets {
664 self.in_sync = true;
665 }
666 } else {
667 self.bad_run = self.bad_run.saturating_add(1);
668 self.good_run = 0;
669 if self.in_sync && self.bad_run >= self.config.sync_loss_packets {
670 self.in_sync = false;
671 self.emit(
672 Indicator::TsSyncLoss,
673 None,
674 t,
675 "sync lost after hysteresis threshold",
676 );
677 }
678 }
679
680 if !self.in_sync {
684 return &self.events;
685 }
686
687 let packet = match TsPacket::parse(ts_packet) {
689 Ok(p) => p,
690 Err(_) => return &self.events,
691 };
692 let header = &packet.header;
693 let pid = header.pid;
694
695 if header.tei {
697 self.emit(
698 Indicator::TransportError,
699 Some(pid),
700 t,
701 format!("transport_error_indicator set on PID 0x{pid:04X}"),
702 );
703 }
704
705 if pid != PID_NULL {
707 self.check_cc(
708 pid,
709 header.continuity_counter,
710 header.has_payload,
711 t,
712 ts_packet,
713 );
714 }
715
716 if pid == PID_PAT && header.scrambling != 0 {
718 self.emit(
719 Indicator::PatError2,
720 Some(PID_PAT),
721 t,
722 format!(
723 "scrambling_control_field != 00 on PID 0x0000 (got {})",
724 header.scrambling
725 ),
726 );
727 }
728
729 if self.pmt_trackings.contains_key(&pid) && header.scrambling != 0 {
731 self.emit(
732 Indicator::PmtError2,
733 Some(pid),
734 t,
735 format!("scrambling_control_field != 00 on program_map_PID 0x{pid:04X}"),
736 );
737 }
738
739 if header.scrambling != 0 && !self.cat_seen && !self.scrambled_without_cat_reported {
746 self.scrambled_without_cat_reported = true;
747 self.emit(
748 Indicator::CatError,
749 Some(pid),
750 t,
751 format!("scrambled packet on PID 0x{pid:04X} but no CAT seen on PID 0x0001"),
752 );
753 }
754
755 if pid == PID_PAT && header.has_payload {
757 if let Some(payload) = packet.payload {
758 self.pat_reassembler.feed(payload, header.pusi);
759 }
760 self.pat_timer.last_seen = t;
761 self.pat_timer.reported = false;
762 while let Some(section_bytes) = self.pat_reassembler.pop_section() {
763 self.check_crc_and_process_pat(§ion_bytes, pid, t);
764 }
765 }
766
767 if self.pmt_trackings.contains_key(&pid) && header.has_payload {
769 if let Some(payload) = packet.payload {
770 if let Some(tracking) = self.pmt_trackings.get_mut(&pid) {
771 tracking.reassembler.feed(payload, header.pusi);
772 }
773 }
774 let sections: Vec<_> = if let Some(tracking) = self.pmt_trackings.get_mut(&pid) {
775 tracking.timer.last_seen = t;
776 tracking.timer.reported = false;
777 core::iter::from_fn(|| tracking.reassembler.pop_section()).collect()
778 } else {
779 Vec::new()
780 };
781 for section_bytes in §ions {
782 self.check_crc_and_process_pmt(section_bytes, pid, t);
783 }
784 }
785
786 if pid != PID_PAT
790 && !self.pmt_trackings.contains_key(&pid)
791 && self.si_reassemblies.contains_key(&pid)
792 && header.has_payload
793 {
794 if let Some(payload) = packet.payload {
795 if let Some(si_ra) = self.si_reassemblies.get_mut(&pid) {
796 si_ra.reassembler.feed(payload, header.pusi);
797 }
798 }
799 let sections: Vec<_> = if let Some(si_ra) = self.si_reassemblies.get_mut(&pid) {
800 core::iter::from_fn(|| si_ra.reassembler.pop_section()).collect()
801 } else {
802 Vec::new()
803 };
804 for section_bytes in §ions {
805 self.check_crc_for_si(section_bytes, pid, t);
806 self.check_cat_table_id(section_bytes, pid, t);
807 self.check_nit_table_id(section_bytes, pid, t);
808 self.check_sdt_table_id(section_bytes, pid, t);
809 self.check_eit_table_id(section_bytes, pid, t);
810 self.check_rst_table_id(section_bytes, pid, t);
811 self.check_tdt_table_id(section_bytes, pid, t);
812 self.update_si_repetition(section_bytes, pid, t);
813 }
814 }
815 if let Some(tracking) = self.es_trackings.get_mut(&pid) {
823 tracking.timer.last_seen = t;
824 tracking.timer.reported = false;
825 }
826
827 if let Some(Ok(af)) = packet.adaptation_field() {
829 if let Some(pcr) = af.pcr {
830 self.check_pcr(pid, pcr.as_27mhz(), af.discontinuity_indicator, t);
831 }
832 }
833
834 if header.pusi
836 && header.scrambling == 0
837 && self.es_trackings.contains_key(&pid)
838 && header.has_payload
839 {
840 if let Some(payload) = packet.payload {
841 self.check_pts(pid, payload, t);
842 }
843 }
844
845 if pid != PID_NULL {
847 self.track_unreferenced_pid(pid, t);
848 }
849
850 self.check_presence_timeouts(t);
852
853 &self.events
854 }
855
856 pub fn stats(&self) -> Stats {
858 Stats {
859 in_sync: self.in_sync,
860 ..self.stats
861 }
862 }
863
864 fn emit(
867 &mut self,
868 indicator: Indicator,
869 pid: Option<u16>,
870 at: Duration,
871 detail: impl Into<String>,
872 ) {
873 let event = ConformanceEvent {
874 indicator,
875 priority: indicator.priority(),
876 pid,
877 at,
878 detail: detail.into(),
879 };
880 self.stats.events += 1;
881 self.events.push(event);
882 }
883
884 fn check_cc(&mut self, pid: u16, cc: u8, has_payload: bool, t: Duration, raw: &[u8]) {
886 let discontinuity = if raw.len() >= 5 {
889 let b3 = raw[3];
890 let has_adaptation = (b3 & 0x20) != 0;
891 if has_adaptation {
892 let af_len = raw[4] as usize;
893 if af_len > 0 && raw.len() > 5 {
894 (raw[5] & 0x80) != 0
895 } else {
896 false
897 }
898 } else {
899 false
900 }
901 } else {
902 false
903 };
904
905 let (expected, is_duplicate, should_emit_dup, should_emit_cc) = {
907 let state = self.cc_states.entry(pid).or_insert_with(|| CcState {
908 last_cc: cc,
909 had_payload: has_payload,
910 dup_used: false,
911 initialised: false,
912 });
913
914 if !state.initialised {
915 state.last_cc = cc;
916 state.had_payload = has_payload;
917 state.dup_used = false;
918 state.initialised = true;
919 return;
920 }
921
922 if discontinuity {
923 (0u8, false, false, false)
925 } else {
926 let is_duplicate = cc == state.last_cc && has_payload;
927 let mut should_emit_dup = false;
928 let mut should_emit_cc = false;
929
930 if is_duplicate {
931 if state.dup_used {
932 should_emit_dup = true;
933 }
934 } else {
935 state.dup_used = false;
936 let expected = if has_payload {
937 (state.last_cc.wrapping_add(1)) & 0x0F
938 } else {
939 state.last_cc
940 };
941 if cc != expected {
942 should_emit_cc = true;
943 }
944 }
945
946 (
947 if has_payload {
948 (state.last_cc.wrapping_add(1)) & 0x0F
949 } else {
950 state.last_cc
951 },
952 is_duplicate,
953 should_emit_dup,
954 should_emit_cc,
955 )
956 }
957 };
958
959 if should_emit_dup {
961 self.emit(
962 Indicator::ContinuityCountError,
963 Some(pid),
964 t,
965 format!("second consecutive duplicate on PID 0x{pid:04X} (cc={cc})"),
966 );
967 }
968 if should_emit_cc {
969 self.emit(
970 Indicator::ContinuityCountError,
971 Some(pid),
972 t,
973 format!("expected cc={expected}, got {cc} on PID 0x{pid:04X}"),
974 );
975 }
976
977 let state = self.cc_states.get_mut(&pid).unwrap();
979 if discontinuity {
980 state.last_cc = cc;
981 state.had_payload = has_payload;
982 state.dup_used = false;
983 } else if is_duplicate {
984 state.dup_used = true;
986 } else {
987 state.dup_used = false;
988 state.last_cc = cc;
989 state.had_payload = has_payload;
990 }
991 }
992
993 fn check_crc_and_process_pat(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
995 self.check_crc_for_section(section_bytes, pid, t);
997
998 self.process_pat_section(section_bytes, t);
999 }
1000
1001 fn check_crc_and_process_pmt(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1003 self.check_crc_for_section(section_bytes, pid, t);
1005
1006 self.process_pmt_section(section_bytes, pid, t);
1007 }
1008
1009 fn check_crc_for_section(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1014 let section = match Section::parse(section_bytes) {
1015 Ok(s) => s,
1016 Err(_) => return,
1017 };
1018
1019 if let Err(mpeg_ts::error::Error::CrcMismatch { .. }) = section.validate_crc(section_bytes)
1021 {
1022 self.emit(
1023 Indicator::CrcError,
1024 Some(pid),
1025 t,
1026 format!(
1027 "CRC-32 mismatch on PID 0x{:04X} (table_id 0x{:02X})",
1028 pid, section.table_id
1029 ),
1030 );
1031 }
1032 }
1033
1034 fn check_crc_for_si(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1036 self.check_crc_for_section(section_bytes, pid, t);
1037 }
1038
1039 fn check_cat_table_id(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1042 if pid != PID_CAT {
1043 return;
1044 }
1045 let section = match Section::parse(section_bytes) {
1046 Ok(s) => s,
1047 Err(_) => return,
1048 };
1049 if section.table_id == CAT_TABLE_ID {
1050 self.cat_seen = true;
1052 self.scrambled_without_cat_reported = false;
1055 } else {
1056 self.emit(
1057 Indicator::CatError,
1058 Some(PID_CAT),
1059 t,
1060 format!(
1061 "section with table_id 0x{:02X} on PID 0x0001 (expected 0x01 for CAT)",
1062 section.table_id
1063 ),
1064 );
1065 }
1066 }
1067
1068 fn check_nit_table_id(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1075 if pid != PID_NIT {
1076 return;
1077 }
1078 let section = match Section::parse(section_bytes) {
1079 Ok(s) => s,
1080 Err(_) => return,
1081 };
1082 let allowed = section.table_id == NIT_ACTUAL_TABLE_ID
1083 || section.table_id == NIT_OTHER_TABLE_ID
1084 || section.table_id == STUFFING_TABLE_ID;
1085 if !allowed {
1086 self.emit(
1087 Indicator::NitError,
1088 Some(PID_NIT),
1089 t,
1090 format!(
1091 "section with table_id 0x{:02X} on PID 0x0010 (expected NIT_actual/NIT_other/ST)",
1092 section.table_id
1093 ),
1094 );
1095 }
1096 }
1097
1098 fn check_sdt_table_id(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1104 if pid != PID_SDT_BAT {
1105 return;
1106 }
1107 let section = match Section::parse(section_bytes) {
1108 Ok(s) => s,
1109 Err(_) => return,
1110 };
1111 let allowed = section.table_id == SDT_ACTUAL_TABLE_ID
1112 || section.table_id == SDT_OTHER_TABLE_ID
1113 || section.table_id == BAT_TABLE_ID
1114 || section.table_id == STUFFING_TABLE_ID;
1115 if !allowed {
1116 self.emit(
1117 Indicator::SdtError,
1118 Some(PID_SDT_BAT),
1119 t,
1120 format!(
1121 "section with table_id 0x{:02X} on PID 0x0011 (expected SDT_actual/SDT_other/BAT/ST)",
1122 section.table_id
1123 ),
1124 );
1125 }
1126 }
1127
1128 fn check_eit_table_id(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1135 if pid != PID_EIT {
1136 return;
1137 }
1138 let section = match Section::parse(section_bytes) {
1139 Ok(s) => s,
1140 Err(_) => return,
1141 };
1142 let table_id = section.table_id;
1143 let allowed = table_id == EIT_PF_ACTUAL_TABLE_ID
1144 || table_id == EIT_PF_OTHER_TABLE_ID
1145 || (dvb_si::tables::eit::TABLE_ID_SCHEDULE_ACTUAL_FIRST
1146 ..=dvb_si::tables::eit::TABLE_ID_SCHEDULE_ACTUAL_LAST)
1147 .contains(&table_id)
1148 || (dvb_si::tables::eit::TABLE_ID_SCHEDULE_OTHER_FIRST
1149 ..=dvb_si::tables::eit::TABLE_ID_SCHEDULE_OTHER_LAST)
1150 .contains(&table_id)
1151 || table_id == STUFFING_TABLE_ID;
1152 if !allowed {
1153 self.emit(
1154 Indicator::EitError,
1155 Some(PID_EIT),
1156 t,
1157 format!(
1158 "section with table_id 0x{table_id:02X} on PID 0x0012 (expected EIT P/F or schedule range or ST)"
1159 ),
1160 );
1161 }
1162 }
1163
1164 fn check_rst_table_id(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1169 if pid != PID_RST {
1170 return;
1171 }
1172 let section = match Section::parse(section_bytes) {
1173 Ok(s) => s,
1174 Err(_) => return,
1175 };
1176 let allowed = section.table_id == RST_TABLE_ID || section.table_id == STUFFING_TABLE_ID;
1177 if !allowed {
1178 self.emit(
1179 Indicator::RstError,
1180 Some(PID_RST),
1181 t,
1182 format!(
1183 "section with table_id 0x{:02X} on PID 0x0013 (expected RST/ST)",
1184 section.table_id
1185 ),
1186 );
1187 }
1188 }
1189
1190 fn check_tdt_table_id(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
1196 if pid != PID_TDT_TOT {
1197 return;
1198 }
1199 let section = match Section::parse(section_bytes) {
1200 Ok(s) => s,
1201 Err(_) => return,
1202 };
1203 let allowed = section.table_id == TDT_TABLE_ID
1204 || section.table_id == TOT_TABLE_ID
1205 || section.table_id == STUFFING_TABLE_ID;
1206 if !allowed {
1207 self.emit(
1208 Indicator::TdtError,
1209 Some(PID_TDT_TOT),
1210 t,
1211 format!(
1212 "section with table_id 0x{:02X} on PID 0x0014 (expected TDT/TOT/ST)",
1213 section.table_id
1214 ),
1215 );
1216 }
1217 }
1218
1219 fn is_referenced_or_reserved_pid(&self, pid: u16) -> bool {
1230 pid == PID_PAT
1231 || pid == PID_CAT
1232 || pid == PID_NIT
1233 || pid == PID_SDT_BAT
1234 || pid == PID_EIT
1235 || pid == PID_RST
1236 || pid == PID_TDT_TOT
1237 || pid == PID_NULL
1238 || (RESERVED_PID_MIN..=RESERVED_PID_MAX).contains(&pid)
1239 || self.pmt_trackings.contains_key(&pid)
1240 || self.es_trackings.contains_key(&pid)
1241 }
1242
1243 fn track_unreferenced_pid(&mut self, pid: u16, t: Duration) {
1251 if self.is_referenced_or_reserved_pid(pid) {
1252 self.unreferenced_pid_timers.remove(&pid);
1253 return;
1254 }
1255 self.unreferenced_pid_timers
1256 .entry(pid)
1257 .or_insert_with(|| UnreferencedPidTracking {
1258 first_seen: t,
1259 reported: false,
1260 });
1261 }
1262
1263 fn process_pat_section(&mut self, section_bytes: &[u8], t: Duration) {
1265 let section = match Section::parse(section_bytes) {
1266 Ok(s) => s,
1267 Err(_) => return,
1268 };
1269
1270 if section.table_id != PAT_TABLE_ID {
1272 self.emit(
1273 Indicator::PatError2,
1274 Some(PID_PAT),
1275 t,
1276 format!(
1277 "section with table_id 0x{:02X} on PID 0x0000 (expected 0x00)",
1278 section.table_id
1279 ),
1280 );
1281 return;
1282 }
1283
1284 let pat = match PatSection::parse(section_bytes) {
1286 Ok(p) => p,
1287 Err(_) => return,
1288 };
1289
1290 for entry in pat.programmes() {
1292 let pmt_pid = entry.pid;
1293 self.pmt_trackings
1294 .entry(pmt_pid)
1295 .or_insert_with(|| PmtTracking {
1296 timer: PresenceTimer {
1297 last_seen: t,
1298 reported: false,
1299 },
1300 reassembler: SectionReassembler::default(),
1301 });
1302 self.unreferenced_pid_timers.remove(&pmt_pid);
1306 }
1307 }
1308
1309 fn process_pmt_section(&mut self, section_bytes: &[u8], _pid: u16, t: Duration) {
1311 let section = match Section::parse(section_bytes) {
1312 Ok(s) => s,
1313 Err(_) => return,
1314 };
1315
1316 let pmt_table_id: u8 = dvb_si::tables::pmt::TABLE_ID;
1321 if section.table_id != pmt_table_id {
1322 return;
1323 }
1324
1325 let pmt = match PmtSection::parse(section_bytes) {
1327 Ok(p) => p,
1328 Err(_) => return,
1329 };
1330
1331 let mut new_es_pids: Vec<u16> = Vec::new();
1333 if pmt.pcr_pid != PID_NULL && !self.es_trackings.contains_key(&pmt.pcr_pid) {
1334 new_es_pids.push(pmt.pcr_pid);
1335 }
1336 for stream in &pmt.streams {
1337 let es_pid = stream.elementary_pid;
1338 if !self.es_trackings.contains_key(&es_pid) {
1339 new_es_pids.push(es_pid);
1340 }
1341 }
1342
1343 for es_pid in new_es_pids {
1344 self.es_trackings.insert(
1345 es_pid,
1346 EsTracking {
1347 timer: PresenceTimer {
1348 last_seen: t,
1349 reported: false,
1350 },
1351 },
1352 );
1353 self.unreferenced_pid_timers.remove(&es_pid);
1356 }
1357 }
1358
1359 fn check_pcr(&mut self, pid: u16, pcr_27mhz: u64, discontinuity: bool, t: Duration) {
1361 let state = self.pcr_states.entry(pid).or_insert_with(|| PcrState {
1362 last_pcr_27mhz: 0,
1363 last_pcr_time: Duration::ZERO,
1364 initialised: false,
1365 });
1366
1367 if !state.initialised {
1368 state.last_pcr_27mhz = pcr_27mhz;
1369 state.last_pcr_time = t;
1370 state.initialised = true;
1371 return;
1372 }
1373
1374 let last_pcr_time = state.last_pcr_time;
1376 let last_pcr_27mhz = state.last_pcr_27mhz;
1377
1378 let rep_interval = t.saturating_sub(last_pcr_time);
1381 let should_emit_rep = rep_interval > self.config.pcr_repetition_limit;
1382
1383 let delta =
1386 (pcr_27mhz.wrapping_add(PCR_MODULUS_27MHZ) - last_pcr_27mhz) % PCR_MODULUS_27MHZ;
1387 let delta_ms = delta * 1000 / CLOCK_27MHZ;
1388 let limit_ms = self.config.pcr_discontinuity_limit.as_millis() as u64;
1389 let should_emit_disc = delta_ms > limit_ms && !discontinuity;
1390
1391 if should_emit_rep {
1393 self.emit(
1394 Indicator::PcrRepetitionError,
1395 Some(pid),
1396 t,
1397 format!(
1398 "PCR interval {} ms exceeds limit {} ms on PID 0x{:04X}",
1399 rep_interval.as_millis(),
1400 self.config.pcr_repetition_limit.as_millis(),
1401 pid
1402 ),
1403 );
1404 }
1405 if should_emit_disc {
1406 self.emit(
1407 Indicator::PcrDiscontinuityError,
1408 Some(pid),
1409 t,
1410 format!(
1411 "PCR delta {delta_ms} ms exceeds limit {limit_ms} ms on PID 0x{pid:04X} without discontinuity_indicator"
1412 ),
1413 );
1414 }
1415
1416 let state = self.pcr_states.get_mut(&pid).unwrap();
1418 state.last_pcr_27mhz = pcr_27mhz;
1419 state.last_pcr_time = t;
1420 }
1421
1422 fn check_pts(&mut self, pid: u16, payload: &[u8], t: Duration) {
1427 if payload.len() < PES_FLAGS_OFFSET + 2 {
1429 return;
1430 }
1431 if payload[0] != PES_PREFIX_0 || payload[1] != PES_PREFIX_1 || payload[2] != PES_PREFIX_2 {
1432 return;
1433 }
1434
1435 let flags_byte = payload[PES_FLAGS_OFFSET];
1437 if (flags_byte >> 6) != 0b10 {
1438 return;
1439 }
1440
1441 let pts_dts_flags = payload[PES_FLAGS_OFFSET + 1] & PES_PTS_DTS_FLAGS_MASK;
1443 let pts_present = (pts_dts_flags & PES_PTS_PRESENT) != 0;
1444 if !pts_present {
1445 return;
1446 }
1447
1448 let state = self.pts_states.entry(pid).or_insert_with(|| PtsState {
1449 last_pts_time: Duration::ZERO,
1450 armed: false,
1451 });
1452
1453 if !state.armed {
1454 state.last_pts_time = t;
1456 state.armed = true;
1457 return;
1458 }
1459
1460 let last_pts_time = state.last_pts_time;
1462 let pts_interval = t.saturating_sub(last_pts_time);
1463 let should_emit = pts_interval > self.config.pts_repetition_limit;
1464
1465 if should_emit {
1466 self.emit(
1467 Indicator::PtsError,
1468 Some(pid),
1469 t,
1470 format!(
1471 "PTS interval {} ms exceeds limit {} ms on PID 0x{:04X}",
1472 pts_interval.as_millis(),
1473 self.config.pts_repetition_limit.as_millis(),
1474 pid
1475 ),
1476 );
1477 }
1478
1479 let state = self.pts_states.get_mut(&pid).unwrap();
1481 state.last_pts_time = t;
1482 }
1483
1484 fn update_si_repetition(&mut self, section_bytes: &[u8], _pid: u16, t: Duration) {
1488 let table_id = match Section::parse(section_bytes) {
1489 Ok(s) => s.table_id,
1490 Err(_) => return,
1491 };
1492
1493 let is_tracked = table_id == NIT_ACTUAL_TABLE_ID
1494 || table_id == SDT_ACTUAL_TABLE_ID
1495 || table_id == EIT_PF_ACTUAL_TABLE_ID
1496 || table_id == TDT_TABLE_ID;
1497
1498 if !is_tracked {
1499 return;
1500 }
1501
1502 let timer = self
1503 .si_timers
1504 .entry(table_id)
1505 .or_insert_with(|| SiRepetitionTimer {
1506 last_seen: Duration::ZERO,
1507 reported: false,
1508 armed: false,
1509 });
1510
1511 timer.last_seen = t;
1512 timer.reported = false;
1513 timer.armed = true;
1514 }
1515
1516 fn check_presence_timeouts(&mut self, t: Duration) {
1518 if t.saturating_sub(self.pat_timer.last_seen) > self.config.pat_max_interval
1520 && !self.pat_timer.reported
1521 {
1522 self.pat_timer.reported = true;
1523 self.emit(
1524 Indicator::PatError2,
1525 Some(PID_PAT),
1526 t,
1527 format!(
1528 "no PAT section within {} ms",
1529 self.config.pat_max_interval.as_millis()
1530 ),
1531 );
1532 }
1533
1534 let pmt_timeouts: Vec<(u16, u64)> = self
1537 .pmt_trackings
1538 .iter()
1539 .filter_map(|(&pid, tracking)| {
1540 if t.saturating_sub(tracking.timer.last_seen) > self.config.pmt_max_interval
1541 && !tracking.timer.reported
1542 {
1543 Some((pid, self.config.pmt_max_interval.as_millis() as u64))
1544 } else {
1545 None
1546 }
1547 })
1548 .collect();
1549 for (pid, interval_ms) in pmt_timeouts {
1550 if let Some(tracking) = self.pmt_trackings.get_mut(&pid) {
1551 tracking.timer.reported = true;
1552 }
1553 self.emit(
1554 Indicator::PmtError2,
1555 Some(pid),
1556 t,
1557 format!("no PMT section on program_map_PID 0x{pid:04X} within {interval_ms} ms"),
1558 );
1559 }
1560
1561 let pid_timeouts: Vec<(u16, u64)> = self
1563 .es_trackings
1564 .iter()
1565 .filter_map(|(&pid, tracking)| {
1566 if t.saturating_sub(tracking.timer.last_seen) > self.config.pid_error_period
1567 && !tracking.timer.reported
1568 {
1569 Some((pid, self.config.pid_error_period.as_secs()))
1570 } else {
1571 None
1572 }
1573 })
1574 .collect();
1575 for (pid, period_secs) in pid_timeouts {
1576 if let Some(tracking) = self.es_trackings.get_mut(&pid) {
1577 tracking.timer.reported = true;
1578 }
1579 self.emit(
1580 Indicator::PidError,
1581 Some(pid),
1582 t,
1583 format!("referenced PID 0x{pid:04X} absent for > {period_secs} s"),
1584 );
1585 }
1586
1587 let si_timeouts: Vec<(u8, u64, u16, u64)> = self
1590 .si_timers
1591 .iter()
1592 .filter_map(|(&table_id, timer)| {
1593 if !timer.armed || timer.reported {
1594 return None;
1595 }
1596 let (limit, pid) = match table_id {
1597 NIT_ACTUAL_TABLE_ID => (self.config.si_nit_interval, PID_NIT),
1598 SDT_ACTUAL_TABLE_ID => (self.config.si_sdt_interval, PID_SDT_BAT),
1599 EIT_PF_ACTUAL_TABLE_ID => (self.config.si_eit_pf_interval, PID_EIT),
1600 TDT_TABLE_ID => (self.config.si_tdt_interval, PID_TDT_TOT),
1601 _ => return None,
1602 };
1603 let interval = t.saturating_sub(timer.last_seen);
1604 if interval > limit {
1605 Some((
1606 table_id,
1607 interval.as_millis() as u64,
1608 pid,
1609 limit.as_millis() as u64,
1610 ))
1611 } else {
1612 None
1613 }
1614 })
1615 .collect();
1616 for (table_id, interval_ms, pid, limit_ms) in si_timeouts {
1617 if let Some(timer) = self.si_timers.get_mut(&table_id) {
1618 timer.reported = true;
1619 }
1620 let (table_name, group_indicator) = match table_id {
1630 NIT_ACTUAL_TABLE_ID => ("NIT_actual", Some(Indicator::NitError)),
1631 SDT_ACTUAL_TABLE_ID => ("SDT_actual", Some(Indicator::SdtError)),
1632 EIT_PF_ACTUAL_TABLE_ID => ("EIT_P/F_actual", Some(Indicator::EitError)),
1633 TDT_TABLE_ID => ("TDT", Some(Indicator::TdtError)),
1634 _ => ("unknown", None),
1635 };
1636 self.emit(
1637 Indicator::SiRepetitionError,
1638 Some(pid),
1639 t,
1640 format!("{table_name} repetition interval {interval_ms} ms exceeds {limit_ms} ms"),
1641 );
1642 if let Some(indicator) = group_indicator {
1643 self.emit(
1644 indicator,
1645 Some(pid),
1646 t,
1647 format!("no {table_name} section on PID 0x{pid:04X} within {limit_ms} ms"),
1648 );
1649 }
1650 }
1651
1652 let unref_timeouts: Vec<(u16, u64)> = self
1656 .unreferenced_pid_timers
1657 .iter()
1658 .filter_map(|(&pid, timer)| {
1659 if timer.reported {
1660 return None;
1661 }
1662 let elapsed = t.saturating_sub(timer.first_seen);
1663 if elapsed > self.config.unreferenced_pid_period {
1664 Some((pid, self.config.unreferenced_pid_period.as_millis() as u64))
1665 } else {
1666 None
1667 }
1668 })
1669 .collect();
1670 for (pid, period_ms) in unref_timeouts {
1671 if let Some(timer) = self.unreferenced_pid_timers.get_mut(&pid) {
1672 timer.reported = true;
1673 }
1674 self.emit(
1675 Indicator::UnreferencedPid,
1676 Some(pid),
1677 t,
1678 format!(
1679 "PID 0x{pid:04X} present for > {period_ms} ms without being referenced by PAT/CAT/a PMT or a well-known SI PID"
1680 ),
1681 );
1682 }
1683 }
1684}
1685
1686impl Default for ConformanceMonitor {
1687 fn default() -> Self {
1688 Self::new()
1689 }
1690}
1691
1692#[cfg(test)]
1693mod tests;