Skip to main content

dvb_si/tables/
eit.rs

1//! Event Information Table — ETSI EN 300 468 §5.2.4.
2//!
3//! EIT carries programme event metadata. Four variants distinguished by
4//! table_id:
5//! - `0x4E` — Present/Following for the actual TS
6//! - `0x4F` — Present/Following for another TS
7//! - `0x50..=0x5F` — Schedule sub-tables for the actual TS
8//! - `0x60..=0x6F` — Schedule sub-tables for another TS
9
10use crate::descriptors::DescriptorLoop;
11use crate::error::{Error, Result};
12use crate::traits::Table;
13use dvb_common::{Parse, Serialize};
14
15/// table_id for present/following on the actual TS.
16pub const TABLE_ID_PF_ACTUAL: u8 = 0x4E;
17/// table_id for present/following on other TSes.
18pub const TABLE_ID_PF_OTHER: u8 = 0x4F;
19/// First table_id in the schedule range for the actual TS.
20pub const TABLE_ID_SCHEDULE_ACTUAL_FIRST: u8 = 0x50;
21/// Last table_id in the schedule range for the actual TS (inclusive).
22pub const TABLE_ID_SCHEDULE_ACTUAL_LAST: u8 = 0x5F;
23/// First table_id in the schedule range for other TSes.
24pub const TABLE_ID_SCHEDULE_OTHER_FIRST: u8 = 0x60;
25/// Last table_id in the schedule range for other TSes (inclusive).
26pub const TABLE_ID_SCHEDULE_OTHER_LAST: u8 = 0x6F;
27/// Well-known PID on which EIT is carried.
28pub const PID: u16 = 0x0012;
29
30const MIN_HEADER_LEN: usize = 3;
31const EXTENSION_HEADER_LEN: usize = 5;
32/// transport_stream_id(2) + original_network_id(2) + segment_last_section_number(1)
33/// + last_table_id(1) = 6 bytes between the section header and the first event.
34const POST_EXTENSION_LEN: usize = 6;
35const CRC_LEN: usize = 4;
36const EVENT_HEADER_LEN: usize = 12;
37
38/// EIT variant distinguished by table_id range.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize))]
41pub enum EitKind {
42    /// Present/Following, actual TS.
43    PresentFollowingActual,
44    /// Present/Following, other TS.
45    PresentFollowingOther,
46    /// Schedule, actual TS — table_id `0x50..=0x5F`.
47    ScheduleActual,
48    /// Schedule, other TS — table_id `0x60..=0x6F`.
49    ScheduleOther,
50}
51
52impl EitKind {
53    /// Classify a table_id byte into a kind, if recognised.
54    #[must_use]
55    pub fn from_table_id(table_id: u8) -> Option<Self> {
56        match table_id {
57            TABLE_ID_PF_ACTUAL => Some(Self::PresentFollowingActual),
58            TABLE_ID_PF_OTHER => Some(Self::PresentFollowingOther),
59            TABLE_ID_SCHEDULE_ACTUAL_FIRST..=TABLE_ID_SCHEDULE_ACTUAL_LAST => {
60                Some(Self::ScheduleActual)
61            }
62            TABLE_ID_SCHEDULE_OTHER_FIRST..=TABLE_ID_SCHEDULE_OTHER_LAST => {
63                Some(Self::ScheduleOther)
64            }
65            _ => None,
66        }
67    }
68}
69
70/// One event in the EIT.
71#[derive(Debug, Clone, PartialEq, Eq)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize))]
73#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
74pub struct EitEvent<'a> {
75    /// 16-bit event_id.
76    pub event_id: u16,
77    /// 40-bit start_time: 16-bit MJD followed by 24-bit BCD UTC (HHMMSS).
78    pub start_time_raw: [u8; 5],
79    /// 24-bit BCD duration HHMMSS.
80    pub duration_raw: [u8; 3],
81    /// 3-bit running_status.
82    pub running_status: u8,
83    /// free_CA_mode flag.
84    pub free_ca_mode: bool,
85    /// Descriptor loop for this event. Serializes as the typed descriptor
86    /// sequence; `.raw()` yields the wire bytes.
87    pub descriptors: DescriptorLoop<'a>,
88}
89
90/// Event Information Table.
91#[derive(Debug, Clone, PartialEq, Eq)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize))]
93#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
94pub struct EitSection<'a> {
95    /// Variant based on table_id.
96    pub kind: EitKind,
97    /// Raw table_id byte as parsed (for schedule sub-tables, identifies the slot).
98    pub table_id: u8,
99    /// service_id the events belong to (table_id_extension).
100    pub service_id: u16,
101    /// 5-bit version_number.
102    pub version_number: u8,
103    /// current_next_indicator bit.
104    pub current_next_indicator: bool,
105    /// section_number.
106    pub section_number: u8,
107    /// last_section_number.
108    pub last_section_number: u8,
109    /// transport_stream_id the events are carried on.
110    pub transport_stream_id: u16,
111    /// original_network_id.
112    pub original_network_id: u16,
113    /// segment_last_section_number.
114    pub segment_last_section_number: u8,
115    /// last_table_id (for schedule sub-table grouping).
116    pub last_table_id: u8,
117    /// Events in wire order.
118    pub events: Vec<EitEvent<'a>>,
119}
120
121impl<'a> Parse<'a> for EitSection<'a> {
122    type Error = crate::error::Error;
123    fn parse(bytes: &'a [u8]) -> Result<Self> {
124        let min_len = MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXTENSION_LEN + CRC_LEN;
125        if bytes.len() < min_len {
126            return Err(Error::BufferTooShort {
127                need: min_len,
128                have: bytes.len(),
129                what: "EitSection",
130            });
131        }
132
133        let table_id = bytes[0];
134        let kind = EitKind::from_table_id(table_id).ok_or(Error::UnexpectedTableId {
135            table_id,
136            what: "EitSection",
137            expected: &[
138                TABLE_ID_PF_ACTUAL,
139                TABLE_ID_PF_OTHER,
140                TABLE_ID_SCHEDULE_ACTUAL_FIRST,
141                TABLE_ID_SCHEDULE_OTHER_FIRST,
142            ],
143        })?;
144
145        let section_length = ((bytes[1] & 0x0F) as u16) << 8 | bytes[2] as u16;
146        let total = MIN_HEADER_LEN + section_length as usize;
147        if bytes.len() < total {
148            return Err(Error::SectionLengthOverflow {
149                declared: section_length as usize,
150                available: bytes.len() - MIN_HEADER_LEN,
151            });
152        }
153
154        let service_id = u16::from_be_bytes([bytes[3], bytes[4]]);
155        let version_number = (bytes[5] >> 1) & 0x1F;
156        let current_next_indicator = (bytes[5] & 0x01) != 0;
157        let section_number = bytes[6];
158        let last_section_number = bytes[7];
159
160        let transport_stream_id = u16::from_be_bytes([bytes[8], bytes[9]]);
161        let original_network_id = u16::from_be_bytes([bytes[10], bytes[11]]);
162        let segment_last_section_number = bytes[12];
163        let last_table_id = bytes[13];
164
165        let events_start = MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXTENSION_LEN;
166        let events_end = total - CRC_LEN;
167        let mut events = Vec::new();
168        let mut pos = events_start;
169        while pos + EVENT_HEADER_LEN <= events_end {
170            let event_id = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]);
171            let start_time_raw = [
172                bytes[pos + 2],
173                bytes[pos + 3],
174                bytes[pos + 4],
175                bytes[pos + 5],
176                bytes[pos + 6],
177            ];
178            let duration_raw = [bytes[pos + 7], bytes[pos + 8], bytes[pos + 9]];
179            let status_and_len_hi = bytes[pos + 10];
180            let running_status = (status_and_len_hi >> 5) & 0x07;
181            let free_ca_mode = (status_and_len_hi & 0x10) != 0;
182            let descriptors_loop_length =
183                (((status_and_len_hi & 0x0F) as usize) << 8) | bytes[pos + 11] as usize;
184            let desc_start = pos + EVENT_HEADER_LEN;
185            let desc_end = desc_start + descriptors_loop_length;
186            if desc_end > events_end {
187                return Err(Error::SectionLengthOverflow {
188                    declared: descriptors_loop_length,
189                    available: events_end - desc_start,
190                });
191            }
192            events.push(EitEvent {
193                event_id,
194                start_time_raw,
195                duration_raw,
196                running_status,
197                free_ca_mode,
198                descriptors: DescriptorLoop::new(&bytes[desc_start..desc_end]),
199            });
200            pos = desc_end;
201        }
202
203        Ok(EitSection {
204            kind,
205            table_id,
206            service_id,
207            version_number,
208            current_next_indicator,
209            section_number,
210            last_section_number,
211            transport_stream_id,
212            original_network_id,
213            segment_last_section_number,
214            last_table_id,
215            events,
216        })
217    }
218}
219
220impl Serialize for EitSection<'_> {
221    type Error = crate::error::Error;
222    fn serialized_len(&self) -> usize {
223        let ev_bytes: usize = self
224            .events
225            .iter()
226            .map(|e| EVENT_HEADER_LEN + e.descriptors.len())
227            .sum();
228        MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXTENSION_LEN + ev_bytes + CRC_LEN
229    }
230
231    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
232        let len = self.serialized_len();
233        if buf.len() < len {
234            return Err(Error::OutputBufferTooSmall {
235                need: len,
236                have: buf.len(),
237            });
238        }
239        let section_length: u16 = (len - MIN_HEADER_LEN) as u16;
240        buf[0] = self.table_id;
241        buf[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
242        buf[2] = (section_length & 0xFF) as u8;
243        buf[3..5].copy_from_slice(&self.service_id.to_be_bytes());
244        buf[5] = 0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
245        buf[6] = self.section_number;
246        buf[7] = self.last_section_number;
247        buf[8..10].copy_from_slice(&self.transport_stream_id.to_be_bytes());
248        buf[10..12].copy_from_slice(&self.original_network_id.to_be_bytes());
249        buf[12] = self.segment_last_section_number;
250        buf[13] = self.last_table_id;
251
252        let mut pos = MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXTENSION_LEN;
253        for ev in &self.events {
254            buf[pos..pos + 2].copy_from_slice(&ev.event_id.to_be_bytes());
255            buf[pos + 2..pos + 7].copy_from_slice(&ev.start_time_raw);
256            buf[pos + 7..pos + 10].copy_from_slice(&ev.duration_raw);
257            let dll = ev.descriptors.len() as u16;
258            buf[pos + 10] = ((ev.running_status & 0x07) << 5)
259                | (u8::from(ev.free_ca_mode) << 4)
260                | ((dll >> 8) as u8 & 0x0F);
261            buf[pos + 11] = (dll & 0xFF) as u8;
262            let desc_start = pos + EVENT_HEADER_LEN;
263            buf[desc_start..desc_start + ev.descriptors.len()]
264                .copy_from_slice(ev.descriptors.raw());
265            pos = desc_start + ev.descriptors.len();
266        }
267
268        let crc_pos = len - CRC_LEN;
269        let crc = dvb_common::crc32_mpeg2::compute(&buf[..crc_pos]);
270        buf[crc_pos..len].copy_from_slice(&crc.to_be_bytes());
271        Ok(len)
272    }
273}
274
275impl<'a> Table<'a> for EitSection<'a> {
276    const TABLE_ID: u8 = TABLE_ID_PF_ACTUAL;
277    const PID: u16 = PID;
278}
279
280impl<'a> crate::traits::TableDef<'a> for EitSection<'a> {
281    const TABLE_ID_RANGES: &'static [(u8, u8)] =
282        &[(TABLE_ID_PF_ACTUAL, TABLE_ID_SCHEDULE_OTHER_LAST)];
283    const NAME: &'static str = "EVENT_INFORMATION";
284}
285
286#[cfg(feature = "chrono")]
287impl EitEvent<'_> {
288    /// Decode `start_time_raw` (16-bit MJD + 24-bit BCD UTC) to a UTC datetime.
289    /// Returns `None` if the BCD nibbles are out of range.
290    ///
291    /// MJD→calendar conversion per ETSI EN 300 468 Annex C.
292    #[must_use]
293    pub fn start_time(&self) -> Option<chrono::DateTime<chrono::Utc>> {
294        use chrono::{NaiveDate, NaiveDateTime, TimeZone};
295        let mjd = u16::from_be_bytes([self.start_time_raw[0], self.start_time_raw[1]]);
296        let (y, m, d) = mjd_to_ymd(mjd);
297        let h = bcd_byte(self.start_time_raw[2])?;
298        let mi = bcd_byte(self.start_time_raw[3])?;
299        let s = bcd_byte(self.start_time_raw[4])?;
300        let date = NaiveDate::from_ymd_opt(y, m, d)?;
301        let time = chrono::NaiveTime::from_hms_opt(u32::from(h), u32::from(mi), u32::from(s))?;
302        let naive = NaiveDateTime::new(date, time);
303        chrono::Utc.from_local_datetime(&naive).single()
304    }
305}
306
307#[cfg(feature = "chrono")]
308fn bcd_byte(b: u8) -> Option<u8> {
309    let hi = b >> 4;
310    let lo = b & 0x0F;
311    if hi > 9 || lo > 9 {
312        return None;
313    }
314    Some(hi * 10 + lo)
315}
316
317#[cfg(feature = "chrono")]
318fn mjd_to_ymd(mjd: u16) -> (i32, u32, u32) {
319    // ETSI EN 300 468 Annex C: Y', M', K, Y, M, D via the Zeller-like formula.
320    let mjd = i64::from(mjd);
321    let y_prime = ((mjd as f64 - 15_078.2) / 365.25) as i64;
322    let m_prime = ((mjd as f64 - 14_956.1 - (y_prime as f64 * 365.25).floor()) / 30.6001) as i64;
323    let d = mjd
324        - 14_956
325        - (y_prime as f64 * 365.25).floor() as i64
326        - (m_prime as f64 * 30.6001).floor() as i64;
327    let k = if m_prime == 14 || m_prime == 15 { 1 } else { 0 };
328    let y = y_prime + k + 1900;
329    let m = m_prime - 1 - k * 12;
330    (y as i32, m as u32, d as u32)
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    type TestEvent = (u16, [u8; 5], [u8; 3], u8, bool, Vec<u8>);
338
339    fn build_eit(
340        table_id: u8,
341        service_id: u16,
342        version: u8,
343        tsid: u16,
344        onid: u16,
345        events: &[TestEvent],
346    ) -> Vec<u8> {
347        let ev_bytes: usize = events
348            .iter()
349            .map(|(_, _, _, _, _, d)| EVENT_HEADER_LEN + d.len())
350            .sum();
351        let section_length: u16 =
352            (EXTENSION_HEADER_LEN + POST_EXTENSION_LEN + ev_bytes + CRC_LEN) as u16;
353        let mut v = Vec::new();
354        v.push(table_id);
355        v.push(0xB0 | ((section_length >> 8) as u8 & 0x0F));
356        v.push((section_length & 0xFF) as u8);
357        v.extend_from_slice(&service_id.to_be_bytes());
358        v.push(0xC0 | ((version & 0x1F) << 1) | 0x01);
359        v.push(0);
360        v.push(0);
361        v.extend_from_slice(&tsid.to_be_bytes());
362        v.extend_from_slice(&onid.to_be_bytes());
363        v.push(0);
364        v.push(table_id);
365        for (eid, start, dur, rs, fca, desc) in events {
366            v.extend_from_slice(&eid.to_be_bytes());
367            v.extend_from_slice(start);
368            v.extend_from_slice(dur);
369            let dll = desc.len() as u16;
370            v.push(((*rs & 0x07) << 5) | (u8::from(*fca) << 4) | ((dll >> 8) as u8 & 0x0F));
371            v.push((dll & 0xFF) as u8);
372            v.extend_from_slice(desc);
373        }
374        v.extend_from_slice(&[0, 0, 0, 0]);
375        v
376    }
377
378    #[test]
379    fn parse_pf_actual_and_other_map_to_correct_kind() {
380        for (tid, expected) in [
381            (TABLE_ID_PF_ACTUAL, EitKind::PresentFollowingActual),
382            (TABLE_ID_PF_OTHER, EitKind::PresentFollowingOther),
383        ] {
384            let bytes = build_eit(tid, 1, 0, 0x20, 0x30, &[]);
385            assert_eq!(EitSection::parse(&bytes).unwrap().kind, expected);
386        }
387    }
388
389    #[test]
390    fn schedule_tables_0x50_through_0x5f_all_decode_as_schedule_actual() {
391        for tid in TABLE_ID_SCHEDULE_ACTUAL_FIRST..=TABLE_ID_SCHEDULE_ACTUAL_LAST {
392            let bytes = build_eit(tid, 1, 0, 0x20, 0x30, &[]);
393            assert_eq!(
394                EitSection::parse(&bytes).unwrap().kind,
395                EitKind::ScheduleActual
396            );
397        }
398    }
399
400    #[test]
401    fn schedule_tables_0x60_through_0x6f_all_decode_as_schedule_other() {
402        for tid in TABLE_ID_SCHEDULE_OTHER_FIRST..=TABLE_ID_SCHEDULE_OTHER_LAST {
403            let bytes = build_eit(tid, 1, 0, 0x20, 0x30, &[]);
404            assert_eq!(
405                EitSection::parse(&bytes).unwrap().kind,
406                EitKind::ScheduleOther
407            );
408        }
409    }
410
411    #[test]
412    fn event_loop_with_descriptor_bytes_preserved() {
413        let desc = vec![0x4D, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05];
414        let bytes = build_eit(
415            TABLE_ID_PF_ACTUAL,
416            1,
417            0,
418            0x20,
419            0x30,
420            &[(
421                42,
422                [0xDF, 0xA1, 0x12, 0x34, 0x56],
423                [0x00, 0x30, 0x00],
424                4,
425                false,
426                desc.clone(),
427            )],
428        );
429        let eit = EitSection::parse(&bytes).unwrap();
430        assert_eq!(eit.events.len(), 1);
431        assert_eq!(eit.events[0].event_id, 42);
432        assert_eq!(eit.events[0].descriptors.raw(), &desc[..]);
433    }
434
435    #[test]
436    fn running_status_extracted() {
437        let bytes = build_eit(
438            TABLE_ID_PF_ACTUAL,
439            1,
440            0,
441            0x20,
442            0x30,
443            &[(1, [0; 5], [0; 3], 2, false, vec![])],
444        );
445        assert_eq!(
446            EitSection::parse(&bytes).unwrap().events[0].running_status,
447            2
448        );
449    }
450
451    #[test]
452    fn free_ca_mode_flag_extracted() {
453        let bytes = build_eit(
454            TABLE_ID_PF_ACTUAL,
455            1,
456            0,
457            0x20,
458            0x30,
459            &[(1, [0; 5], [0; 3], 0, true, vec![])],
460        );
461        assert!(EitSection::parse(&bytes).unwrap().events[0].free_ca_mode);
462    }
463
464    #[test]
465    fn serialize_round_trip_preserves_all_events() {
466        let desc1: [u8; 2] = [0x54, 0x00];
467        let eit = EitSection {
468            kind: EitKind::PresentFollowingActual,
469            table_id: TABLE_ID_PF_ACTUAL,
470            service_id: 0x0100,
471            version_number: 3,
472            current_next_indicator: true,
473            section_number: 0,
474            last_section_number: 0,
475            transport_stream_id: 0x1234,
476            original_network_id: 0x0020,
477            segment_last_section_number: 0,
478            last_table_id: TABLE_ID_PF_ACTUAL,
479            events: vec![
480                EitEvent {
481                    event_id: 1,
482                    start_time_raw: [0xDF, 0xA1, 0x12, 0x34, 0x56],
483                    duration_raw: [0x00, 0x30, 0x00],
484                    running_status: 4,
485                    free_ca_mode: false,
486                    descriptors: DescriptorLoop::new(&desc1),
487                },
488                EitEvent {
489                    event_id: 2,
490                    start_time_raw: [0xDF, 0xA1, 0x13, 0x00, 0x00],
491                    duration_raw: [0x01, 0x00, 0x00],
492                    running_status: 1,
493                    free_ca_mode: true,
494                    descriptors: DescriptorLoop::new(&[]),
495                },
496            ],
497        };
498        let mut buf = vec![0u8; eit.serialized_len()];
499        eit.serialize_into(&mut buf).unwrap();
500        let re = EitSection::parse(&buf).unwrap();
501        assert_eq!(eit, re);
502    }
503
504    #[test]
505    fn zero_events_is_valid() {
506        let bytes = build_eit(TABLE_ID_PF_ACTUAL, 1, 0, 0x20, 0x30, &[]);
507        let eit = EitSection::parse(&bytes).unwrap();
508        assert_eq!(eit.events.len(), 0);
509    }
510
511    #[test]
512    #[cfg(feature = "chrono")]
513    fn event_start_time_decodes_to_utc_datetime() {
514        // MJD 59945 is 2023-01-01 per ETSI EN 300 468 Annex C; BCD time 12:34:56.
515        let mjd: u16 = 59945;
516        let ev = EitEvent {
517            event_id: 1,
518            start_time_raw: [(mjd >> 8) as u8, (mjd & 0xFF) as u8, 0x12, 0x34, 0x56],
519            duration_raw: [0, 0, 0],
520            running_status: 0,
521            free_ca_mode: false,
522            descriptors: DescriptorLoop::new(&[]),
523        };
524        let dt = ev.start_time().unwrap();
525        use chrono::Datelike;
526        assert_eq!(dt.year(), 2023);
527        assert_eq!(dt.month(), 1);
528        assert_eq!(dt.day(), 1);
529        use chrono::Timelike;
530        assert_eq!(dt.hour(), 12);
531        assert_eq!(dt.minute(), 34);
532        assert_eq!(dt.second(), 56);
533    }
534}