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}
162dvb_common::impl_spec_display!(SubtitlingType, Reserved);
163
164/// One subtitling component.
165#[derive(Debug, Clone, PartialEq, Eq)]
166#[cfg_attr(feature = "serde", derive(serde::Serialize))]
167pub struct SubtitlingEntry {
168    /// ISO 639-2 language code.
169    pub language_code: LangCode,
170    /// subtitling_type (ETSI EN 300 468 Table 26, `stream_content = 0x03`).
171    pub subtitling_type: SubtitlingType,
172    /// composition_page_id.
173    pub composition_page_id: u16,
174    /// ancillary_page_id.
175    pub ancillary_page_id: u16,
176}
177
178/// Subtitling Descriptor.
179#[derive(Debug, Clone, PartialEq, Eq)]
180#[cfg_attr(feature = "serde", derive(serde::Serialize))]
181pub struct SubtitlingDescriptor {
182    /// Entries in wire order.
183    pub entries: Vec<SubtitlingEntry>,
184}
185
186impl<'a> Parse<'a> for SubtitlingDescriptor {
187    type Error = crate::error::Error;
188    fn parse(bytes: &'a [u8]) -> Result<Self> {
189        let body = descriptor_body(
190            bytes,
191            TAG,
192            "SubtitlingDescriptor",
193            "unexpected tag for subtitling_descriptor",
194        )?;
195        if body.len() % ENTRY_LEN != 0 {
196            return Err(Error::InvalidDescriptor {
197                tag: TAG,
198                reason: "subtitling_descriptor length must be a multiple of 8",
199            });
200        }
201        let mut entries = Vec::with_capacity(body.len() / ENTRY_LEN);
202        for chunk in body.chunks_exact(ENTRY_LEN) {
203            entries.push(SubtitlingEntry {
204                language_code: LangCode([chunk[0], chunk[1], chunk[2]]),
205                subtitling_type: SubtitlingType::from_u8(chunk[3]),
206                composition_page_id: u16::from_be_bytes([chunk[4], chunk[5]]),
207                ancillary_page_id: u16::from_be_bytes([chunk[6], chunk[7]]),
208            });
209        }
210        Ok(Self { entries })
211    }
212}
213
214impl Serialize for SubtitlingDescriptor {
215    type Error = crate::error::Error;
216    fn serialized_len(&self) -> usize {
217        HEADER_LEN + self.entries.len() * ENTRY_LEN
218    }
219
220    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
221        let len = self.serialized_len();
222        if buf.len() < len {
223            return Err(Error::OutputBufferTooSmall {
224                need: len,
225                have: buf.len(),
226            });
227        }
228        buf[0] = TAG;
229        buf[1] = (self.entries.len() * ENTRY_LEN) as u8;
230        let mut pos = HEADER_LEN;
231        for e in &self.entries {
232            buf[pos..pos + 3].copy_from_slice(&e.language_code.0);
233            buf[pos + 3] = e.subtitling_type.to_u8();
234            buf[pos + 4..pos + 6].copy_from_slice(&e.composition_page_id.to_be_bytes());
235            buf[pos + 6..pos + 8].copy_from_slice(&e.ancillary_page_id.to_be_bytes());
236            pos += ENTRY_LEN;
237        }
238        Ok(len)
239    }
240}
241impl<'a> crate::traits::DescriptorDef<'a> for SubtitlingDescriptor {
242    const TAG: u8 = TAG;
243    const NAME: &'static str = "SUBTITLING";
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn parse_single_entry() {
252        let bytes = [TAG, 8, b'e', b'n', b'g', 0x10, 0x00, 0x01, 0x00, 0x02];
253        let d = SubtitlingDescriptor::parse(&bytes).unwrap();
254        assert_eq!(d.entries.len(), 1);
255        assert_eq!(d.entries[0].language_code, LangCode(*b"eng"));
256        assert_eq!(
257            d.entries[0].subtitling_type,
258            SubtitlingType::DvbSubtitlesNormal
259        );
260        assert_eq!(d.entries[0].composition_page_id, 1);
261        assert_eq!(d.entries[0].ancillary_page_id, 2);
262    }
263
264    #[test]
265    fn parse_rejects_wrong_tag() {
266        assert!(matches!(
267            SubtitlingDescriptor::parse(&[0x5A, 0]).unwrap_err(),
268            Error::InvalidDescriptor { tag: 0x5A, .. }
269        ));
270    }
271
272    #[test]
273    fn parse_rejects_length_not_multiple_of_8() {
274        let bytes = [TAG, 7, 0, 0, 0, 0, 0, 0, 0];
275        assert!(matches!(
276            SubtitlingDescriptor::parse(&bytes).unwrap_err(),
277            Error::InvalidDescriptor { .. }
278        ));
279    }
280
281    #[test]
282    fn serialize_round_trip() {
283        let d = SubtitlingDescriptor {
284            entries: vec![
285                SubtitlingEntry {
286                    language_code: LangCode(*b"fra"),
287                    subtitling_type: SubtitlingType::DvbSubtitlesNormal,
288                    composition_page_id: 0x1234,
289                    ancillary_page_id: 0x5678,
290                },
291                SubtitlingEntry {
292                    language_code: LangCode(*b"deu"),
293                    subtitling_type: SubtitlingType::DvbSubtitlesHardOfHearing,
294                    composition_page_id: 0,
295                    ancillary_page_id: 0,
296                },
297            ],
298        };
299        let mut buf = vec![0u8; d.serialized_len()];
300        d.serialize_into(&mut buf).unwrap();
301        assert_eq!(SubtitlingDescriptor::parse(&buf).unwrap(), d);
302    }
303
304    #[test]
305    fn empty_descriptor_valid() {
306        let d = SubtitlingDescriptor::parse(&[TAG, 0]).unwrap();
307        assert_eq!(d.entries.len(), 0);
308    }
309
310    #[test]
311    fn subtitling_type_full_range_round_trip() {
312        for b in 0..=0xFF_u8 {
313            let st = SubtitlingType::from_u8(b);
314            assert_eq!(st.to_u8(), b, "round-trip failed for byte 0x{b:02X}");
315        }
316    }
317
318    #[test]
319    fn subtitling_type_name_for_known() {
320        assert_eq!(
321            SubtitlingType::EbuTeletextSubtitles.name(),
322            "EBU teletext subtitles"
323        );
324        assert_eq!(
325            SubtitlingType::DvbSubtitlesNormal.name(),
326            "DVB subtitles (normal), no aspect ratio critical"
327        );
328        assert_eq!(
329            SubtitlingType::DvbSubtitlesNormalUhd.name(),
330            "DVB subtitles (normal), UHD"
331        );
332        assert_eq!(
333            SubtitlingType::DvbSubtitlesHardOfHearingUhd.name(),
334            "DVB subtitles (hard of hearing), UHD"
335        );
336        assert_eq!(
337            SubtitlingType::OpenSignLanguage.name(),
338            "open (in-vision) sign language interpretation"
339        );
340        assert_eq!(
341            SubtitlingType::ClosedSignLanguage.name(),
342            "closed sign language interpretation"
343        );
344        assert_eq!(SubtitlingType::Reserved(0x50).name(), "reserved");
345    }
346
347    #[test]
348    fn subtitling_type_round_trip_0x15_0x25() {
349        assert_eq!(SubtitlingType::from_u8(0x15).to_u8(), 0x15);
350        assert_eq!(SubtitlingType::from_u8(0x25).to_u8(), 0x25);
351    }
352}