Skip to main content

frame_source/
srt_reader.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use std::{io::Read, time::Duration};
5
6use crate::Result;
7
8use winnow::{
9    BStr,
10    ascii::{dec_uint, digit1, line_ending},
11    combinator::{eof, opt, seq, terminated, trace},
12    error::{ContextError, ErrMode, InputError},
13    prelude::*,
14    token::{take, take_until, take_while},
15};
16
17fn parse_digits<'s>(input: &mut &'s BStr) -> ModalResult<u64> {
18    trace("parse_digits", move |input: &mut &'s BStr| {
19        digit1
20            .parse_to()
21            .parse_next(input)
22            .map_err(|_e: ErrMode<InputError<&'s BStr>>| ErrMode::Cut(ContextError::new()))
23    })
24    .parse_next(input)
25}
26
27fn parse_duration(input: &mut &BStr) -> ModalResult<Duration> {
28    trace("parse_duration", move |input: &mut &BStr| {
29        let (hours, _, minutes, _, seconds, _, millis) = (
30            take(2usize),
31            ':',
32            take(2usize),
33            ':',
34            take(2usize),
35            ',',
36            take(3usize),
37        )
38            .parse_next(input)?;
39
40        let hours: u64 = parse_digits(&mut hours.into())?;
41        let minutes: u64 = parse_digits(&mut minutes.into())?;
42        let seconds: u64 = parse_digits(&mut seconds.into())?;
43        let millis: u64 = parse_digits(&mut millis.into())?;
44
45        let minutes = hours * 60 + minutes;
46        let secs = 60 * minutes + seconds;
47        let nanos = millis * 1_000_000;
48        Ok(Duration::new(secs, nanos.try_into().unwrap()))
49    })
50    .parse_next(input)
51}
52
53#[rustfmt::skip]
54#[derive(Debug)]
55pub struct Stanza {
56    pub(crate) _count: usize,
57    pub(crate) _start: std::time::Duration,
58    pub(crate) _stop: std::time::Duration,
59    pub(crate) lines: String,
60}
61
62impl Stanza {
63    pub fn lines(&self) -> &str {
64        &self.lines
65    }
66}
67
68fn parse_stanza(input: &mut &BStr) -> ModalResult<Stanza> {
69    trace("parse_stanza", move |input: &mut &BStr| {
70        let mut num = dec_uint::<_, usize, ContextError>;
71
72        // first line: count
73        let count_res: winnow::Result<(usize,)> = seq!(num, _: line_ending).parse_next(input);
74        let count = count_res.map_err(ErrMode::Cut)?.0;
75
76        // "00:00:00,100 --> 00:00:00,210"
77        let start_stop_res: ModalResult<(Duration, Duration)> =
78            seq!(parse_duration, _: " --> ", parse_duration, _: line_ending).parse_next(input);
79        let (start, stop) = start_stop_res?;
80
81        // TODO: match against two `line_ending`s (rather than only '\n')
82        let till_newlines = take_until(0.., "\n\n");
83
84        let res: ModalResult<&[u8]> = match opt(till_newlines).parse_next(input)? {
85            Some(lines0) => {
86                // Clear one trailing newline. (Leave other as stanza seperator.)
87                "\n".parse_next(input)?;
88                Ok(lines0)
89            }
90            _ => {
91                // We reached EOF
92                terminated(take_while(0.., |_| true), eof).parse_next(input)
93            }
94        };
95        let lines0 = res?;
96        let lines =
97            String::from_utf8(lines0.to_vec()).map_err(|_e| ErrMode::Cut(ContextError::new()))?;
98
99        Ok(Stanza {
100            _count: count,
101            _start: start,
102            _stop: stop,
103            lines,
104        })
105    })
106    .parse_next(input)
107}
108
109/// Parse as many stanzas as possible from `input`, stopping early (without
110/// erroring) at the first stanza that fails to parse. Whatever stanzas parsed
111/// cleanly are returned; the caller can tell whether parsing stopped early by
112/// checking if `input` still has bytes left afterwards.
113fn parse_stanzas(input: &mut &BStr) -> Vec<Stanza> {
114    let mut result = vec![];
115    loop {
116        match opt(eof::<_, ContextError>).parse_next(input) {
117            Ok(Some(_)) | Err(_) => break,
118            Ok(None) => {}
119        }
120        match parse_stanza.parse_next(input) {
121            Ok(x) => result.push(x),
122            Err(_) => break,
123        }
124        match opt(eof::<_, ContextError>).parse_next(input) {
125            Ok(Some(_)) | Err(_) => break,
126            Ok(None) => {}
127        }
128        if line_ending::<_, ContextError>.parse_next(input).is_err() {
129            break;
130        }
131    }
132    result
133}
134
135/// Result of parsing an SRT file: whatever stanzas parsed cleanly, plus the
136/// line at which parsing stopped early if the file wasn't fully consumed.
137pub struct SrtParseOutcome {
138    pub stanzas: Vec<Stanza>,
139    /// Line at which parsing stopped early because what followed didn't
140    /// parse as a valid stanza. `None` if every byte of the file was consumed
141    /// (a clean parse, or a cleanly empty stanza list).
142    pub truncated_at_line: Option<usize>,
143}
144
145pub fn read_srt_file(p: &std::path::Path) -> Result<SrtParseOutcome> {
146    let mut fd = std::fs::File::open(p)?;
147    let mut buf = Vec::new();
148    fd.read_to_end(&mut buf)?;
149    let mut buf_bstr: &BStr = buf.as_slice().into();
150
151    let stanzas = parse_stanzas(&mut buf_bstr);
152    let truncated_at_line = if buf_bstr.is_empty() {
153        None
154    } else {
155        let offset = buf.len() - buf_bstr.len();
156        Some(buf[..offset].iter().filter(|&&b| b == b'\n').count() + 1)
157    };
158
159    Ok(SrtParseOutcome {
160        stanzas,
161        truncated_at_line,
162    })
163}
164
165#[cfg(test)]
166mod test {
167    use super::*;
168
169    const B0: &[u8] = b"";
170
171    const B1: &[u8] = br#"1
17200:00:00,000 --> 00:00:00,040
173{"frame_cnt":1,"timestamp":"2024-11-21T21:04:19.534412+01:00"}
174"#;
175
176    const B2A: &[u8] = br#"1
17700:00:00,000 --> 00:00:00,040
178{"frame_cnt":1,"timestamp":"2024-11-21T21:04:19.534412+01:00"}
179
1802
18100:00:00,040 --> 00:00:00,080
182{"frame_cnt":2,"timestamp":"2024-11-21T21:04:19.552417+01:00"}"#;
183
184    const B2B: &[u8] = br#"1
18500:00:00,000 --> 00:00:00,040
186{"frame_cnt":1,"timestamp":"2024-11-21T21:04:19.534412+01:00"}
187
1882
18900:00:00,040 --> 00:00:00,080
190{"frame_cnt":2,"timestamp":"2024-11-21T21:04:19.552417+01:00"}
191
192"#;
193
194    const B3A: &[u8] = br#"1
19500:00:00,000 --> 00:00:00,040
196{"frame_cnt":1,"timestamp":"2024-11-21T21:04:19.534412+01:00"}
197
1982
19900:00:00,040 --> 00:00:00,080
200{"frame_cnt":2,"timestamp":"2024-11-21T21:04:19.552417+01:00"}
201
2023
20300:00:00,080 --> 00:00:00,120
204{"frame_cnt":3,"timestamp":"2024-11-21T21:04:19.563575+01:00"}"#;
205
206    const B3B: &[u8] = br#"1
20700:00:00,000 --> 00:00:00,040
208{"frame_cnt":1,"timestamp":"2024-11-21T21:04:19.534412+01:00"}
209
2102
21100:00:00,040 --> 00:00:00,080
212{"frame_cnt":2,"timestamp":"2024-11-21T21:04:19.552417+01:00"}
213
2143
21500:00:00,080 --> 00:00:00,120
216{"frame_cnt":3,"timestamp":"2024-11-21T21:04:19.563575+01:00"}
217"#;
218
219    #[test]
220    fn test_parse() {
221        for (sz, in_b3) in [(0, B0), (1, B1), (2, B2A), (2, B2B), (3, B3A), (3, B3B)] {
222            println!(
223                "testing size {sz} with value:\n{:?}",
224                String::from_utf8_lossy(in_b3)
225            );
226            let b3 = parse_stanzas(&mut in_b3.into());
227            assert_eq!(b3.len(), sz);
228        }
229    }
230}
231
232#[cfg(test)]
233mod test_duration {
234    use super::*;
235
236    trait Srt {
237        fn srt(&self) -> String;
238    }
239
240    impl Srt for Duration {
241        fn srt(&self) -> String {
242            // from https://en.wikipedia.org/wiki/SubRip :
243            // "hours:minutes:seconds,milliseconds with time units fixed to two
244            // zero-padded digits and fractions fixed to three zero-padded digits
245            // (00:00:00,000). The fractional separator used is the comma, since the
246            // program was written in France."
247            let total_secs = self.as_secs();
248            let hours = total_secs / (60 * 60);
249            let minutes = (total_secs % (60 * 60)) / 60;
250            let seconds = total_secs % 60;
251            debug_assert_eq!(total_secs, hours * 60 * 60 + minutes * 60 + seconds);
252            let millis = self.subsec_millis();
253            format!("{hours:02}:{minutes:02}:{seconds:02},{millis:03}")
254        }
255    }
256    #[test]
257    fn test_duration_roundtrip() {
258        for (h, m, s, ms) in [(1, 2, 3, 4), (3, 2, 1, 0), (10, 9, 8, 999)] {
259            let m = h * 60 + m;
260            let s = m * 60 + s;
261            let ms = s * 1000 + ms;
262            let dur = Duration::from_millis(ms);
263            let dur_str = dur.srt();
264            let dur_bytes: &BStr = dur_str.as_str().into();
265            let parsed = trace("parse_duration", parse_duration).parse(dur_bytes);
266            let parsed = parsed.unwrap();
267            assert_eq!(dur, parsed);
268        }
269    }
270}