Skip to main content

dvb_si/descriptors/
subtitling.rs

1//! Subtitling Descriptor — ETSI EN 300 468 §6.2.42 (tag 0x59).
2//!
3//! Carried inside PMT's ES_info loop. Enumerates DVB subtitle services:
4//! one entry per 3-char language code + subtitling_type + composition/
5//! ancillary page triple (8 bytes).
6
7use super::descriptor_body;
8use crate::error::{Error, Result};
9use crate::text::LangCode;
10use dvb_common::{Parse, Serialize};
11
12/// Descriptor tag for subtitling_descriptor.
13pub const TAG: u8 = 0x59;
14const HEADER_LEN: usize = 2;
15const ENTRY_LEN: usize = 8;
16
17/// Subtitling type — ETSI EN 300 468 Table 26 (`stream_content = 0x03`).
18///
19/// Wire values `0x10`–`0x2F` are defined per §6.2.41 / Table 26.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize))]
22#[non_exhaustive]
23pub enum SubtitlingType {
24    /// 0x01 — EBU teletext subtitles.
25    EbuTeletextSubtitles,
26    /// 0x02 — associated EBU teletext.
27    AssociatedEbuTeletext,
28    /// 0x03 — VBI data.
29    VbiData,
30    /// 0x10 — DVB subtitles (normal) with no monitor aspect ratio critical.
31    DvbSubtitlesNormal,
32    /// 0x11 — DVB subtitles (normal) for display on 4:3 aspect ratio monitor.
33    DvbSubtitlesNormal4x3,
34    /// 0x12 — DVB subtitles (normal) for display on 16:9 aspect ratio monitor.
35    DvbSubtitlesNormal16x9,
36    /// 0x13 — DVB subtitles (normal) for display on 2.21:1 aspect ratio monitor.
37    DvbSubtitlesNormal2p21x1,
38    /// 0x14 — DVB subtitles (normal) for display on a high definition monitor.
39    DvbSubtitlesNormalHd,
40    /// 0x15 — DVB subtitles (normal), plano-stereoscopic disparity, HD.
41    DvbSubtitlesNormalPlanoStereoscopicHd,
42    /// 0x16 — DVB subtitles (normal) for display on an ultra high definition
43    /// monitor.
44    DvbSubtitlesNormalUhd,
45    /// 0x20 — DVB subtitles (hard of hearing) with no monitor aspect ratio
46    /// critical.
47    DvbSubtitlesHardOfHearing,
48    /// 0x21 — DVB subtitles (hard of hearing) for display on 4:3 aspect ratio
49    /// monitor.
50    DvbSubtitlesHardOfHearing4x3,
51    /// 0x22 — DVB subtitles (hard of hearing) for display on 16:9 aspect ratio
52    /// monitor.
53    DvbSubtitlesHardOfHearing16x9,
54    /// 0x23 — DVB subtitles (hard of hearing) for display on 2.21:1 aspect ratio
55    /// monitor.
56    DvbSubtitlesHardOfHearing2p21x1,
57    /// 0x24 — DVB subtitles (hard of hearing) for display on a high definition
58    /// monitor.
59    DvbSubtitlesHardOfHearingHd,
60    /// 0x25 — DVB subtitles (hard of hearing), plano-stereoscopic disparity, HD.
61    DvbSubtitlesHardOfHearingPlanoStereoscopicHd,
62    /// 0x26 — DVB subtitles (hard of hearing) for display on an ultra high
63    /// definition monitor.
64    DvbSubtitlesHardOfHearingUhd,
65    /// 0x30 — open (in-vision) sign language interpretation for the deaf.
66    OpenSignLanguage,
67    /// 0x31 — closed sign language interpretation for the deaf.
68    ClosedSignLanguage,
69    /// Reserved/unallocated wire value, preserved verbatim for round-trip.
70    Reserved(u8),
71}
72
73impl SubtitlingType {
74    #[must_use]
75    /// Creates a value from a wire byte, preserving every possible
76    /// byte value for lossless round-trip.
77    pub fn from_u8(v: u8) -> Self {
78        match v {
79            0x01 => Self::EbuTeletextSubtitles,
80            0x02 => Self::AssociatedEbuTeletext,
81            0x03 => Self::VbiData,
82            0x10 => Self::DvbSubtitlesNormal,
83            0x11 => Self::DvbSubtitlesNormal4x3,
84            0x12 => Self::DvbSubtitlesNormal16x9,
85            0x13 => Self::DvbSubtitlesNormal2p21x1,
86            0x14 => Self::DvbSubtitlesNormalHd,
87            0x15 => Self::DvbSubtitlesNormalPlanoStereoscopicHd,
88            0x16 => Self::DvbSubtitlesNormalUhd,
89            0x20 => Self::DvbSubtitlesHardOfHearing,
90            0x21 => Self::DvbSubtitlesHardOfHearing4x3,
91            0x22 => Self::DvbSubtitlesHardOfHearing16x9,
92            0x23 => Self::DvbSubtitlesHardOfHearing2p21x1,
93            0x24 => Self::DvbSubtitlesHardOfHearingHd,
94            0x25 => Self::DvbSubtitlesHardOfHearingPlanoStereoscopicHd,
95            0x26 => Self::DvbSubtitlesHardOfHearingUhd,
96            0x30 => Self::OpenSignLanguage,
97            0x31 => Self::ClosedSignLanguage,
98            v => Self::Reserved(v),
99        }
100    }
101
102    #[must_use]
103    /// Returns the wire byte for this value.
104    pub fn to_u8(self) -> u8 {
105        match self {
106            Self::EbuTeletextSubtitles => 0x01,
107            Self::AssociatedEbuTeletext => 0x02,
108            Self::VbiData => 0x03,
109            Self::DvbSubtitlesNormal => 0x10,
110            Self::DvbSubtitlesNormal4x3 => 0x11,
111            Self::DvbSubtitlesNormal16x9 => 0x12,
112            Self::DvbSubtitlesNormal2p21x1 => 0x13,
113            Self::DvbSubtitlesNormalHd => 0x14,
114            Self::DvbSubtitlesNormalPlanoStereoscopicHd => 0x15,
115            Self::DvbSubtitlesNormalUhd => 0x16,
116            Self::DvbSubtitlesHardOfHearing => 0x20,
117            Self::DvbSubtitlesHardOfHearing4x3 => 0x21,
118            Self::DvbSubtitlesHardOfHearing16x9 => 0x22,
119            Self::DvbSubtitlesHardOfHearing2p21x1 => 0x23,
120            Self::DvbSubtitlesHardOfHearingHd => 0x24,
121            Self::DvbSubtitlesHardOfHearingPlanoStereoscopicHd => 0x25,
122            Self::DvbSubtitlesHardOfHearingUhd => 0x26,
123            Self::OpenSignLanguage => 0x30,
124            Self::ClosedSignLanguage => 0x31,
125            Self::Reserved(v) => v,
126        }
127    }
128
129    #[must_use]
130    /// Returns a human-readable spec name for this value.
131    pub fn name(self) -> &'static str {
132        match self {
133            Self::EbuTeletextSubtitles => "EBU teletext subtitles",
134            Self::AssociatedEbuTeletext => "associated EBU teletext",
135            Self::VbiData => "VBI data",
136            Self::DvbSubtitlesNormal => "DVB subtitles (normal), no aspect ratio critical",
137            Self::DvbSubtitlesNormal4x3 => "DVB subtitles (normal), 4:3",
138            Self::DvbSubtitlesNormal16x9 => "DVB subtitles (normal), 16:9",
139            Self::DvbSubtitlesNormal2p21x1 => "DVB subtitles (normal), 2.21:1",
140            Self::DvbSubtitlesNormalHd => "DVB subtitles (normal), HD",
141            Self::DvbSubtitlesNormalPlanoStereoscopicHd => {
142                "DVB subtitles (normal), plano-stereoscopic disparity, HD"
143            }
144            Self::DvbSubtitlesNormalUhd => "DVB subtitles (normal), UHD",
145            Self::DvbSubtitlesHardOfHearing => {
146                "DVB subtitles (hard of hearing), no aspect ratio critical"
147            }
148            Self::DvbSubtitlesHardOfHearing4x3 => "DVB subtitles (hard of hearing), 4:3",
149            Self::DvbSubtitlesHardOfHearing16x9 => "DVB subtitles (hard of hearing), 16:9",
150            Self::DvbSubtitlesHardOfHearing2p21x1 => "DVB subtitles (hard of hearing), 2.21:1",
151            Self::DvbSubtitlesHardOfHearingHd => "DVB subtitles (hard of hearing), HD",
152            Self::DvbSubtitlesHardOfHearingPlanoStereoscopicHd => {
153                "DVB subtitles (hard of hearing), plano-stereoscopic disparity, HD"
154            }
155            Self::DvbSubtitlesHardOfHearingUhd => "DVB subtitles (hard of hearing), UHD",
156            Self::OpenSignLanguage => "open (in-vision) sign language interpretation",
157            Self::ClosedSignLanguage => "closed sign language interpretation",
158            Self::Reserved(_) => "reserved",
159        }
160    }
161}
162
163/// One subtitling component.
164#[derive(Debug, Clone, PartialEq, Eq)]
165#[cfg_attr(feature = "serde", derive(serde::Serialize))]
166pub struct SubtitlingEntry {
167    /// ISO 639-2 language code.
168    pub language_code: LangCode,
169    /// subtitling_type (ETSI EN 300 468 Table 26, `stream_content = 0x03`).
170    pub subtitling_type: SubtitlingType,
171    /// composition_page_id.
172    pub composition_page_id: u16,
173    /// ancillary_page_id.
174    pub ancillary_page_id: u16,
175}
176
177/// Subtitling Descriptor.
178#[derive(Debug, Clone, PartialEq, Eq)]
179#[cfg_attr(feature = "serde", derive(serde::Serialize))]
180pub struct SubtitlingDescriptor {
181    /// Entries in wire order.
182    pub entries: Vec<SubtitlingEntry>,
183}
184
185impl<'a> Parse<'a> for SubtitlingDescriptor {
186    type Error = crate::error::Error;
187    fn parse(bytes: &'a [u8]) -> Result<Self> {
188        let body = descriptor_body(
189            bytes,
190            TAG,
191            "SubtitlingDescriptor",
192            "unexpected tag for subtitling_descriptor",
193        )?;
194        if body.len() % ENTRY_LEN != 0 {
195            return Err(Error::InvalidDescriptor {
196                tag: TAG,
197                reason: "subtitling_descriptor length must be a multiple of 8",
198            });
199        }
200        let mut entries = Vec::with_capacity(body.len() / ENTRY_LEN);
201        for chunk in body.chunks_exact(ENTRY_LEN) {
202            entries.push(SubtitlingEntry {
203                language_code: LangCode([chunk[0], chunk[1], chunk[2]]),
204                subtitling_type: SubtitlingType::from_u8(chunk[3]),
205                composition_page_id: u16::from_be_bytes([chunk[4], chunk[5]]),
206                ancillary_page_id: u16::from_be_bytes([chunk[6], chunk[7]]),
207            });
208        }
209        Ok(Self { entries })
210    }
211}
212
213impl Serialize for SubtitlingDescriptor {
214    type Error = crate::error::Error;
215    fn serialized_len(&self) -> usize {
216        HEADER_LEN + self.entries.len() * ENTRY_LEN
217    }
218
219    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
220        let len = self.serialized_len();
221        if buf.len() < len {
222            return Err(Error::OutputBufferTooSmall {
223                need: len,
224                have: buf.len(),
225            });
226        }
227        buf[0] = TAG;
228        buf[1] = (self.entries.len() * ENTRY_LEN) as u8;
229        let mut pos = HEADER_LEN;
230        for e in &self.entries {
231            buf[pos..pos + 3].copy_from_slice(&e.language_code.0);
232            buf[pos + 3] = e.subtitling_type.to_u8();
233            buf[pos + 4..pos + 6].copy_from_slice(&e.composition_page_id.to_be_bytes());
234            buf[pos + 6..pos + 8].copy_from_slice(&e.ancillary_page_id.to_be_bytes());
235            pos += ENTRY_LEN;
236        }
237        Ok(len)
238    }
239}
240impl<'a> crate::traits::DescriptorDef<'a> for SubtitlingDescriptor {
241    const TAG: u8 = TAG;
242    const NAME: &'static str = "SUBTITLING";
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn parse_single_entry() {
251        let bytes = [TAG, 8, b'e', b'n', b'g', 0x10, 0x00, 0x01, 0x00, 0x02];
252        let d = SubtitlingDescriptor::parse(&bytes).unwrap();
253        assert_eq!(d.entries.len(), 1);
254        assert_eq!(d.entries[0].language_code, LangCode(*b"eng"));
255        assert_eq!(
256            d.entries[0].subtitling_type,
257            SubtitlingType::DvbSubtitlesNormal
258        );
259        assert_eq!(d.entries[0].composition_page_id, 1);
260        assert_eq!(d.entries[0].ancillary_page_id, 2);
261    }
262
263    #[test]
264    fn parse_rejects_wrong_tag() {
265        assert!(matches!(
266            SubtitlingDescriptor::parse(&[0x5A, 0]).unwrap_err(),
267            Error::InvalidDescriptor { tag: 0x5A, .. }
268        ));
269    }
270
271    #[test]
272    fn parse_rejects_length_not_multiple_of_8() {
273        let bytes = [TAG, 7, 0, 0, 0, 0, 0, 0, 0];
274        assert!(matches!(
275            SubtitlingDescriptor::parse(&bytes).unwrap_err(),
276            Error::InvalidDescriptor { .. }
277        ));
278    }
279
280    #[test]
281    fn serialize_round_trip() {
282        let d = SubtitlingDescriptor {
283            entries: vec![
284                SubtitlingEntry {
285                    language_code: LangCode(*b"fra"),
286                    subtitling_type: SubtitlingType::DvbSubtitlesNormal,
287                    composition_page_id: 0x1234,
288                    ancillary_page_id: 0x5678,
289                },
290                SubtitlingEntry {
291                    language_code: LangCode(*b"deu"),
292                    subtitling_type: SubtitlingType::DvbSubtitlesHardOfHearing,
293                    composition_page_id: 0,
294                    ancillary_page_id: 0,
295                },
296            ],
297        };
298        let mut buf = vec![0u8; d.serialized_len()];
299        d.serialize_into(&mut buf).unwrap();
300        assert_eq!(SubtitlingDescriptor::parse(&buf).unwrap(), d);
301    }
302
303    #[test]
304    fn empty_descriptor_valid() {
305        let d = SubtitlingDescriptor::parse(&[TAG, 0]).unwrap();
306        assert_eq!(d.entries.len(), 0);
307    }
308
309    #[test]
310    fn subtitling_type_full_range_round_trip() {
311        for b in 0..=0xFF_u8 {
312            let st = SubtitlingType::from_u8(b);
313            assert_eq!(st.to_u8(), b, "round-trip failed for byte 0x{b:02X}");
314        }
315    }
316
317    #[test]
318    fn subtitling_type_name_for_known() {
319        assert_eq!(
320            SubtitlingType::EbuTeletextSubtitles.name(),
321            "EBU teletext subtitles"
322        );
323        assert_eq!(
324            SubtitlingType::DvbSubtitlesNormal.name(),
325            "DVB subtitles (normal), no aspect ratio critical"
326        );
327        assert_eq!(
328            SubtitlingType::DvbSubtitlesNormalUhd.name(),
329            "DVB subtitles (normal), UHD"
330        );
331        assert_eq!(
332            SubtitlingType::DvbSubtitlesHardOfHearingUhd.name(),
333            "DVB subtitles (hard of hearing), UHD"
334        );
335        assert_eq!(
336            SubtitlingType::OpenSignLanguage.name(),
337            "open (in-vision) sign language interpretation"
338        );
339        assert_eq!(
340            SubtitlingType::ClosedSignLanguage.name(),
341            "closed sign language interpretation"
342        );
343        assert_eq!(SubtitlingType::Reserved(0x50).name(), "reserved");
344    }
345
346    #[test]
347    fn subtitling_type_round_trip_0x15_0x25() {
348        assert_eq!(SubtitlingType::from_u8(0x15).to_u8(), 0x15);
349        assert_eq!(SubtitlingType::from_u8(0x25).to_u8(), 0x25);
350    }
351}