Skip to main content

vsd_mp4/sub/
wvtt.rs

1/*
2    REFERENCES
3    ----------
4
5    1. https://github.com/shaka-project/shaka-player/blob/a4e926772e1b754fe968ee6f97490f08a40fe535/lib/text/mp4_vtt_parser.js
6
7*/
8
9//! WebVTT subtitle parser for MP4 container streams.
10
11use crate::{
12    Error, Reader, Result, bail,
13    boxes::{MdhdBox, TfdtBox, TfhdBox, TrunBox, TrunSample},
14    data,
15    parser::{self, Mp4Parser},
16    sub::builder::{Cue, Subtitles},
17};
18use std::{cell::RefCell, rc::Rc};
19
20/// A parser for extracting WebVTT (VTT) subtitles from MP4 files.
21pub struct WvttSubsParser {
22    pub timescale: u32,
23}
24
25impl WvttSubsParser {
26    /// Creates a new `Mp4VttParser` from the given initialization segment.
27    pub fn from_init(data: &[u8]) -> Result<Self> {
28        let saw_wvtt = data!(false);
29        let timescale = data!();
30
31        let saw_wvtt_c = saw_wvtt.clone();
32        let timescale_c = timescale.clone();
33
34        Mp4Parser::new()
35            .base_box("moov", parser::children)
36            .base_box("trak", parser::children)
37            .base_box("mdia", parser::children)
38            .full_box("mdhd", move |mut box_| {
39                let box_version = box_.version.unwrap();
40
41                if box_version != 0 && box_version != 1 {
42                    bail!("MDHD box version can only be 0 or 1.");
43                }
44
45                let mdhd_box = MdhdBox::new(&mut box_)?;
46                *timescale_c.borrow_mut() = Some(mdhd_box.timescale);
47                Ok(())
48            })
49            .base_box("minf", parser::children)
50            .base_box("stbl", parser::children)
51            .full_box("stsd", parser::sample_description)
52            .base_box("wvtt", move |_| {
53                // A valid vtt init segment, though we have no actual subtitles yet.
54                *saw_wvtt_c.borrow_mut() = true;
55                Ok(())
56            })
57            .parse(data, false, false)?;
58
59        if !saw_wvtt.take() {
60            bail!("WVTT box not found.");
61        }
62
63        if let Some(timescale) = timescale.take() {
64            Ok(Self { timescale })
65        } else {
66            Err(Error::Other(
67                "Missing timescale (should exist inside MDHD box).".to_owned(),
68            ))
69        }
70    }
71
72    /// Parses the given media segment data to extract subtitles.
73    pub fn parse(&self, data: &[u8], period_start: Option<f32>) -> Result<Subtitles> {
74        let period_start = period_start.unwrap_or_default();
75        let timescale = self.timescale;
76
77        let base_time = data!(0_u64);
78        let default_duration = data!();
79        let presentations = data!(Vec::new());
80        let saw_tfdt = data!(false);
81        let saw_trun = data!(false);
82        let subtitles = data!(Subtitles::new());
83
84        let base_time_c = base_time.clone();
85        let default_duration_c = default_duration.clone();
86        let presentations_c = presentations.clone();
87        let saw_tfdt_c = saw_tfdt.clone();
88        let saw_trun_c = saw_trun.clone();
89        let subtitles_c = subtitles.clone();
90
91        Mp4Parser::new()
92            .base_box("moof", parser::children)
93            .base_box("traf", parser::children)
94            .full_box("tfdt", move |mut box_| {
95                *saw_tfdt_c.borrow_mut() = true;
96
97                let box_version = box_.version.unwrap();
98
99                if box_version != 0 && box_version != 1 {
100                    bail!("TFDT box version can only be 0 or 1.");
101                }
102
103                let tfdt_box = TfdtBox::new(&mut box_)?;
104                *base_time_c.borrow_mut() = tfdt_box.base_media_decode_time;
105                Ok(())
106            })
107            .full_box("tfhd", move |mut box_| {
108                if box_.flags.is_none() {
109                    bail!("TFHD box should have a valid flags value.");
110                }
111
112                let tfhd_box = TfhdBox::new(&mut box_)?;
113                *default_duration_c.borrow_mut() = tfhd_box.default_sample_duration;
114                Ok(())
115            })
116            .full_box("trun", move |mut box_| {
117                *saw_trun_c.borrow_mut() = true;
118
119                if box_.version.is_none() {
120                    bail!("TRUN box should have a valid version value.");
121                }
122
123                if box_.flags.is_none() {
124                    bail!("TRUN box should have a valid flags value.");
125                }
126
127                let trun_box = TrunBox::new(&mut box_)?;
128                *presentations_c.borrow_mut() = trun_box.sample_data;
129                Ok(())
130            })
131            .base_box(
132                "mdat",
133                parser::alldata(move |data| {
134                    if !*saw_tfdt.borrow() && !*saw_trun.borrow() {
135                        bail!("Some required boxes (either TFDT or TRUN) are missing.");
136                    }
137
138                    let cues = Self::parse_mdat(
139                        *base_time.borrow(),
140                        *default_duration.borrow(),
141                        period_start,
142                        &presentations.borrow(),
143                        &data,
144                        timescale,
145                    )?;
146                    subtitles_c.borrow_mut().extend_cues(cues);
147                    Ok(())
148                }),
149            )
150            .parse(data, false, false)?;
151
152        Ok(subtitles.take())
153    }
154
155    fn parse_mdat(
156        base_time: u64,
157        default_duration: Option<u32>,
158        period_start: f32,
159        presentations: &[TrunSample],
160        raw_payload: &[u8],
161        timescale: u32,
162    ) -> Result<Vec<Cue>> {
163        let mut cues = Vec::new();
164        let mut current_time = base_time;
165        let mut reader = Reader::new_big_endian(raw_payload);
166
167        for presentation in presentations {
168            // If one presentation corresponds to multiple payloads, it is assumed
169            // that all of those payloads have the same start time and duration.
170            let duration = presentation.sample_duration.or(default_duration);
171            let start_time = if let Some(sample_composition_time_offset) =
172                presentation.sample_composition_time_offset
173            {
174                base_time + sample_composition_time_offset as u64
175            } else {
176                current_time
177            };
178
179            current_time = start_time + duration.unwrap_or_default() as u64;
180
181            // Read samples until it adds up to the given size.
182            let mut total_size = 0;
183            loop {
184                // Read the payload size.
185                let payload_size = reader.read_u32()? as i32;
186                total_size += payload_size;
187
188                // Skip the type.
189                let payload_type = reader.read_u32()?;
190                let payload_name = parser::type_to_string(payload_type as usize)?;
191
192                // Read the data payload.
193                let mut payload = None;
194
195                match payload_name.as_str() {
196                    "vttc" => {
197                        if payload_size > 8 {
198                            payload = Some(reader.read_bytes_u8((payload_size - 8) as usize)?);
199                        }
200                    }
201                    "vtte" => {
202                        // It's a vtte, which is a vtt cue that is empty. Ignore any data that
203                        // does exist.
204                        reader.skip((payload_size - 8) as u64)?;
205                    }
206                    _ => {
207                        // println!("Unknown box {} ! Skipping!", payload_name);
208                        reader.skip((payload_size - 8) as u64)?;
209                    }
210                }
211
212                if duration.is_some() {
213                    if let Some(payload) = payload {
214                        let cue = Self::parse_vttc(
215                            &payload,
216                            period_start + start_time as f32 / timescale as f32,
217                            period_start + current_time as f32 / timescale as f32,
218                        )?;
219                        cues.push(cue);
220                    }
221                } else {
222                    bail!("WVTT sample duration unknown, and no default found.");
223                }
224
225                if !(presentation.sample_size.is_none()
226                    || total_size <= presentation.sample_size.unwrap_or_default() as i32)
227                {
228                    bail!(
229                        "The samples do not fit evenly into the sample sizes given in the TRUN box."
230                    );
231                };
232
233                // If no sampleSize was specified, it's assumed that this presentation
234                // corresponds to only a single cue.
235                if !(presentation.sample_size.is_some()
236                    && (total_size < presentation.sample_size.unwrap_or_default() as i32))
237                {
238                    break;
239                }
240            }
241        }
242
243        if reader.has_more_data() {
244            bail!("MDAT which contain VTT cues and non-VTT data are not currently supported.");
245        };
246
247        Ok(cues.into_iter().flatten().collect())
248    }
249
250    /// Parses a vttc box into a cue.
251    fn parse_vttc(data: &[u8], start_time: f32, end_time: f32) -> Result<Option<Cue>> {
252        let payload = Rc::new(RefCell::new(String::new()));
253        let settings = Rc::new(RefCell::new(String::new()));
254
255        let payload_c = payload.clone();
256        let settings_c = settings.clone();
257
258        Mp4Parser::new()
259            .base_box(
260                "payl",
261                parser::alldata(move |data| {
262                    *payload_c.borrow_mut() = String::from_utf8(data)?;
263                    Ok(())
264                }),
265            )
266            // .base_box(
267            //     "iden",
268            //     parser::alldata(move |data| {
269            //         *id_c.borrow_mut() = String::from_utf8(data)?;
270            //         Ok(())
271            //     }),
272            // )
273            .base_box(
274                "sttg",
275                parser::alldata(move |data| {
276                    *settings_c.borrow_mut() = String::from_utf8(data)?;
277                    Ok(())
278                }),
279            )
280            .parse(data, false, false)?;
281
282        let payload = payload.take();
283
284        if !payload.is_empty() {
285            return Ok(Some(Cue {
286                payload,
287                settings: settings.take(),
288                start_time,
289                end_time,
290            }));
291        }
292
293        Ok(None)
294    }
295}