1use super::descriptor_body;
8use crate::error::{Error, Result};
9use crate::text::LangCode;
10use dvb_common::{Parse, Serialize};
11
12pub const TAG: u8 = 0x59;
14const HEADER_LEN: usize = 2;
15const ENTRY_LEN: usize = 8;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize))]
22#[non_exhaustive]
23pub enum SubtitlingType {
24 EbuTeletextSubtitles,
26 AssociatedEbuTeletext,
28 VbiData,
30 DvbSubtitlesNormal,
32 DvbSubtitlesNormal4x3,
34 DvbSubtitlesNormal16x9,
36 DvbSubtitlesNormal2p21x1,
38 DvbSubtitlesNormalHd,
40 DvbSubtitlesNormalPlanoStereoscopicHd,
42 DvbSubtitlesNormalUhd,
45 DvbSubtitlesHardOfHearing,
48 DvbSubtitlesHardOfHearing4x3,
51 DvbSubtitlesHardOfHearing16x9,
54 DvbSubtitlesHardOfHearing2p21x1,
57 DvbSubtitlesHardOfHearingHd,
60 DvbSubtitlesHardOfHearingPlanoStereoscopicHd,
62 DvbSubtitlesHardOfHearingUhd,
65 OpenSignLanguage,
67 ClosedSignLanguage,
69 Reserved(u8),
71}
72
73impl SubtitlingType {
74 #[must_use]
75 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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
165#[cfg_attr(feature = "serde", derive(serde::Serialize))]
166pub struct SubtitlingEntry {
167 pub language_code: LangCode,
169 pub subtitling_type: SubtitlingType,
171 pub composition_page_id: u16,
173 pub ancillary_page_id: u16,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
179#[cfg_attr(feature = "serde", derive(serde::Serialize))]
180pub struct SubtitlingDescriptor {
181 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}