1use core::time::Duration;
29use std::collections::HashMap;
30
31use dvb_common::Parse;
32use dvb_si::section::Section;
33use dvb_si::tables::pat::{PatSection, TABLE_ID as PAT_TABLE_ID};
34use dvb_si::tables::pmt::PmtSection;
35use dvb_si::ts::{SectionReassembler, TsPacket};
36
37const PID_PAT: u16 = 0x0000;
41const PID_CAT: u16 = 0x0001;
43const PID_NIT: u16 = 0x0010;
45const PID_SDT_BAT: u16 = 0x0011;
47const PID_EIT: u16 = 0x0012;
49const PID_TDT_TOT: u16 = 0x0014;
51const PID_NULL: u16 = 0x1FFF;
53
54const SYNC_BYTE: u8 = 0x47;
56
57const SI_PIDS: [u16; 6] = [PID_PAT, PID_CAT, PID_NIT, PID_SDT_BAT, PID_EIT, PID_TDT_TOT];
59
60const DEFAULT_PAT_MAX_INTERVAL_MS: u64 = 500;
65
66const DEFAULT_PMT_MAX_INTERVAL_MS: u64 = 500;
68
69const DEFAULT_PID_ERROR_PERIOD_SECS: u64 = 5;
71
72const DEFAULT_SYNC_ACQUIRE_PACKETS: u8 = 5;
75
76const DEFAULT_SYNC_LOSS_PACKETS: u8 = 2;
79
80const DEFAULT_PCR_REPETITION_LIMIT_MS: u64 = 100;
83
84const DEFAULT_PCR_DISCONTINUITY_LIMIT_MS: u64 = 100;
87
88const DEFAULT_PTS_REPETITION_LIMIT_MS: u64 = 700;
91
92const DEFAULT_SI_NIT_INTERVAL_SECS: u64 = 10;
95
96const DEFAULT_SI_SDT_INTERVAL_SECS: u64 = 2;
99
100const DEFAULT_SI_EIT_PF_INTERVAL_SECS: u64 = 2;
103
104const DEFAULT_SI_TDT_INTERVAL_SECS: u64 = 30;
107
108const PCR_MODULUS_27MHZ: u64 = (1u64 << 33) * 300;
113
114const CLOCK_27MHZ: u64 = 27_000_000;
116
117const PES_PREFIX_0: u8 = 0x00;
119const PES_PREFIX_1: u8 = 0x00;
121const PES_PREFIX_2: u8 = 0x01;
123
124const PES_FLAGS_OFFSET: usize = 6;
127
128const PES_PTS_DTS_FLAGS_MASK: u8 = 0b1100_0000;
131
132const PES_PTS_PRESENT: u8 = 0b1000_0000;
134
135const CAT_TABLE_ID: u8 = dvb_si::table_id::TableId::Cat as u8;
137
138const NIT_ACTUAL_TABLE_ID: u8 = dvb_si::table_id::TableId::NetworkInformationActual as u8;
140
141const SDT_ACTUAL_TABLE_ID: u8 = dvb_si::table_id::TableId::ServiceDescriptionActual as u8;
143
144const EIT_PF_ACTUAL_TABLE_ID: u8 = dvb_si::table_id::TableId::EventInformationPfActual as u8;
146
147const TDT_TABLE_ID: u8 = dvb_si::table_id::TableId::TimeAndDate as u8;
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154#[cfg_attr(feature = "serde", derive(serde::Serialize))]
155#[non_exhaustive]
156pub enum Priority {
157 First,
159 Second,
161 Third,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
169#[cfg_attr(feature = "serde", derive(serde::Serialize))]
170#[non_exhaustive]
171pub enum Indicator {
172 TsSyncLoss,
176 SyncByteError,
178 PatError2,
180 ContinuityCountError,
182 PmtError2,
184 PidError,
186
187 TransportError,
190 CrcError,
192 PcrRepetitionError,
194 PcrDiscontinuityError,
197 PtsError,
199 CatError,
201
202 SiRepetitionError,
206}
207
208impl Indicator {
209 #[must_use]
211 pub fn priority(self) -> Priority {
212 match self {
213 Self::TsSyncLoss
214 | Self::SyncByteError
215 | Self::PatError2
216 | Self::ContinuityCountError
217 | Self::PmtError2
218 | Self::PidError => Priority::First,
219 Self::TransportError
220 | Self::CrcError
221 | Self::PcrRepetitionError
222 | Self::PcrDiscontinuityError
223 | Self::PtsError
224 | Self::CatError => Priority::Second,
225 Self::SiRepetitionError => Priority::Third,
226 }
227 }
228
229 #[must_use]
231 pub fn name(self) -> &'static str {
232 match self {
233 Self::TsSyncLoss => "TS_sync_loss",
234 Self::SyncByteError => "Sync_byte_error",
235 Self::PatError2 => "PAT_error_2",
236 Self::ContinuityCountError => "Continuity_count_error",
237 Self::PmtError2 => "PMT_error_2",
238 Self::PidError => "PID_error",
239 Self::TransportError => "Transport_error",
240 Self::CrcError => "CRC_error",
241 Self::PcrRepetitionError => "PCR_repetition_error",
242 Self::PcrDiscontinuityError => "PCR_discontinuity_indicator_error",
243 Self::PtsError => "PTS_error",
244 Self::CatError => "CAT_error",
245 Self::SiRepetitionError => "SI_repetition_error",
246 }
247 }
248
249 #[must_use]
251 pub fn clause(self) -> &'static str {
252 match self {
253 Self::TsSyncLoss => "TR 101 290 v1.4.1 Table 5.0a indicator 1.1",
254 Self::SyncByteError => "TR 101 290 v1.4.1 Table 5.0a indicator 1.2",
255 Self::PatError2 => "TR 101 290 v1.4.1 Table 5.0a indicator 1.3.a",
256 Self::ContinuityCountError => "TR 101 290 v1.4.1 Table 5.0a indicator 1.4",
257 Self::PmtError2 => "TR 101 290 v1.4.1 Table 5.0a indicator 1.5.a",
258 Self::PidError => "TR 101 290 v1.4.1 Table 5.0a indicator 1.6",
259 Self::TransportError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.1",
260 Self::CrcError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.2",
261 Self::PcrRepetitionError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.3a",
262 Self::PcrDiscontinuityError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.3b",
263 Self::PtsError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.5",
264 Self::CatError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.6",
265 Self::SiRepetitionError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.2",
266 }
267 }
268}
269
270#[derive(Debug, Clone, PartialEq, Eq)]
272#[cfg_attr(feature = "serde", derive(serde::Serialize))]
273#[non_exhaustive]
274pub struct ConformanceEvent {
275 pub indicator: Indicator,
277 pub priority: Priority,
279 pub pid: Option<u16>,
281 pub at: Duration,
283 pub detail: String,
285}
286
287#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
289#[cfg_attr(feature = "serde", derive(serde::Serialize))]
290#[non_exhaustive]
291pub struct Stats {
292 pub packets: u64,
294 pub events: u64,
296 pub in_sync: bool,
298}
299
300#[derive(Debug, Clone)]
302#[non_exhaustive]
303pub struct Config {
304 pub pat_max_interval: Duration,
307 pub pmt_max_interval: Duration,
310 pub pid_error_period: Duration,
313 pub sync_acquire_packets: u8,
316 pub sync_loss_packets: u8,
319 pub pcr_repetition_limit: Duration,
322 pub pcr_discontinuity_limit: Duration,
325 pub pts_repetition_limit: Duration,
328 pub si_nit_interval: Duration,
331 pub si_sdt_interval: Duration,
334 pub si_eit_pf_interval: Duration,
337 pub si_tdt_interval: Duration,
340}
341
342impl Default for Config {
343 fn default() -> Self {
344 Self {
345 pat_max_interval: Duration::from_millis(DEFAULT_PAT_MAX_INTERVAL_MS),
346 pmt_max_interval: Duration::from_millis(DEFAULT_PMT_MAX_INTERVAL_MS),
347 pid_error_period: Duration::from_secs(DEFAULT_PID_ERROR_PERIOD_SECS),
348 sync_acquire_packets: DEFAULT_SYNC_ACQUIRE_PACKETS,
349 sync_loss_packets: DEFAULT_SYNC_LOSS_PACKETS,
350 pcr_repetition_limit: Duration::from_millis(DEFAULT_PCR_REPETITION_LIMIT_MS),
351 pcr_discontinuity_limit: Duration::from_millis(DEFAULT_PCR_DISCONTINUITY_LIMIT_MS),
352 pts_repetition_limit: Duration::from_millis(DEFAULT_PTS_REPETITION_LIMIT_MS),
353 si_nit_interval: Duration::from_secs(DEFAULT_SI_NIT_INTERVAL_SECS),
354 si_sdt_interval: Duration::from_secs(DEFAULT_SI_SDT_INTERVAL_SECS),
355 si_eit_pf_interval: Duration::from_secs(DEFAULT_SI_EIT_PF_INTERVAL_SECS),
356 si_tdt_interval: Duration::from_secs(DEFAULT_SI_TDT_INTERVAL_SECS),
357 }
358 }
359}
360
361struct CcState {
365 last_cc: u8,
366 had_payload: bool,
367 dup_used: bool,
368 initialised: bool,
369}
370
371struct PresenceTimer {
373 last_seen: Duration,
374 reported: bool,
375}
376
377struct PmtTracking {
379 timer: PresenceTimer,
380 reassembler: SectionReassembler,
381}
382
383struct EsTracking {
385 timer: PresenceTimer,
386}
387
388struct PcrState {
390 last_pcr_27mhz: u64,
391 last_pcr_time: Duration,
392 initialised: bool,
393}
394
395struct PtsState {
397 last_pts_time: Duration,
398 armed: bool,
399}
400
401struct SiReassembly {
403 reassembler: SectionReassembler,
404}
405
406struct SiRepetitionTimer {
410 last_seen: Duration,
411 reported: bool,
412 armed: bool,
413}
414
415pub struct ConformanceMonitor {
423 config: Config,
424 events: Vec<ConformanceEvent>,
425 stats: Stats,
426
427 in_sync: bool,
429 good_run: u8,
430 bad_run: u8,
431
432 cc_states: HashMap<u16, CcState>,
434
435 pat_reassembler: SectionReassembler,
437 pat_timer: PresenceTimer,
438
439 pmt_trackings: HashMap<u16, PmtTracking>,
441
442 es_trackings: HashMap<u16, EsTracking>,
444
445 si_reassemblies: HashMap<u16, SiReassembly>,
447
448 pcr_states: HashMap<u16, PcrState>,
450
451 pts_states: HashMap<u16, PtsState>,
453
454 cat_seen: bool,
456 scrambled_without_cat_reported: bool,
457
458 si_timers: HashMap<u8, SiRepetitionTimer>,
460}
461
462impl ConformanceMonitor {
463 pub fn new() -> Self {
465 Self::with_config(Config::default())
466 }
467
468 pub fn with_config(config: Config) -> Self {
470 let mut si_reassemblies = HashMap::new();
471 for &pid in &SI_PIDS {
472 si_reassemblies.insert(
473 pid,
474 SiReassembly {
475 reassembler: SectionReassembler::default(),
476 },
477 );
478 }
479 Self {
480 config,
481 events: Vec::new(),
482 stats: Stats {
483 packets: 0,
484 events: 0,
485 in_sync: false,
486 },
487 in_sync: false,
488 good_run: 0,
489 bad_run: 0,
490 cc_states: HashMap::new(),
491 pat_reassembler: SectionReassembler::default(),
492 pat_timer: PresenceTimer {
493 last_seen: Duration::ZERO,
494 reported: false,
495 },
496 pmt_trackings: HashMap::new(),
497 es_trackings: HashMap::new(),
498 si_reassemblies,
499 pcr_states: HashMap::new(),
500 pts_states: HashMap::new(),
501 cat_seen: false,
502 scrambled_without_cat_reported: false,
503 si_timers: HashMap::new(),
504 }
505 }
506
507 pub fn feed(&mut self, ts_packet: &[u8], t: Duration) -> &[ConformanceEvent] {
513 self.events.clear();
514 self.stats.packets += 1;
515
516 let sync_ok = !ts_packet.is_empty() && ts_packet[0] == SYNC_BYTE;
518 if !sync_ok {
519 self.emit(Indicator::SyncByteError, None, t, "sync_byte != 0x47");
520 }
521
522 if sync_ok {
524 self.good_run = self.good_run.saturating_add(1);
525 self.bad_run = 0;
526 if !self.in_sync && self.good_run >= self.config.sync_acquire_packets {
527 self.in_sync = true;
528 }
529 } else {
530 self.bad_run = self.bad_run.saturating_add(1);
531 self.good_run = 0;
532 if self.in_sync && self.bad_run >= self.config.sync_loss_packets {
533 self.in_sync = false;
534 self.emit(
535 Indicator::TsSyncLoss,
536 None,
537 t,
538 "sync lost after hysteresis threshold",
539 );
540 }
541 }
542
543 if !self.in_sync {
547 return &self.events;
548 }
549
550 let packet = match TsPacket::parse(ts_packet) {
552 Ok(p) => p,
553 Err(_) => return &self.events,
554 };
555 let header = &packet.header;
556 let pid = header.pid;
557
558 if header.tei {
560 self.emit(
561 Indicator::TransportError,
562 Some(pid),
563 t,
564 format!("transport_error_indicator set on PID 0x{:04X}", pid),
565 );
566 }
567
568 if pid != PID_NULL {
570 self.check_cc(
571 pid,
572 header.continuity_counter,
573 header.has_payload,
574 t,
575 ts_packet,
576 );
577 }
578
579 if pid == PID_PAT && header.scrambling != 0 {
581 self.emit(
582 Indicator::PatError2,
583 Some(PID_PAT),
584 t,
585 format!(
586 "scrambling_control_field != 00 on PID 0x0000 (got {})",
587 header.scrambling
588 ),
589 );
590 }
591
592 if self.pmt_trackings.contains_key(&pid) && header.scrambling != 0 {
594 self.emit(
595 Indicator::PmtError2,
596 Some(pid),
597 t,
598 format!(
599 "scrambling_control_field != 00 on program_map_PID 0x{:04X}",
600 pid
601 ),
602 );
603 }
604
605 if header.scrambling != 0 && !self.cat_seen && !self.scrambled_without_cat_reported {
612 self.scrambled_without_cat_reported = true;
613 self.emit(
614 Indicator::CatError,
615 Some(pid),
616 t,
617 format!(
618 "scrambled packet on PID 0x{:04X} but no CAT seen on PID 0x0001",
619 pid
620 ),
621 );
622 }
623
624 if pid == PID_PAT && header.has_payload {
626 if let Some(payload) = packet.payload {
627 self.pat_reassembler.feed(payload, header.pusi);
628 }
629 self.pat_timer.last_seen = t;
630 self.pat_timer.reported = false;
631 while let Some(section_bytes) = self.pat_reassembler.pop_section() {
632 self.check_crc_and_process_pat(§ion_bytes, pid, t);
633 }
634 }
635
636 if self.pmt_trackings.contains_key(&pid) && header.has_payload {
638 if let Some(payload) = packet.payload {
639 if let Some(tracking) = self.pmt_trackings.get_mut(&pid) {
640 tracking.reassembler.feed(payload, header.pusi);
641 }
642 }
643 let sections: Vec<_> = if let Some(tracking) = self.pmt_trackings.get_mut(&pid) {
644 tracking.timer.last_seen = t;
645 tracking.timer.reported = false;
646 std::iter::from_fn(|| tracking.reassembler.pop_section()).collect()
647 } else {
648 Vec::new()
649 };
650 for section_bytes in §ions {
651 self.check_crc_and_process_pmt(section_bytes, pid, t);
652 }
653 }
654
655 if pid != PID_PAT
659 && !self.pmt_trackings.contains_key(&pid)
660 && self.si_reassemblies.contains_key(&pid)
661 && header.has_payload
662 {
663 if let Some(payload) = packet.payload {
664 if let Some(si_ra) = self.si_reassemblies.get_mut(&pid) {
665 si_ra.reassembler.feed(payload, header.pusi);
666 }
667 }
668 let sections: Vec<_> = if let Some(si_ra) = self.si_reassemblies.get_mut(&pid) {
669 std::iter::from_fn(|| si_ra.reassembler.pop_section()).collect()
670 } else {
671 Vec::new()
672 };
673 for section_bytes in §ions {
674 self.check_crc_for_si(section_bytes, pid, t);
675 self.check_cat_table_id(section_bytes, pid, t);
676 self.update_si_repetition(section_bytes, pid, t);
677 }
678 }
679 if let Some(tracking) = self.es_trackings.get_mut(&pid) {
687 tracking.timer.last_seen = t;
688 tracking.timer.reported = false;
689 }
690
691 if let Some(Ok(af)) = packet.adaptation_field() {
693 if let Some(pcr) = af.pcr {
694 self.check_pcr(pid, pcr.as_27mhz(), af.discontinuity_indicator, t);
695 }
696 }
697
698 if header.pusi
700 && header.scrambling == 0
701 && self.es_trackings.contains_key(&pid)
702 && header.has_payload
703 {
704 if let Some(payload) = packet.payload {
705 self.check_pts(pid, payload, t);
706 }
707 }
708
709 self.check_presence_timeouts(t);
711
712 &self.events
713 }
714
715 pub fn stats(&self) -> Stats {
717 Stats {
718 in_sync: self.in_sync,
719 ..self.stats
720 }
721 }
722
723 fn emit(
726 &mut self,
727 indicator: Indicator,
728 pid: Option<u16>,
729 at: Duration,
730 detail: impl Into<String>,
731 ) {
732 let event = ConformanceEvent {
733 indicator,
734 priority: indicator.priority(),
735 pid,
736 at,
737 detail: detail.into(),
738 };
739 self.stats.events += 1;
740 self.events.push(event);
741 }
742
743 fn check_cc(&mut self, pid: u16, cc: u8, has_payload: bool, t: Duration, raw: &[u8]) {
745 let discontinuity = if raw.len() >= 5 {
748 let b3 = raw[3];
749 let has_adaptation = (b3 & 0x20) != 0;
750 if has_adaptation {
751 let af_len = raw[4] as usize;
752 if af_len > 0 && raw.len() > 5 {
753 (raw[5] & 0x80) != 0
754 } else {
755 false
756 }
757 } else {
758 false
759 }
760 } else {
761 false
762 };
763
764 let (expected, is_duplicate, should_emit_dup, should_emit_cc) = {
766 let state = self.cc_states.entry(pid).or_insert_with(|| CcState {
767 last_cc: cc,
768 had_payload: has_payload,
769 dup_used: false,
770 initialised: false,
771 });
772
773 if !state.initialised {
774 state.last_cc = cc;
775 state.had_payload = has_payload;
776 state.dup_used = false;
777 state.initialised = true;
778 return;
779 }
780
781 if discontinuity {
782 (0u8, false, false, false)
784 } else {
785 let is_duplicate = cc == state.last_cc && has_payload;
786 let mut should_emit_dup = false;
787 let mut should_emit_cc = false;
788
789 if is_duplicate {
790 if state.dup_used {
791 should_emit_dup = true;
792 }
793 } else {
794 state.dup_used = false;
795 let expected = if has_payload {
796 (state.last_cc.wrapping_add(1)) & 0x0F
797 } else {
798 state.last_cc
799 };
800 if cc != expected {
801 should_emit_cc = true;
802 }
803 }
804
805 (
806 if has_payload {
807 (state.last_cc.wrapping_add(1)) & 0x0F
808 } else {
809 state.last_cc
810 },
811 is_duplicate,
812 should_emit_dup,
813 should_emit_cc,
814 )
815 }
816 };
817
818 if should_emit_dup {
820 self.emit(
821 Indicator::ContinuityCountError,
822 Some(pid),
823 t,
824 format!(
825 "second consecutive duplicate on PID 0x{:04X} (cc={})",
826 pid, cc
827 ),
828 );
829 }
830 if should_emit_cc {
831 self.emit(
832 Indicator::ContinuityCountError,
833 Some(pid),
834 t,
835 format!("expected cc={}, got {} on PID 0x{:04X}", expected, cc, pid),
836 );
837 }
838
839 let state = self.cc_states.get_mut(&pid).unwrap();
841 if discontinuity {
842 state.last_cc = cc;
843 state.had_payload = has_payload;
844 state.dup_used = false;
845 } else if is_duplicate {
846 state.dup_used = true;
848 } else {
849 state.dup_used = false;
850 state.last_cc = cc;
851 state.had_payload = has_payload;
852 }
853 }
854
855 fn check_crc_and_process_pat(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
857 self.check_crc_for_section(section_bytes, pid, t);
859
860 self.process_pat_section(section_bytes, t);
861 }
862
863 fn check_crc_and_process_pmt(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
865 self.check_crc_for_section(section_bytes, pid, t);
867
868 self.process_pmt_section(section_bytes, pid, t);
869 }
870
871 fn check_crc_for_section(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
876 let section = match Section::parse(section_bytes) {
877 Ok(s) => s,
878 Err(_) => return,
879 };
880
881 if let Err(dvb_si::error::Error::CrcMismatch { .. }) = section.validate_crc(section_bytes) {
883 self.emit(
884 Indicator::CrcError,
885 Some(pid),
886 t,
887 format!(
888 "CRC-32 mismatch on PID 0x{:04X} (table_id 0x{:02X})",
889 pid, section.table_id
890 ),
891 );
892 }
893 }
894
895 fn check_crc_for_si(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
897 self.check_crc_for_section(section_bytes, pid, t);
898 }
899
900 fn check_cat_table_id(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
903 if pid != PID_CAT {
904 return;
905 }
906 let section = match Section::parse(section_bytes) {
907 Ok(s) => s,
908 Err(_) => return,
909 };
910 if section.table_id == CAT_TABLE_ID {
911 self.cat_seen = true;
913 self.scrambled_without_cat_reported = false;
916 } else {
917 self.emit(
918 Indicator::CatError,
919 Some(PID_CAT),
920 t,
921 format!(
922 "section with table_id 0x{:02X} on PID 0x0001 (expected 0x01 for CAT)",
923 section.table_id
924 ),
925 );
926 }
927 }
928
929 fn process_pat_section(&mut self, section_bytes: &[u8], t: Duration) {
931 let section = match Section::parse(section_bytes) {
932 Ok(s) => s,
933 Err(_) => return,
934 };
935
936 if section.table_id != PAT_TABLE_ID {
938 self.emit(
939 Indicator::PatError2,
940 Some(PID_PAT),
941 t,
942 format!(
943 "section with table_id 0x{:02X} on PID 0x0000 (expected 0x00)",
944 section.table_id
945 ),
946 );
947 return;
948 }
949
950 let pat = match PatSection::parse(section_bytes) {
952 Ok(p) => p,
953 Err(_) => return,
954 };
955
956 for entry in pat.programmes() {
958 let pmt_pid = entry.pid;
959 self.pmt_trackings
960 .entry(pmt_pid)
961 .or_insert_with(|| PmtTracking {
962 timer: PresenceTimer {
963 last_seen: t,
964 reported: false,
965 },
966 reassembler: SectionReassembler::default(),
967 });
968 }
969 }
970
971 fn process_pmt_section(&mut self, section_bytes: &[u8], _pid: u16, t: Duration) {
973 let section = match Section::parse(section_bytes) {
974 Ok(s) => s,
975 Err(_) => return,
976 };
977
978 let pmt_table_id: u8 = dvb_si::tables::pmt::TABLE_ID;
983 if section.table_id != pmt_table_id {
984 return;
985 }
986
987 let pmt = match PmtSection::parse(section_bytes) {
989 Ok(p) => p,
990 Err(_) => return,
991 };
992
993 let mut new_es_pids: Vec<u16> = Vec::new();
995 if pmt.pcr_pid != PID_NULL && !self.es_trackings.contains_key(&pmt.pcr_pid) {
996 new_es_pids.push(pmt.pcr_pid);
997 }
998 for stream in &pmt.streams {
999 let es_pid = stream.elementary_pid;
1000 if !self.es_trackings.contains_key(&es_pid) {
1001 new_es_pids.push(es_pid);
1002 }
1003 }
1004
1005 for es_pid in new_es_pids {
1006 self.es_trackings.insert(
1007 es_pid,
1008 EsTracking {
1009 timer: PresenceTimer {
1010 last_seen: t,
1011 reported: false,
1012 },
1013 },
1014 );
1015 }
1016 }
1017
1018 fn check_pcr(&mut self, pid: u16, pcr_27mhz: u64, discontinuity: bool, t: Duration) {
1020 let state = self.pcr_states.entry(pid).or_insert_with(|| PcrState {
1021 last_pcr_27mhz: 0,
1022 last_pcr_time: Duration::ZERO,
1023 initialised: false,
1024 });
1025
1026 if !state.initialised {
1027 state.last_pcr_27mhz = pcr_27mhz;
1028 state.last_pcr_time = t;
1029 state.initialised = true;
1030 return;
1031 }
1032
1033 let last_pcr_time = state.last_pcr_time;
1035 let last_pcr_27mhz = state.last_pcr_27mhz;
1036
1037 let rep_interval = t.saturating_sub(last_pcr_time);
1040 let should_emit_rep = rep_interval > self.config.pcr_repetition_limit;
1041
1042 let delta =
1045 (pcr_27mhz.wrapping_add(PCR_MODULUS_27MHZ) - last_pcr_27mhz) % PCR_MODULUS_27MHZ;
1046 let delta_ms = delta * 1000 / CLOCK_27MHZ;
1047 let limit_ms = self.config.pcr_discontinuity_limit.as_millis() as u64;
1048 let should_emit_disc = delta_ms > limit_ms && !discontinuity;
1049
1050 if should_emit_rep {
1052 self.emit(
1053 Indicator::PcrRepetitionError,
1054 Some(pid),
1055 t,
1056 format!(
1057 "PCR interval {} ms exceeds limit {} ms on PID 0x{:04X}",
1058 rep_interval.as_millis(),
1059 self.config.pcr_repetition_limit.as_millis(),
1060 pid
1061 ),
1062 );
1063 }
1064 if should_emit_disc {
1065 self.emit(
1066 Indicator::PcrDiscontinuityError,
1067 Some(pid),
1068 t,
1069 format!(
1070 "PCR delta {} ms exceeds limit {} ms on PID 0x{:04X} without discontinuity_indicator",
1071 delta_ms, limit_ms, pid
1072 ),
1073 );
1074 }
1075
1076 let state = self.pcr_states.get_mut(&pid).unwrap();
1078 state.last_pcr_27mhz = pcr_27mhz;
1079 state.last_pcr_time = t;
1080 }
1081
1082 fn check_pts(&mut self, pid: u16, payload: &[u8], t: Duration) {
1087 if payload.len() < PES_FLAGS_OFFSET + 2 {
1089 return;
1090 }
1091 if payload[0] != PES_PREFIX_0 || payload[1] != PES_PREFIX_1 || payload[2] != PES_PREFIX_2 {
1092 return;
1093 }
1094
1095 let flags_byte = payload[PES_FLAGS_OFFSET];
1097 if (flags_byte >> 6) != 0b10 {
1098 return;
1099 }
1100
1101 let pts_dts_flags = payload[PES_FLAGS_OFFSET + 1] & PES_PTS_DTS_FLAGS_MASK;
1103 let pts_present = (pts_dts_flags & PES_PTS_PRESENT) != 0;
1104 if !pts_present {
1105 return;
1106 }
1107
1108 let state = self.pts_states.entry(pid).or_insert_with(|| PtsState {
1109 last_pts_time: Duration::ZERO,
1110 armed: false,
1111 });
1112
1113 if !state.armed {
1114 state.last_pts_time = t;
1116 state.armed = true;
1117 return;
1118 }
1119
1120 let last_pts_time = state.last_pts_time;
1122 let pts_interval = t.saturating_sub(last_pts_time);
1123 let should_emit = pts_interval > self.config.pts_repetition_limit;
1124
1125 if should_emit {
1126 self.emit(
1127 Indicator::PtsError,
1128 Some(pid),
1129 t,
1130 format!(
1131 "PTS interval {} ms exceeds limit {} ms on PID 0x{:04X}",
1132 pts_interval.as_millis(),
1133 self.config.pts_repetition_limit.as_millis(),
1134 pid
1135 ),
1136 );
1137 }
1138
1139 let state = self.pts_states.get_mut(&pid).unwrap();
1141 state.last_pts_time = t;
1142 }
1143
1144 fn update_si_repetition(&mut self, section_bytes: &[u8], _pid: u16, t: Duration) {
1148 let table_id = match Section::parse(section_bytes) {
1149 Ok(s) => s.table_id,
1150 Err(_) => return,
1151 };
1152
1153 let is_tracked = table_id == NIT_ACTUAL_TABLE_ID
1154 || table_id == SDT_ACTUAL_TABLE_ID
1155 || table_id == EIT_PF_ACTUAL_TABLE_ID
1156 || table_id == TDT_TABLE_ID;
1157
1158 if !is_tracked {
1159 return;
1160 }
1161
1162 let timer = self
1163 .si_timers
1164 .entry(table_id)
1165 .or_insert_with(|| SiRepetitionTimer {
1166 last_seen: Duration::ZERO,
1167 reported: false,
1168 armed: false,
1169 });
1170
1171 timer.last_seen = t;
1172 timer.reported = false;
1173 timer.armed = true;
1174 }
1175
1176 fn check_presence_timeouts(&mut self, t: Duration) {
1178 if t.saturating_sub(self.pat_timer.last_seen) > self.config.pat_max_interval
1180 && !self.pat_timer.reported
1181 {
1182 self.pat_timer.reported = true;
1183 self.emit(
1184 Indicator::PatError2,
1185 Some(PID_PAT),
1186 t,
1187 format!(
1188 "no PAT section within {} ms",
1189 self.config.pat_max_interval.as_millis()
1190 ),
1191 );
1192 }
1193
1194 let pmt_timeouts: Vec<(u16, u64)> = self
1197 .pmt_trackings
1198 .iter()
1199 .filter_map(|(&pid, tracking)| {
1200 if t.saturating_sub(tracking.timer.last_seen) > self.config.pmt_max_interval
1201 && !tracking.timer.reported
1202 {
1203 Some((pid, self.config.pmt_max_interval.as_millis() as u64))
1204 } else {
1205 None
1206 }
1207 })
1208 .collect();
1209 for (pid, interval_ms) in pmt_timeouts {
1210 if let Some(tracking) = self.pmt_trackings.get_mut(&pid) {
1211 tracking.timer.reported = true;
1212 }
1213 self.emit(
1214 Indicator::PmtError2,
1215 Some(pid),
1216 t,
1217 format!(
1218 "no PMT section on program_map_PID 0x{:04X} within {} ms",
1219 pid, interval_ms
1220 ),
1221 );
1222 }
1223
1224 let pid_timeouts: Vec<(u16, u64)> = self
1226 .es_trackings
1227 .iter()
1228 .filter_map(|(&pid, tracking)| {
1229 if t.saturating_sub(tracking.timer.last_seen) > self.config.pid_error_period
1230 && !tracking.timer.reported
1231 {
1232 Some((pid, self.config.pid_error_period.as_secs()))
1233 } else {
1234 None
1235 }
1236 })
1237 .collect();
1238 for (pid, period_secs) in pid_timeouts {
1239 if let Some(tracking) = self.es_trackings.get_mut(&pid) {
1240 tracking.timer.reported = true;
1241 }
1242 self.emit(
1243 Indicator::PidError,
1244 Some(pid),
1245 t,
1246 format!(
1247 "referenced PID 0x{:04X} absent for > {} s",
1248 pid, period_secs
1249 ),
1250 );
1251 }
1252
1253 let si_timeouts: Vec<(u8, u64, u16, u64)> = self
1256 .si_timers
1257 .iter()
1258 .filter_map(|(&table_id, timer)| {
1259 if !timer.armed || timer.reported {
1260 return None;
1261 }
1262 let (limit, pid) = match table_id {
1263 NIT_ACTUAL_TABLE_ID => (self.config.si_nit_interval, PID_NIT),
1264 SDT_ACTUAL_TABLE_ID => (self.config.si_sdt_interval, PID_SDT_BAT),
1265 EIT_PF_ACTUAL_TABLE_ID => (self.config.si_eit_pf_interval, PID_EIT),
1266 TDT_TABLE_ID => (self.config.si_tdt_interval, PID_TDT_TOT),
1267 _ => return None,
1268 };
1269 let interval = t.saturating_sub(timer.last_seen);
1270 if interval > limit {
1271 Some((
1272 table_id,
1273 interval.as_millis() as u64,
1274 pid,
1275 limit.as_millis() as u64,
1276 ))
1277 } else {
1278 None
1279 }
1280 })
1281 .collect();
1282 for (table_id, interval_ms, pid, limit_ms) in si_timeouts {
1283 if let Some(timer) = self.si_timers.get_mut(&table_id) {
1284 timer.reported = true;
1285 }
1286 let table_name = match table_id {
1287 NIT_ACTUAL_TABLE_ID => "NIT_actual",
1288 SDT_ACTUAL_TABLE_ID => "SDT_actual",
1289 EIT_PF_ACTUAL_TABLE_ID => "EIT_P/F_actual",
1290 TDT_TABLE_ID => "TDT",
1291 _ => "unknown",
1292 };
1293 self.emit(
1294 Indicator::SiRepetitionError,
1295 Some(pid),
1296 t,
1297 format!(
1298 "{} repetition interval {} ms exceeds {} ms",
1299 table_name, interval_ms, limit_ms
1300 ),
1301 );
1302 }
1303 }
1304}
1305
1306impl Default for ConformanceMonitor {
1307 fn default() -> Self {
1308 Self::new()
1309 }
1310}
1311
1312#[cfg(test)]
1313mod tests;