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