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}
162dvb_common::impl_spec_display!(SubtitlingType, Reserved);
163
164#[derive(Debug, Clone, PartialEq, Eq)]
166#[cfg_attr(feature = "serde", derive(serde::Serialize))]
167pub struct SubtitlingEntry {
168 pub language_code: LangCode,
170 pub subtitling_type: SubtitlingType,
172 pub composition_page_id: u16,
174 pub ancillary_page_id: u16,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
180#[cfg_attr(feature = "serde", derive(serde::Serialize))]
181pub struct SubtitlingDescriptor {
182 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}