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