Skip to main content

dvb_si/collect/
eit.rs

1use alloc::collections::BTreeMap;
2use alloc::sync::Arc;
3use alloc::vec;
4use alloc::vec::Vec;
5
6use crate::descriptors::DescriptorRegistry;
7use crate::tables::RunningStatus;
8use crate::tables::eit;
9use broadcast_common::Parse;
10use mpeg_ts::section::Section;
11
12use super::{
13    CollectError, CollectResult, CompleteSectionSet, ParsedDescriptorLoop, SectionSetKey,
14    SectionSetMeta,
15};
16
17/// Default cap on the number of in-progress logical keys (section sets +
18/// schedule ranges) retained by [`EitCollector`].
19///
20/// 256 concurrent collections is generous — a real DVB network has at most a
21/// few dozen services per transponder — while bounding a hostile stream that
22/// rotates `original_network_id` / `transport_stream_id` / `service_id` (or
23/// `current_next_indicator`) to force unbounded map growth. The cap is applied
24/// independently to the sections map and the schedules map; each is limited to
25/// `max_logical_keys` entries. When a map is full, incoming sections for new
26/// keys are skipped until [`clear`](EitCollector::clear) or
27/// [`retain_logical`](EitCollector::retain_logical) frees capacity.
28pub const DEFAULT_MAX_LOGICAL_KEYS: usize = 256;
29
30/// EIT-specific collector.
31///
32/// Present/following EITs complete as one normal section set. Schedule EITs
33/// complete only when every schedule table_id from the kind's first table_id
34/// through the advertised `last_table_id` has completed its own section set.
35///
36/// # Memory bounds
37///
38/// The collector is bounded by [`DEFAULT_MAX_LOGICAL_KEYS`] (configurable via
39/// [`with_max_logical_keys`](Self::with_max_logical_keys)). When the sections
40/// or schedules map is full, incoming sections for new keys are skipped until
41/// space frees — the same skip-until-space policy as
42/// [`crate::carousel::ModuleReassembler`].
43#[derive(Debug)]
44pub struct EitCollector {
45    sections: BTreeMap<EitSectionSetKey, PartialEitSectionSet>,
46    schedules: BTreeMap<EitLogicalKey, PartialEitSchedule>,
47    max_logical_keys: usize,
48}
49
50impl Default for EitCollector {
51    fn default() -> Self {
52        Self {
53            sections: BTreeMap::new(),
54            schedules: BTreeMap::new(),
55            max_logical_keys: DEFAULT_MAX_LOGICAL_KEYS,
56        }
57    }
58}
59
60impl EitCollector {
61    /// Create an empty EIT collector with the default cap
62    /// ([`DEFAULT_MAX_LOGICAL_KEYS`]).
63    #[must_use]
64    pub fn new() -> Self {
65        Self::default()
66    }
67
68    /// Replace the logical-key cap (default [`DEFAULT_MAX_LOGICAL_KEYS`]).
69    /// The cap is applied independently to the sections and schedules maps.
70    /// Sections for new keys are skipped when the relevant map is full, until
71    /// [`clear`](Self::clear) or [`retain_logical`](Self::retain_logical)
72    /// frees capacity.
73    #[must_use]
74    pub fn with_max_logical_keys(mut self, max_logical_keys: usize) -> Self {
75        self.max_logical_keys = max_logical_keys;
76        self
77    }
78
79    /// Push one complete EIT section.
80    ///
81    /// Returns `Some` for a completed present/following table or a completed
82    /// schedule table-id range.
83    ///
84    /// # Errors
85    ///
86    /// Returns a [`CollectError`] if the incoming section is malformed,
87    /// inconsistent with already retained bytes, or not an EIT section. Treat
88    /// the error as applying to this section only unless your application wants
89    /// strict stream-fail behavior.
90    pub fn push_section(&mut self, bytes: impl AsRef<[u8]>) -> CollectResult<Option<CompletedEit>> {
91        self.push_section_with_pid(None, bytes)
92    }
93
94    /// Push one complete EIT section with PID context.
95    pub fn push_section_with_pid(
96        &mut self,
97        pid: Option<u16>,
98        bytes: impl AsRef<[u8]>,
99    ) -> CollectResult<Option<CompletedEit>> {
100        let raw = bytes.as_ref();
101        let section = Section::parse(raw)?;
102        if !section.section_syntax_indicator {
103            return Err(CollectError::ShortFormSection {
104                table_id: section.table_id,
105            });
106        }
107        if section.section_number > section.last_section_number {
108            return Err(CollectError::SectionNumberOutOfRange {
109                table_id: section.table_id,
110                section_number: section.section_number,
111                last_section_number: section.last_section_number,
112            });
113        }
114        section.validate_crc(raw)?;
115
116        let eit = eit::EitSection::parse(raw)?;
117        let logical_key = EitLogicalKey {
118            pid,
119            kind: eit.kind,
120            service_id: eit.service_id,
121            transport_stream_id: eit.transport_stream_id,
122            original_network_id: eit.original_network_id,
123            current_next_indicator: eit.current_next_indicator,
124        };
125        let key = EitSectionSetKey {
126            logical_key,
127            table_id: eit.table_id,
128        };
129        let meta = EitSectionSetMeta {
130            key,
131            version_number: eit.version_number,
132            last_section_number: eit.last_section_number,
133        };
134        let bytes: Arc<[u8]> = Arc::from(raw);
135
136        // Cap check: sections map
137        if !self.sections.contains_key(&key) && self.sections.len() >= self.max_logical_keys {
138            return Ok(None);
139        }
140
141        let partial = self
142            .sections
143            .entry(key)
144            .or_insert_with(|| PartialEitSectionSet::new(meta));
145        if partial.meta.version_number != meta.version_number
146            || partial.meta.last_section_number != meta.last_section_number
147        {
148            partial.reset(meta);
149        }
150
151        partial.insert(eit.section_number, eit.segment_last_section_number, bytes)?;
152        let complete = match partial.to_complete() {
153            Some(complete) => complete,
154            None => return Ok(None),
155        };
156
157        match eit.kind {
158            eit::EitKind::PresentFollowingActual | eit::EitKind::PresentFollowingOther => {
159                partial.emitted = true;
160                Ok(Some(CompletedEit::PresentFollowing(complete)))
161            }
162            eit::EitKind::ScheduleActual | eit::EitKind::ScheduleOther => {
163                let first_table_id = match eit.kind {
164                    eit::EitKind::ScheduleActual => eit::TABLE_ID_SCHEDULE_ACTUAL_FIRST,
165                    eit::EitKind::ScheduleOther => eit::TABLE_ID_SCHEDULE_OTHER_FIRST,
166                    _ => unreachable!("matched schedule kind above"),
167                };
168                if eit.table_id < first_table_id || eit.table_id > eit.last_table_id {
169                    return Err(CollectError::EitTableIdOutOfRange {
170                        table_id: eit.table_id,
171                        first_table_id,
172                        last_table_id: eit.last_table_id,
173                    });
174                }
175
176                // Cap check: schedules map (before marking the section set emitted)
177                if !self.schedules.contains_key(&logical_key)
178                    && self.schedules.len() >= self.max_logical_keys
179                {
180                    return Ok(None);
181                }
182
183                partial.emitted = true;
184
185                let schedule_meta = EitScheduleMeta {
186                    key: logical_key,
187                    first_table_id,
188                    last_table_id: eit.last_table_id,
189                };
190                let schedule = self
191                    .schedules
192                    .entry(logical_key)
193                    .or_insert_with(|| PartialEitSchedule::new(schedule_meta));
194                if schedule.meta.last_table_id != schedule_meta.last_table_id {
195                    schedule.reset(schedule_meta);
196                }
197                schedule.insert(eit.table_id, complete);
198                if let Some(complete) = schedule.to_complete() {
199                    schedule.emitted = true;
200                    Ok(Some(CompletedEit::Schedule(complete)))
201                } else {
202                    Ok(None)
203                }
204            }
205        }
206    }
207
208    /// Drop all retained EIT partial and completed schedule state.
209    ///
210    /// Long-running receivers that collect EPG data continuously can call this
211    /// at an application-defined carousel boundary if they do not need older
212    /// schedule state.
213    pub fn clear(&mut self) {
214        self.sections.clear();
215        self.schedules.clear();
216    }
217
218    /// Retain only logical EIT keys accepted by `keep`.
219    ///
220    /// This is the explicit pruning hook for long-running EIT schedule
221    /// collection. Both in-progress section sets and completed schedule ranges
222    /// for rejected keys are removed.
223    pub fn retain_logical<F>(&mut self, mut keep: F)
224    where
225        F: FnMut(&EitLogicalKey) -> bool,
226    {
227        self.sections.retain(|key, _| keep(&key.logical_key));
228        self.schedules.retain(|key, _| keep(key));
229    }
230
231    /// Number of retained EIT section-set states.
232    #[must_use]
233    pub fn section_set_len(&self) -> usize {
234        self.sections.len()
235    }
236
237    /// Number of retained EIT logical schedule states.
238    #[must_use]
239    pub fn schedule_len(&self) -> usize {
240        self.schedules.len()
241    }
242}
243
244/// Completed EIT collection result.
245#[derive(Debug, Clone)]
246#[non_exhaustive]
247pub enum CompletedEit {
248    /// One completed present/following EIT section set.
249    PresentFollowing(CompleteSectionSet),
250    /// A completed schedule EIT range spanning one or more table IDs.
251    Schedule(CompleteEitSchedule),
252}
253
254impl CompletedEit {
255    /// Parse the completed EIT table(s) without a descriptor registry.
256    pub fn tables(&self) -> crate::Result<Vec<CompleteEit<'_>>> {
257        self.tables_with_registry(None)
258    }
259
260    /// Parse the completed EIT table(s) with an optional descriptor registry.
261    pub fn tables_with_registry<'a>(
262        &'a self,
263        registry: Option<&'a DescriptorRegistry>,
264    ) -> crate::Result<Vec<CompleteEit<'a>>> {
265        match self {
266            Self::PresentFollowing(set) => Ok(vec![CompleteEit::parse(set, registry)?]),
267            Self::Schedule(schedule) => schedule.tables_with_registry(registry),
268        }
269    }
270}
271
272/// Logical EIT table key used by [`EitCollector`].
273#[non_exhaustive]
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
275pub struct EitLogicalKey {
276    /// Optional PID context supplied by the caller.
277    pub pid: Option<u16>,
278    /// EIT kind derived from table_id.
279    pub kind: eit::EitKind,
280    /// service_id.
281    pub service_id: u16,
282    /// transport_stream_id.
283    pub transport_stream_id: u16,
284    /// original_network_id.
285    pub original_network_id: u16,
286    /// current_next_indicator.
287    pub current_next_indicator: bool,
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
291struct EitSectionSetKey {
292    logical_key: EitLogicalKey,
293    table_id: u8,
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297struct EitScheduleMeta {
298    key: EitLogicalKey,
299    first_table_id: u8,
300    last_table_id: u8,
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304struct EitSectionSetMeta {
305    key: EitSectionSetKey,
306    version_number: u8,
307    last_section_number: u8,
308}
309
310/// Number of section numbers grouped into one 3-hour EIT schedule segment
311/// (ETSI EN 300 468 §5.2.4: `segment_last_section_number`).
312const EIT_SEGMENT_SIZE: u8 = 8;
313
314#[derive(Debug)]
315struct PartialEitSectionSet {
316    meta: EitSectionSetMeta,
317    slots: Vec<Option<Arc<[u8]>>>,
318    filled: usize,
319    emitted: bool,
320    /// Per-segment (`section_number / 8`) `segment_last_section_number` as
321    /// observed from any section received from that segment.
322    ///
323    /// Present/following EITs and small schedules never need more than one
324    /// entry here; full EIT-schedule sub-tables span up to 32 segments. A
325    /// sparse segment (no events) still transmits exactly one section — with
326    /// `segment_last_section_number` equal to that segment's first section —
327    /// so this map always gets populated for every segment that will ever
328    /// exist, without waiting for 8 sections that will never arrive.
329    segment_ends: BTreeMap<u8, u8>,
330}
331
332impl PartialEitSectionSet {
333    fn new(meta: EitSectionSetMeta) -> Self {
334        let len = meta.last_section_number as usize + 1;
335        Self {
336            meta,
337            slots: vec![None; len],
338            filled: 0,
339            emitted: false,
340            segment_ends: BTreeMap::new(),
341        }
342    }
343
344    fn reset(&mut self, meta: EitSectionSetMeta) {
345        *self = Self::new(meta);
346    }
347
348    fn insert(
349        &mut self,
350        section_number: u8,
351        segment_last_section_number: u8,
352        bytes: Arc<[u8]>,
353    ) -> CollectResult<bool> {
354        let index = section_number as usize;
355        if let Some(existing) = &self.slots[index] {
356            if existing.as_ref() == bytes.as_ref() {
357                return Ok(false);
358            }
359            return Err(CollectError::ConflictingSection {
360                table_id: self.meta.key.table_id,
361                section_number,
362            });
363        }
364
365        self.slots[index] = Some(bytes);
366        self.filled += 1;
367        self.emitted = false;
368        self.segment_ends.insert(
369            section_number / EIT_SEGMENT_SIZE,
370            segment_last_section_number,
371        );
372        Ok(true)
373    }
374
375    /// Whether every section that this section set's observed segments
376    /// declare (via `segment_last_section_number`) has been received.
377    ///
378    /// Unlike a plain PSI/SI table, an EIT schedule sub-table need not fill
379    /// every slot in `0..=last_section_number` — a segment that carries no
380    /// events transmits only its first section, with
381    /// `segment_last_section_number` naming that same section (ETSI EN 300
382    /// 468 §5.2.4). So completeness is judged per 8-section segment: once any
383    /// section from a segment has arrived, its `segment_last_section_number`
384    /// tells us exactly which sections in that segment to expect, rather than
385    /// assuming all 8.
386    fn complete(&self) -> bool {
387        let last_segment = self.meta.last_section_number / EIT_SEGMENT_SIZE;
388        for segment in 0..=last_segment {
389            let Some(&segment_last_raw) = self.segment_ends.get(&segment) else {
390                // No section from this segment has arrived yet, so we don't
391                // even know how many sections to expect from it.
392                return false;
393            };
394            let segment_start = segment * EIT_SEGMENT_SIZE;
395            let segment_last = segment_start
396                .saturating_add(segment_last_raw % EIT_SEGMENT_SIZE)
397                .min(self.meta.last_section_number);
398            for section_number in segment_start..=segment_last {
399                if self.slots[section_number as usize].is_none() {
400                    return false;
401                }
402            }
403        }
404        true
405    }
406
407    fn to_complete(&self) -> Option<CompleteSectionSet> {
408        if !self.complete() || self.emitted {
409            return None;
410        }
411
412        // Only the sections that actually exist on the wire are collected —
413        // a sparse segment legitimately never transmits some of the slots in
414        // `0..=last_section_number`, so holes here are expected, not a bug.
415        let sections = self.slots.iter().filter_map(Clone::clone).collect();
416        Some(CompleteSectionSet {
417            meta: SectionSetMeta {
418                key: SectionSetKey {
419                    pid: self.meta.key.logical_key.pid,
420                    table_id: self.meta.key.table_id,
421                    extension_id: self.meta.key.logical_key.service_id,
422                    current_next_indicator: self.meta.key.logical_key.current_next_indicator,
423                },
424                version_number: self.meta.version_number,
425                last_section_number: self.meta.last_section_number,
426            },
427            sections,
428        })
429    }
430}
431
432#[derive(Debug)]
433struct PartialEitSchedule {
434    meta: EitScheduleMeta,
435    table_sets: BTreeMap<u8, CompleteSectionSet>,
436    emitted: bool,
437}
438
439impl PartialEitSchedule {
440    fn new(meta: EitScheduleMeta) -> Self {
441        Self {
442            meta,
443            table_sets: BTreeMap::new(),
444            emitted: false,
445        }
446    }
447
448    fn reset(&mut self, meta: EitScheduleMeta) {
449        *self = Self::new(meta);
450    }
451
452    fn insert(&mut self, table_id: u8, set: CompleteSectionSet) {
453        self.table_sets.insert(table_id, set);
454        self.emitted = false;
455    }
456
457    fn complete(&self) -> bool {
458        (self.meta.first_table_id..=self.meta.last_table_id)
459            .all(|table_id| self.table_sets.contains_key(&table_id))
460    }
461
462    fn to_complete(&self) -> Option<CompleteEitSchedule> {
463        if !self.complete() || self.emitted {
464            return None;
465        }
466        let table_sets = (self.meta.first_table_id..=self.meta.last_table_id)
467            .map(|table_id| {
468                self.table_sets
469                    .get(&table_id)
470                    .expect("complete EIT schedule has no missing table IDs")
471                    .clone()
472            })
473            .collect();
474        Some(CompleteEitSchedule {
475            first_table_id: self.meta.first_table_id,
476            last_table_id: self.meta.last_table_id,
477            table_sets,
478        })
479    }
480}
481
482/// Completed EIT schedule spanning all schedule table IDs through
483/// `last_table_id`.
484#[derive(Debug, Clone)]
485pub struct CompleteEitSchedule {
486    first_table_id: u8,
487    last_table_id: u8,
488    table_sets: Vec<CompleteSectionSet>,
489}
490
491impl CompleteEitSchedule {
492    /// First schedule table_id in this range.
493    #[must_use]
494    pub const fn first_table_id(&self) -> u8 {
495        self.first_table_id
496    }
497
498    /// Last schedule table_id in this range.
499    #[must_use]
500    pub const fn last_table_id(&self) -> u8 {
501        self.last_table_id
502    }
503
504    /// Completed section sets, one per schedule table_id in order.
505    #[must_use]
506    pub fn table_sets(&self) -> &[CompleteSectionSet] {
507        &self.table_sets
508    }
509
510    /// Per-table_id 5-bit version numbers in schedule table_id order.
511    ///
512    /// DVB EIT schedule sub-tables version independently, so there is no single
513    /// schedule-wide version number.
514    pub fn table_versions(&self) -> impl ExactSizeIterator<Item = (u8, u8)> + '_ {
515        self.table_sets
516            .iter()
517            .map(|set| (set.meta().key.table_id, set.meta().version_number))
518    }
519
520    /// Parse each completed schedule table-id set.
521    pub fn tables(&self) -> crate::Result<Vec<CompleteEit<'_>>> {
522        self.tables_with_registry(None)
523    }
524
525    /// Parse each completed schedule table-id set with an optional descriptor
526    /// registry.
527    pub fn tables_with_registry<'a>(
528        &'a self,
529        registry: Option<&'a DescriptorRegistry>,
530    ) -> crate::Result<Vec<CompleteEit<'a>>> {
531        self.table_sets
532            .iter()
533            .map(|set| CompleteEit::parse(set, registry))
534            .collect()
535    }
536}
537
538/// Event entry in a complete EIT.
539#[derive(Debug)]
540#[non_exhaustive]
541pub struct CompleteEitEvent<'a> {
542    /// 16-bit event_id.
543    pub event_id: u16,
544    /// 40-bit start time.
545    pub start_time_raw: [u8; 5],
546    /// 24-bit duration.
547    pub duration_raw: [u8; 3],
548    /// 3-bit running status (EN 300 468 Table 6).
549    pub running_status: RunningStatus,
550    /// free_CA_mode.
551    pub free_ca_mode: bool,
552    /// Typed descriptor loop for this event.
553    pub descriptors: ParsedDescriptorLoop<'a>,
554}
555
556impl CompleteEitEvent<'_> {
557    /// Decode the 24-bit BCD `duration` (HHMMSS) to a [`core::time::Duration`].
558    ///
559    /// Returns `None` if the BCD nibbles are out of range.
560    #[must_use]
561    pub fn duration(&self) -> Option<core::time::Duration> {
562        broadcast_common::time::decode_bcd_duration(self.duration_raw)
563    }
564
565    /// Decode `start_time_raw` (16-bit MJD + 24-bit BCD UTC) to a UTC datetime.
566    ///
567    /// Returns `None` if the date/time fields are out of range. MJD→calendar
568    /// conversion per ETSI EN 300 468 Annex C.
569    #[cfg(feature = "chrono")]
570    #[must_use]
571    pub fn start_time(&self) -> Option<chrono::DateTime<chrono::Utc>> {
572        broadcast_common::time::decode_mjd_bcd_utc(self.start_time_raw)
573    }
574}
575
576/// Complete EIT for one exact table_id/extension section sequence.
577///
578/// EIT schedule collection across `last_table_id` is intentionally represented
579/// as multiple complete section sets: one per schedule table_id. That preserves
580/// the DVB schedule sub-table structure while still exposing flattened events.
581#[derive(Debug)]
582#[non_exhaustive]
583pub struct CompleteEit<'a> {
584    /// Variant based on table_id.
585    pub kind: eit::EitKind,
586    /// Raw table_id byte.
587    pub table_id: u8,
588    /// service_id.
589    pub service_id: u16,
590    /// 5-bit version_number.
591    pub version_number: u8,
592    /// current_next_indicator bit.
593    pub current_next_indicator: bool,
594    /// transport_stream_id.
595    pub transport_stream_id: u16,
596    /// original_network_id.
597    pub original_network_id: u16,
598    /// segment_last_section_number from section 0.
599    pub segment_last_section_number: u8,
600    /// last_table_id.
601    pub last_table_id: u8,
602    /// Events from all sections in wire order.
603    pub events: Vec<CompleteEitEvent<'a>>,
604}
605
606impl<'a> CompleteEit<'a> {
607    pub(crate) fn parse(
608        set: &'a CompleteSectionSet,
609        registry: Option<&'a DescriptorRegistry>,
610    ) -> crate::Result<Self> {
611        let sections: Vec<eit::EitSection<'a>> = set.parse_sections()?;
612        let first = sections.first().ok_or(crate::Error::BufferTooShort {
613            need: 1,
614            have: 0,
615            what: "CompleteEit sections",
616        })?;
617        let mut events = Vec::new();
618        for section in &sections {
619            events.extend(section.events.iter().map(|event| CompleteEitEvent {
620                event_id: event.event_id,
621                start_time_raw: event.start_time_raw,
622                duration_raw: event.duration_raw,
623                running_status: event.running_status,
624                free_ca_mode: event.free_ca_mode,
625                descriptors: ParsedDescriptorLoop::parse(event.descriptors, registry),
626            }));
627        }
628        Ok(Self {
629            kind: first.kind,
630            table_id: first.table_id,
631            service_id: first.service_id,
632            version_number: first.version_number,
633            current_next_indicator: first.current_next_indicator,
634            transport_stream_id: first.transport_stream_id,
635            original_network_id: first.original_network_id,
636            segment_last_section_number: first.segment_last_section_number,
637            last_table_id: first.last_table_id,
638            events,
639        })
640    }
641}