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 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 entries.push(SubtitlingEntry {
205 language_code: LangCode([chunk[0], chunk[1], chunk[2]]),
206 subtitling_type: SubtitlingType::from_u8(chunk[3]),
207 composition_page_id: u16::from_be_bytes([chunk[4], chunk[5]]),
208 ancillary_page_id: u16::from_be_bytes([chunk[6], chunk[7]]),
209 });
210 }
211 Ok(Self { entries })
212 }
213}
214
215impl Serialize for SubtitlingDescriptor {
216 type Error = crate::error::Error;
217 fn serialized_len(&self) -> usize {
218 HEADER_LEN + self.entries.len() * ENTRY_LEN
219 }
220
221 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
222 let len = self.serialized_len();
223 if buf.len() < len {
224 return Err(Error::OutputBufferTooSmall {
225 need: len,
226 have: buf.len(),
227 });
228 }
229 buf[0] = TAG;
230 buf[1] = (self.entries.len() * ENTRY_LEN) as u8;
231 let mut pos = HEADER_LEN;
232 for e in &self.entries {
233 buf[pos..pos + 3].copy_from_slice(&e.language_code.0);
234 buf[pos + 3] = e.subtitling_type.to_u8();
235 buf[pos + 4..pos + 6].copy_from_slice(&e.composition_page_id.to_be_bytes());
236 buf[pos + 6..pos + 8].copy_from_slice(&e.ancillary_page_id.to_be_bytes());
237 pos += ENTRY_LEN;
238 }
239 Ok(len)
240 }
241}
242impl<'a> crate::traits::DescriptorDef<'a> for SubtitlingDescriptor {
243 const TAG: u8 = TAG;
244 const NAME: &'static str = "SUBTITLING";
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn parse_single_entry() {
253 let bytes = [TAG, 8, b'e', b'n', b'g', 0x10, 0x00, 0x01, 0x00, 0x02];
254 let d = SubtitlingDescriptor::parse(&bytes).unwrap();
255 assert_eq!(d.entries.len(), 1);
256 assert_eq!(d.entries[0].language_code, LangCode(*b"eng"));
257 assert_eq!(
258 d.entries[0].subtitling_type,
259 SubtitlingType::DvbSubtitlesNormal
260 );
261 assert_eq!(d.entries[0].composition_page_id, 1);
262 assert_eq!(d.entries[0].ancillary_page_id, 2);
263 }
264
265 #[test]
266 fn parse_rejects_wrong_tag() {
267 assert!(matches!(
268 SubtitlingDescriptor::parse(&[0x5A, 0]).unwrap_err(),
269 Error::InvalidDescriptor { tag: 0x5A, .. }
270 ));
271 }
272
273 #[test]
274 fn parse_rejects_length_not_multiple_of_8() {
275 let bytes = [TAG, 7, 0, 0, 0, 0, 0, 0, 0];
276 assert!(matches!(
277 SubtitlingDescriptor::parse(&bytes).unwrap_err(),
278 Error::InvalidDescriptor { .. }
279 ));
280 }
281
282 #[test]
283 fn serialize_round_trip() {
284 let d = SubtitlingDescriptor {
285 entries: vec![
286 SubtitlingEntry {
287 language_code: LangCode(*b"fra"),
288 subtitling_type: SubtitlingType::DvbSubtitlesNormal,
289 composition_page_id: 0x1234,
290 ancillary_page_id: 0x5678,
291 },
292 SubtitlingEntry {
293 language_code: LangCode(*b"deu"),
294 subtitling_type: SubtitlingType::DvbSubtitlesHardOfHearing,
295 composition_page_id: 0,
296 ancillary_page_id: 0,
297 },
298 ],
299 };
300 let mut buf = vec![0u8; d.serialized_len()];
301 d.serialize_into(&mut buf).unwrap();
302 assert_eq!(SubtitlingDescriptor::parse(&buf).unwrap(), d);
303 }
304
305 #[test]
306 fn empty_descriptor_valid() {
307 let d = SubtitlingDescriptor::parse(&[TAG, 0]).unwrap();
308 assert_eq!(d.entries.len(), 0);
309 }
310
311 #[test]
312 fn subtitling_type_full_range_round_trip() {
313 for b in 0..=0xFF_u8 {
314 let st = SubtitlingType::from_u8(b);
315 assert_eq!(st.to_u8(), b, "round-trip failed for byte 0x{b:02X}");
316 }
317 }
318
319 #[test]
320 fn subtitling_type_name_for_known() {
321 assert_eq!(
322 SubtitlingType::EbuTeletextSubtitles.name(),
323 "EBU teletext subtitles"
324 );
325 assert_eq!(
326 SubtitlingType::DvbSubtitlesNormal.name(),
327 "DVB subtitles (normal), no aspect ratio critical"
328 );
329 assert_eq!(
330 SubtitlingType::DvbSubtitlesNormalUhd.name(),
331 "DVB subtitles (normal), UHD"
332 );
333 assert_eq!(
334 SubtitlingType::DvbSubtitlesHardOfHearingUhd.name(),
335 "DVB subtitles (hard of hearing), UHD"
336 );
337 assert_eq!(
338 SubtitlingType::OpenSignLanguage.name(),
339 "open (in-vision) sign language interpretation"
340 );
341 assert_eq!(
342 SubtitlingType::ClosedSignLanguage.name(),
343 "closed sign language interpretation"
344 );
345 assert_eq!(SubtitlingType::Reserved(0x50).name(), "reserved");
346 }
347
348 #[test]
349 fn subtitling_type_round_trip_0x15_0x25() {
350 assert_eq!(SubtitlingType::from_u8(0x15).to_u8(), 0x15);
351 assert_eq!(SubtitlingType::from_u8(0x25).to_u8(), 0x25);
352 }
353}