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
165impl Priority {
166 #[must_use]
168 pub fn name(&self) -> &'static str {
169 match self {
170 Self::First => "first priority",
171 Self::Second => "second priority",
172 Self::Third => "third priority",
173 }
174 }
175}
176dvb_common::impl_spec_display!(Priority);
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
182#[cfg_attr(feature = "serde", derive(serde::Serialize))]
183#[non_exhaustive]
184pub enum Indicator {
185 TsSyncLoss,
189 SyncByteError,
191 PatError2,
193 ContinuityCountError,
195 PmtError2,
197 PidError,
199
200 TransportError,
203 CrcError,
205 PcrRepetitionError,
207 PcrDiscontinuityError,
210 PtsError,
212 CatError,
214
215 SiRepetitionError,
219}
220
221impl Indicator {
222 #[must_use]
224 pub fn priority(self) -> Priority {
225 match self {
226 Self::TsSyncLoss
227 | Self::SyncByteError
228 | Self::PatError2
229 | Self::ContinuityCountError
230 | Self::PmtError2
231 | Self::PidError => Priority::First,
232 Self::TransportError
233 | Self::CrcError
234 | Self::PcrRepetitionError
235 | Self::PcrDiscontinuityError
236 | Self::PtsError
237 | Self::CatError => Priority::Second,
238 Self::SiRepetitionError => Priority::Third,
239 }
240 }
241
242 #[must_use]
244 pub fn name(self) -> &'static str {
245 match self {
246 Self::TsSyncLoss => "TS_sync_loss",
247 Self::SyncByteError => "Sync_byte_error",
248 Self::PatError2 => "PAT_error_2",
249 Self::ContinuityCountError => "Continuity_count_error",
250 Self::PmtError2 => "PMT_error_2",
251 Self::PidError => "PID_error",
252 Self::TransportError => "Transport_error",
253 Self::CrcError => "CRC_error",
254 Self::PcrRepetitionError => "PCR_repetition_error",
255 Self::PcrDiscontinuityError => "PCR_discontinuity_indicator_error",
256 Self::PtsError => "PTS_error",
257 Self::CatError => "CAT_error",
258 Self::SiRepetitionError => "SI_repetition_error",
259 }
260 }
261
262 #[must_use]
264 pub fn clause(self) -> &'static str {
265 match self {
266 Self::TsSyncLoss => "TR 101 290 v1.4.1 Table 5.0a indicator 1.1",
267 Self::SyncByteError => "TR 101 290 v1.4.1 Table 5.0a indicator 1.2",
268 Self::PatError2 => "TR 101 290 v1.4.1 Table 5.0a indicator 1.3.a",
269 Self::ContinuityCountError => "TR 101 290 v1.4.1 Table 5.0a indicator 1.4",
270 Self::PmtError2 => "TR 101 290 v1.4.1 Table 5.0a indicator 1.5.a",
271 Self::PidError => "TR 101 290 v1.4.1 Table 5.0a indicator 1.6",
272 Self::TransportError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.1",
273 Self::CrcError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.2",
274 Self::PcrRepetitionError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.3a",
275 Self::PcrDiscontinuityError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.3b",
276 Self::PtsError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.5",
277 Self::CatError => "TR 101 290 v1.4.1 Table 5.0b indicator 2.6",
278 Self::SiRepetitionError => "TR 101 290 v1.4.1 Table 5.0c indicator 3.2",
279 }
280 }
281}
282dvb_common::impl_spec_display!(Indicator);
283
284#[derive(Debug, Clone, PartialEq, Eq)]
286#[cfg_attr(feature = "serde", derive(serde::Serialize))]
287#[non_exhaustive]
288pub struct ConformanceEvent {
289 pub indicator: Indicator,
291 pub priority: Priority,
293 pub pid: Option<u16>,
295 pub at: Duration,
297 pub detail: String,
299}
300
301#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
303#[cfg_attr(feature = "serde", derive(serde::Serialize))]
304#[non_exhaustive]
305pub struct Stats {
306 pub packets: u64,
308 pub events: u64,
310 pub in_sync: bool,
312}
313
314#[derive(Debug, Clone)]
316#[non_exhaustive]
317pub struct Config {
318 pub pat_max_interval: Duration,
321 pub pmt_max_interval: Duration,
324 pub pid_error_period: Duration,
327 pub sync_acquire_packets: u8,
330 pub sync_loss_packets: u8,
333 pub pcr_repetition_limit: Duration,
336 pub pcr_discontinuity_limit: Duration,
339 pub pts_repetition_limit: Duration,
342 pub si_nit_interval: Duration,
345 pub si_sdt_interval: Duration,
348 pub si_eit_pf_interval: Duration,
351 pub si_tdt_interval: Duration,
354}
355
356impl Default for Config {
357 fn default() -> Self {
358 Self {
359 pat_max_interval: Duration::from_millis(DEFAULT_PAT_MAX_INTERVAL_MS),
360 pmt_max_interval: Duration::from_millis(DEFAULT_PMT_MAX_INTERVAL_MS),
361 pid_error_period: Duration::from_secs(DEFAULT_PID_ERROR_PERIOD_SECS),
362 sync_acquire_packets: DEFAULT_SYNC_ACQUIRE_PACKETS,
363 sync_loss_packets: DEFAULT_SYNC_LOSS_PACKETS,
364 pcr_repetition_limit: Duration::from_millis(DEFAULT_PCR_REPETITION_LIMIT_MS),
365 pcr_discontinuity_limit: Duration::from_millis(DEFAULT_PCR_DISCONTINUITY_LIMIT_MS),
366 pts_repetition_limit: Duration::from_millis(DEFAULT_PTS_REPETITION_LIMIT_MS),
367 si_nit_interval: Duration::from_secs(DEFAULT_SI_NIT_INTERVAL_SECS),
368 si_sdt_interval: Duration::from_secs(DEFAULT_SI_SDT_INTERVAL_SECS),
369 si_eit_pf_interval: Duration::from_secs(DEFAULT_SI_EIT_PF_INTERVAL_SECS),
370 si_tdt_interval: Duration::from_secs(DEFAULT_SI_TDT_INTERVAL_SECS),
371 }
372 }
373}
374
375struct CcState {
379 last_cc: u8,
380 had_payload: bool,
381 dup_used: bool,
382 initialised: bool,
383}
384
385struct PresenceTimer {
387 last_seen: Duration,
388 reported: bool,
389}
390
391struct PmtTracking {
393 timer: PresenceTimer,
394 reassembler: SectionReassembler,
395}
396
397struct EsTracking {
399 timer: PresenceTimer,
400}
401
402struct PcrState {
404 last_pcr_27mhz: u64,
405 last_pcr_time: Duration,
406 initialised: bool,
407}
408
409struct PtsState {
411 last_pts_time: Duration,
412 armed: bool,
413}
414
415struct SiReassembly {
417 reassembler: SectionReassembler,
418}
419
420struct SiRepetitionTimer {
424 last_seen: Duration,
425 reported: bool,
426 armed: bool,
427}
428
429pub struct ConformanceMonitor {
437 config: Config,
438 events: Vec<ConformanceEvent>,
439 stats: Stats,
440
441 in_sync: bool,
443 good_run: u8,
444 bad_run: u8,
445
446 cc_states: HashMap<u16, CcState>,
448
449 pat_reassembler: SectionReassembler,
451 pat_timer: PresenceTimer,
452
453 pmt_trackings: HashMap<u16, PmtTracking>,
455
456 es_trackings: HashMap<u16, EsTracking>,
458
459 si_reassemblies: HashMap<u16, SiReassembly>,
461
462 pcr_states: HashMap<u16, PcrState>,
464
465 pts_states: HashMap<u16, PtsState>,
467
468 cat_seen: bool,
470 scrambled_without_cat_reported: bool,
471
472 si_timers: HashMap<u8, SiRepetitionTimer>,
474}
475
476impl ConformanceMonitor {
477 pub fn new() -> Self {
479 Self::with_config(Config::default())
480 }
481
482 pub fn with_config(config: Config) -> Self {
484 let mut si_reassemblies = HashMap::new();
485 for &pid in &SI_PIDS {
486 si_reassemblies.insert(
487 pid,
488 SiReassembly {
489 reassembler: SectionReassembler::default(),
490 },
491 );
492 }
493 Self {
494 config,
495 events: Vec::new(),
496 stats: Stats {
497 packets: 0,
498 events: 0,
499 in_sync: false,
500 },
501 in_sync: false,
502 good_run: 0,
503 bad_run: 0,
504 cc_states: HashMap::new(),
505 pat_reassembler: SectionReassembler::default(),
506 pat_timer: PresenceTimer {
507 last_seen: Duration::ZERO,
508 reported: false,
509 },
510 pmt_trackings: HashMap::new(),
511 es_trackings: HashMap::new(),
512 si_reassemblies,
513 pcr_states: HashMap::new(),
514 pts_states: HashMap::new(),
515 cat_seen: false,
516 scrambled_without_cat_reported: false,
517 si_timers: HashMap::new(),
518 }
519 }
520
521 pub fn feed(&mut self, ts_packet: &[u8], t: Duration) -> &[ConformanceEvent] {
527 self.events.clear();
528 self.stats.packets += 1;
529
530 let sync_ok = !ts_packet.is_empty() && ts_packet[0] == SYNC_BYTE;
532 if !sync_ok {
533 self.emit(Indicator::SyncByteError, None, t, "sync_byte != 0x47");
534 }
535
536 if sync_ok {
538 self.good_run = self.good_run.saturating_add(1);
539 self.bad_run = 0;
540 if !self.in_sync && self.good_run >= self.config.sync_acquire_packets {
541 self.in_sync = true;
542 }
543 } else {
544 self.bad_run = self.bad_run.saturating_add(1);
545 self.good_run = 0;
546 if self.in_sync && self.bad_run >= self.config.sync_loss_packets {
547 self.in_sync = false;
548 self.emit(
549 Indicator::TsSyncLoss,
550 None,
551 t,
552 "sync lost after hysteresis threshold",
553 );
554 }
555 }
556
557 if !self.in_sync {
561 return &self.events;
562 }
563
564 let packet = match TsPacket::parse(ts_packet) {
566 Ok(p) => p,
567 Err(_) => return &self.events,
568 };
569 let header = &packet.header;
570 let pid = header.pid;
571
572 if header.tei {
574 self.emit(
575 Indicator::TransportError,
576 Some(pid),
577 t,
578 format!("transport_error_indicator set on PID 0x{:04X}", pid),
579 );
580 }
581
582 if pid != PID_NULL {
584 self.check_cc(
585 pid,
586 header.continuity_counter,
587 header.has_payload,
588 t,
589 ts_packet,
590 );
591 }
592
593 if pid == PID_PAT && header.scrambling != 0 {
595 self.emit(
596 Indicator::PatError2,
597 Some(PID_PAT),
598 t,
599 format!(
600 "scrambling_control_field != 00 on PID 0x0000 (got {})",
601 header.scrambling
602 ),
603 );
604 }
605
606 if self.pmt_trackings.contains_key(&pid) && header.scrambling != 0 {
608 self.emit(
609 Indicator::PmtError2,
610 Some(pid),
611 t,
612 format!(
613 "scrambling_control_field != 00 on program_map_PID 0x{:04X}",
614 pid
615 ),
616 );
617 }
618
619 if header.scrambling != 0 && !self.cat_seen && !self.scrambled_without_cat_reported {
626 self.scrambled_without_cat_reported = true;
627 self.emit(
628 Indicator::CatError,
629 Some(pid),
630 t,
631 format!(
632 "scrambled packet on PID 0x{:04X} but no CAT seen on PID 0x0001",
633 pid
634 ),
635 );
636 }
637
638 if pid == PID_PAT && header.has_payload {
640 if let Some(payload) = packet.payload {
641 self.pat_reassembler.feed(payload, header.pusi);
642 }
643 self.pat_timer.last_seen = t;
644 self.pat_timer.reported = false;
645 while let Some(section_bytes) = self.pat_reassembler.pop_section() {
646 self.check_crc_and_process_pat(§ion_bytes, pid, t);
647 }
648 }
649
650 if self.pmt_trackings.contains_key(&pid) && header.has_payload {
652 if let Some(payload) = packet.payload {
653 if let Some(tracking) = self.pmt_trackings.get_mut(&pid) {
654 tracking.reassembler.feed(payload, header.pusi);
655 }
656 }
657 let sections: Vec<_> = if let Some(tracking) = self.pmt_trackings.get_mut(&pid) {
658 tracking.timer.last_seen = t;
659 tracking.timer.reported = false;
660 std::iter::from_fn(|| tracking.reassembler.pop_section()).collect()
661 } else {
662 Vec::new()
663 };
664 for section_bytes in §ions {
665 self.check_crc_and_process_pmt(section_bytes, pid, t);
666 }
667 }
668
669 if pid != PID_PAT
673 && !self.pmt_trackings.contains_key(&pid)
674 && self.si_reassemblies.contains_key(&pid)
675 && header.has_payload
676 {
677 if let Some(payload) = packet.payload {
678 if let Some(si_ra) = self.si_reassemblies.get_mut(&pid) {
679 si_ra.reassembler.feed(payload, header.pusi);
680 }
681 }
682 let sections: Vec<_> = if let Some(si_ra) = self.si_reassemblies.get_mut(&pid) {
683 std::iter::from_fn(|| si_ra.reassembler.pop_section()).collect()
684 } else {
685 Vec::new()
686 };
687 for section_bytes in §ions {
688 self.check_crc_for_si(section_bytes, pid, t);
689 self.check_cat_table_id(section_bytes, pid, t);
690 self.update_si_repetition(section_bytes, pid, t);
691 }
692 }
693 if let Some(tracking) = self.es_trackings.get_mut(&pid) {
701 tracking.timer.last_seen = t;
702 tracking.timer.reported = false;
703 }
704
705 if let Some(Ok(af)) = packet.adaptation_field() {
707 if let Some(pcr) = af.pcr {
708 self.check_pcr(pid, pcr.as_27mhz(), af.discontinuity_indicator, t);
709 }
710 }
711
712 if header.pusi
714 && header.scrambling == 0
715 && self.es_trackings.contains_key(&pid)
716 && header.has_payload
717 {
718 if let Some(payload) = packet.payload {
719 self.check_pts(pid, payload, t);
720 }
721 }
722
723 self.check_presence_timeouts(t);
725
726 &self.events
727 }
728
729 pub fn stats(&self) -> Stats {
731 Stats {
732 in_sync: self.in_sync,
733 ..self.stats
734 }
735 }
736
737 fn emit(
740 &mut self,
741 indicator: Indicator,
742 pid: Option<u16>,
743 at: Duration,
744 detail: impl Into<String>,
745 ) {
746 let event = ConformanceEvent {
747 indicator,
748 priority: indicator.priority(),
749 pid,
750 at,
751 detail: detail.into(),
752 };
753 self.stats.events += 1;
754 self.events.push(event);
755 }
756
757 fn check_cc(&mut self, pid: u16, cc: u8, has_payload: bool, t: Duration, raw: &[u8]) {
759 let discontinuity = if raw.len() >= 5 {
762 let b3 = raw[3];
763 let has_adaptation = (b3 & 0x20) != 0;
764 if has_adaptation {
765 let af_len = raw[4] as usize;
766 if af_len > 0 && raw.len() > 5 {
767 (raw[5] & 0x80) != 0
768 } else {
769 false
770 }
771 } else {
772 false
773 }
774 } else {
775 false
776 };
777
778 let (expected, is_duplicate, should_emit_dup, should_emit_cc) = {
780 let state = self.cc_states.entry(pid).or_insert_with(|| CcState {
781 last_cc: cc,
782 had_payload: has_payload,
783 dup_used: false,
784 initialised: false,
785 });
786
787 if !state.initialised {
788 state.last_cc = cc;
789 state.had_payload = has_payload;
790 state.dup_used = false;
791 state.initialised = true;
792 return;
793 }
794
795 if discontinuity {
796 (0u8, false, false, false)
798 } else {
799 let is_duplicate = cc == state.last_cc && has_payload;
800 let mut should_emit_dup = false;
801 let mut should_emit_cc = false;
802
803 if is_duplicate {
804 if state.dup_used {
805 should_emit_dup = true;
806 }
807 } else {
808 state.dup_used = false;
809 let expected = if has_payload {
810 (state.last_cc.wrapping_add(1)) & 0x0F
811 } else {
812 state.last_cc
813 };
814 if cc != expected {
815 should_emit_cc = true;
816 }
817 }
818
819 (
820 if has_payload {
821 (state.last_cc.wrapping_add(1)) & 0x0F
822 } else {
823 state.last_cc
824 },
825 is_duplicate,
826 should_emit_dup,
827 should_emit_cc,
828 )
829 }
830 };
831
832 if should_emit_dup {
834 self.emit(
835 Indicator::ContinuityCountError,
836 Some(pid),
837 t,
838 format!(
839 "second consecutive duplicate on PID 0x{:04X} (cc={})",
840 pid, cc
841 ),
842 );
843 }
844 if should_emit_cc {
845 self.emit(
846 Indicator::ContinuityCountError,
847 Some(pid),
848 t,
849 format!("expected cc={}, got {} on PID 0x{:04X}", expected, cc, pid),
850 );
851 }
852
853 let state = self.cc_states.get_mut(&pid).unwrap();
855 if discontinuity {
856 state.last_cc = cc;
857 state.had_payload = has_payload;
858 state.dup_used = false;
859 } else if is_duplicate {
860 state.dup_used = true;
862 } else {
863 state.dup_used = false;
864 state.last_cc = cc;
865 state.had_payload = has_payload;
866 }
867 }
868
869 fn check_crc_and_process_pat(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
871 self.check_crc_for_section(section_bytes, pid, t);
873
874 self.process_pat_section(section_bytes, t);
875 }
876
877 fn check_crc_and_process_pmt(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
879 self.check_crc_for_section(section_bytes, pid, t);
881
882 self.process_pmt_section(section_bytes, pid, t);
883 }
884
885 fn check_crc_for_section(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
890 let section = match Section::parse(section_bytes) {
891 Ok(s) => s,
892 Err(_) => return,
893 };
894
895 if let Err(dvb_si::error::Error::CrcMismatch { .. }) = section.validate_crc(section_bytes) {
897 self.emit(
898 Indicator::CrcError,
899 Some(pid),
900 t,
901 format!(
902 "CRC-32 mismatch on PID 0x{:04X} (table_id 0x{:02X})",
903 pid, section.table_id
904 ),
905 );
906 }
907 }
908
909 fn check_crc_for_si(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
911 self.check_crc_for_section(section_bytes, pid, t);
912 }
913
914 fn check_cat_table_id(&mut self, section_bytes: &[u8], pid: u16, t: Duration) {
917 if pid != PID_CAT {
918 return;
919 }
920 let section = match Section::parse(section_bytes) {
921 Ok(s) => s,
922 Err(_) => return,
923 };
924 if section.table_id == CAT_TABLE_ID {
925 self.cat_seen = true;
927 self.scrambled_without_cat_reported = false;
930 } else {
931 self.emit(
932 Indicator::CatError,
933 Some(PID_CAT),
934 t,
935 format!(
936 "section with table_id 0x{:02X} on PID 0x0001 (expected 0x01 for CAT)",
937 section.table_id
938 ),
939 );
940 }
941 }
942
943 fn process_pat_section(&mut self, section_bytes: &[u8], t: Duration) {
945 let section = match Section::parse(section_bytes) {
946 Ok(s) => s,
947 Err(_) => return,
948 };
949
950 if section.table_id != PAT_TABLE_ID {
952 self.emit(
953 Indicator::PatError2,
954 Some(PID_PAT),
955 t,
956 format!(
957 "section with table_id 0x{:02X} on PID 0x0000 (expected 0x00)",
958 section.table_id
959 ),
960 );
961 return;
962 }
963
964 let pat = match PatSection::parse(section_bytes) {
966 Ok(p) => p,
967 Err(_) => return,
968 };
969
970 for entry in pat.programmes() {
972 let pmt_pid = entry.pid;
973 self.pmt_trackings
974 .entry(pmt_pid)
975 .or_insert_with(|| PmtTracking {
976 timer: PresenceTimer {
977 last_seen: t,
978 reported: false,
979 },
980 reassembler: SectionReassembler::default(),
981 });
982 }
983 }
984
985 fn process_pmt_section(&mut self, section_bytes: &[u8], _pid: u16, t: Duration) {
987 let section = match Section::parse(section_bytes) {
988 Ok(s) => s,
989 Err(_) => return,
990 };
991
992 let pmt_table_id: u8 = dvb_si::tables::pmt::TABLE_ID;
997 if section.table_id != pmt_table_id {
998 return;
999 }
1000
1001 let pmt = match PmtSection::parse(section_bytes) {
1003 Ok(p) => p,
1004 Err(_) => return,
1005 };
1006
1007 let mut new_es_pids: Vec<u16> = Vec::new();
1009 if pmt.pcr_pid != PID_NULL && !self.es_trackings.contains_key(&pmt.pcr_pid) {
1010 new_es_pids.push(pmt.pcr_pid);
1011 }
1012 for stream in &pmt.streams {
1013 let es_pid = stream.elementary_pid;
1014 if !self.es_trackings.contains_key(&es_pid) {
1015 new_es_pids.push(es_pid);
1016 }
1017 }
1018
1019 for es_pid in new_es_pids {
1020 self.es_trackings.insert(
1021 es_pid,
1022 EsTracking {
1023 timer: PresenceTimer {
1024 last_seen: t,
1025 reported: false,
1026 },
1027 },
1028 );
1029 }
1030 }
1031
1032 fn check_pcr(&mut self, pid: u16, pcr_27mhz: u64, discontinuity: bool, t: Duration) {
1034 let state = self.pcr_states.entry(pid).or_insert_with(|| PcrState {
1035 last_pcr_27mhz: 0,
1036 last_pcr_time: Duration::ZERO,
1037 initialised: false,
1038 });
1039
1040 if !state.initialised {
1041 state.last_pcr_27mhz = pcr_27mhz;
1042 state.last_pcr_time = t;
1043 state.initialised = true;
1044 return;
1045 }
1046
1047 let last_pcr_time = state.last_pcr_time;
1049 let last_pcr_27mhz = state.last_pcr_27mhz;
1050
1051 let rep_interval = t.saturating_sub(last_pcr_time);
1054 let should_emit_rep = rep_interval > self.config.pcr_repetition_limit;
1055
1056 let delta =
1059 (pcr_27mhz.wrapping_add(PCR_MODULUS_27MHZ) - last_pcr_27mhz) % PCR_MODULUS_27MHZ;
1060 let delta_ms = delta * 1000 / CLOCK_27MHZ;
1061 let limit_ms = self.config.pcr_discontinuity_limit.as_millis() as u64;
1062 let should_emit_disc = delta_ms > limit_ms && !discontinuity;
1063
1064 if should_emit_rep {
1066 self.emit(
1067 Indicator::PcrRepetitionError,
1068 Some(pid),
1069 t,
1070 format!(
1071 "PCR interval {} ms exceeds limit {} ms on PID 0x{:04X}",
1072 rep_interval.as_millis(),
1073 self.config.pcr_repetition_limit.as_millis(),
1074 pid
1075 ),
1076 );
1077 }
1078 if should_emit_disc {
1079 self.emit(
1080 Indicator::PcrDiscontinuityError,
1081 Some(pid),
1082 t,
1083 format!(
1084 "PCR delta {} ms exceeds limit {} ms on PID 0x{:04X} without discontinuity_indicator",
1085 delta_ms, limit_ms, pid
1086 ),
1087 );
1088 }
1089
1090 let state = self.pcr_states.get_mut(&pid).unwrap();
1092 state.last_pcr_27mhz = pcr_27mhz;
1093 state.last_pcr_time = t;
1094 }
1095
1096 fn check_pts(&mut self, pid: u16, payload: &[u8], t: Duration) {
1101 if payload.len() < PES_FLAGS_OFFSET + 2 {
1103 return;
1104 }
1105 if payload[0] != PES_PREFIX_0 || payload[1] != PES_PREFIX_1 || payload[2] != PES_PREFIX_2 {
1106 return;
1107 }
1108
1109 let flags_byte = payload[PES_FLAGS_OFFSET];
1111 if (flags_byte >> 6) != 0b10 {
1112 return;
1113 }
1114
1115 let pts_dts_flags = payload[PES_FLAGS_OFFSET + 1] & PES_PTS_DTS_FLAGS_MASK;
1117 let pts_present = (pts_dts_flags & PES_PTS_PRESENT) != 0;
1118 if !pts_present {
1119 return;
1120 }
1121
1122 let state = self.pts_states.entry(pid).or_insert_with(|| PtsState {
1123 last_pts_time: Duration::ZERO,
1124 armed: false,
1125 });
1126
1127 if !state.armed {
1128 state.last_pts_time = t;
1130 state.armed = true;
1131 return;
1132 }
1133
1134 let last_pts_time = state.last_pts_time;
1136 let pts_interval = t.saturating_sub(last_pts_time);
1137 let should_emit = pts_interval > self.config.pts_repetition_limit;
1138
1139 if should_emit {
1140 self.emit(
1141 Indicator::PtsError,
1142 Some(pid),
1143 t,
1144 format!(
1145 "PTS interval {} ms exceeds limit {} ms on PID 0x{:04X}",
1146 pts_interval.as_millis(),
1147 self.config.pts_repetition_limit.as_millis(),
1148 pid
1149 ),
1150 );
1151 }
1152
1153 let state = self.pts_states.get_mut(&pid).unwrap();
1155 state.last_pts_time = t;
1156 }
1157
1158 fn update_si_repetition(&mut self, section_bytes: &[u8], _pid: u16, t: Duration) {
1162 let table_id = match Section::parse(section_bytes) {
1163 Ok(s) => s.table_id,
1164 Err(_) => return,
1165 };
1166
1167 let is_tracked = table_id == NIT_ACTUAL_TABLE_ID
1168 || table_id == SDT_ACTUAL_TABLE_ID
1169 || table_id == EIT_PF_ACTUAL_TABLE_ID
1170 || table_id == TDT_TABLE_ID;
1171
1172 if !is_tracked {
1173 return;
1174 }
1175
1176 let timer = self
1177 .si_timers
1178 .entry(table_id)
1179 .or_insert_with(|| SiRepetitionTimer {
1180 last_seen: Duration::ZERO,
1181 reported: false,
1182 armed: false,
1183 });
1184
1185 timer.last_seen = t;
1186 timer.reported = false;
1187 timer.armed = true;
1188 }
1189
1190 fn check_presence_timeouts(&mut self, t: Duration) {
1192 if t.saturating_sub(self.pat_timer.last_seen) > self.config.pat_max_interval
1194 && !self.pat_timer.reported
1195 {
1196 self.pat_timer.reported = true;
1197 self.emit(
1198 Indicator::PatError2,
1199 Some(PID_PAT),
1200 t,
1201 format!(
1202 "no PAT section within {} ms",
1203 self.config.pat_max_interval.as_millis()
1204 ),
1205 );
1206 }
1207
1208 let pmt_timeouts: Vec<(u16, u64)> = self
1211 .pmt_trackings
1212 .iter()
1213 .filter_map(|(&pid, tracking)| {
1214 if t.saturating_sub(tracking.timer.last_seen) > self.config.pmt_max_interval
1215 && !tracking.timer.reported
1216 {
1217 Some((pid, self.config.pmt_max_interval.as_millis() as u64))
1218 } else {
1219 None
1220 }
1221 })
1222 .collect();
1223 for (pid, interval_ms) in pmt_timeouts {
1224 if let Some(tracking) = self.pmt_trackings.get_mut(&pid) {
1225 tracking.timer.reported = true;
1226 }
1227 self.emit(
1228 Indicator::PmtError2,
1229 Some(pid),
1230 t,
1231 format!(
1232 "no PMT section on program_map_PID 0x{:04X} within {} ms",
1233 pid, interval_ms
1234 ),
1235 );
1236 }
1237
1238 let pid_timeouts: Vec<(u16, u64)> = self
1240 .es_trackings
1241 .iter()
1242 .filter_map(|(&pid, tracking)| {
1243 if t.saturating_sub(tracking.timer.last_seen) > self.config.pid_error_period
1244 && !tracking.timer.reported
1245 {
1246 Some((pid, self.config.pid_error_period.as_secs()))
1247 } else {
1248 None
1249 }
1250 })
1251 .collect();
1252 for (pid, period_secs) in pid_timeouts {
1253 if let Some(tracking) = self.es_trackings.get_mut(&pid) {
1254 tracking.timer.reported = true;
1255 }
1256 self.emit(
1257 Indicator::PidError,
1258 Some(pid),
1259 t,
1260 format!(
1261 "referenced PID 0x{:04X} absent for > {} s",
1262 pid, period_secs
1263 ),
1264 );
1265 }
1266
1267 let si_timeouts: Vec<(u8, u64, u16, u64)> = self
1270 .si_timers
1271 .iter()
1272 .filter_map(|(&table_id, timer)| {
1273 if !timer.armed || timer.reported {
1274 return None;
1275 }
1276 let (limit, pid) = match table_id {
1277 NIT_ACTUAL_TABLE_ID => (self.config.si_nit_interval, PID_NIT),
1278 SDT_ACTUAL_TABLE_ID => (self.config.si_sdt_interval, PID_SDT_BAT),
1279 EIT_PF_ACTUAL_TABLE_ID => (self.config.si_eit_pf_interval, PID_EIT),
1280 TDT_TABLE_ID => (self.config.si_tdt_interval, PID_TDT_TOT),
1281 _ => return None,
1282 };
1283 let interval = t.saturating_sub(timer.last_seen);
1284 if interval > limit {
1285 Some((
1286 table_id,
1287 interval.as_millis() as u64,
1288 pid,
1289 limit.as_millis() as u64,
1290 ))
1291 } else {
1292 None
1293 }
1294 })
1295 .collect();
1296 for (table_id, interval_ms, pid, limit_ms) in si_timeouts {
1297 if let Some(timer) = self.si_timers.get_mut(&table_id) {
1298 timer.reported = true;
1299 }
1300 let table_name = match table_id {
1301 NIT_ACTUAL_TABLE_ID => "NIT_actual",
1302 SDT_ACTUAL_TABLE_ID => "SDT_actual",
1303 EIT_PF_ACTUAL_TABLE_ID => "EIT_P/F_actual",
1304 TDT_TABLE_ID => "TDT",
1305 _ => "unknown",
1306 };
1307 self.emit(
1308 Indicator::SiRepetitionError,
1309 Some(pid),
1310 t,
1311 format!(
1312 "{} repetition interval {} ms exceeds {} ms",
1313 table_name, interval_ms, limit_ms
1314 ),
1315 );
1316 }
1317 }
1318}
1319
1320impl Default for ConformanceMonitor {
1321 fn default() -> Self {
1322 Self::new()
1323 }
1324}
1325
1326#[cfg(test)]
1327mod tests;