diff options
Diffstat (limited to 'gst-plugin-closedcaption/src/mcc_parser.rs')
-rw-r--r-- | gst-plugin-closedcaption/src/mcc_parser.rs | 793 |
1 files changed, 793 insertions, 0 deletions
diff --git a/gst-plugin-closedcaption/src/mcc_parser.rs b/gst-plugin-closedcaption/src/mcc_parser.rs new file mode 100644 index 000000000..fe9aca6a3 --- /dev/null +++ b/gst-plugin-closedcaption/src/mcc_parser.rs @@ -0,0 +1,793 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use either::Either; + +use combine; +use combine::parser::byte::hex_digit; +use combine::parser::range::{range, take_while1}; +use combine::parser::repeat::skip_many; +use combine::{any, choice, eof, from_str, many1, one_of, optional, token, unexpected_any, value}; +use combine::{ParseError, Parser, RangeStream}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TimeCode { + pub hours: u32, + pub minutes: u32, + pub seconds: u32, + pub frames: u32, + pub drop_frame: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum MccLine<'a> { + Header, + Comment, + Empty, + UUID(&'a [u8]), + Metadata(&'a [u8], &'a [u8]), + TimeCodeRate(u8, bool), + Caption(TimeCode, Vec<u8>), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum State { + Init, + Header, + Comments, + Metadata, + Captions, +} + +#[derive(Debug)] +pub struct MccParser { + state: State, +} + +/// Parser for parsing a run of ASCII, decimal digits and converting them into a `u32` +fn digits<'a, I: 'a>() -> impl Parser<Input = I, Output = u32> +where + I: RangeStream<Item = u8, Range = &'a [u8]>, + I::Error: ParseError<I::Item, I::Range, I::Position>, +{ + from_str(take_while1(|c: u8| c >= b'0' && c <= b'9').message("while parsing digits")) +} + +/// Copy from std::ops::RangeBounds as it's not stabilized yet. +/// +/// Checks if `item` is in the range `range`. +fn contains<R: std::ops::RangeBounds<U>, U>(range: &R, item: &U) -> bool +where + U: ?Sized + PartialOrd<U>, +{ + (match range.start_bound() { + std::ops::Bound::Included(ref start) => *start <= item, + std::ops::Bound::Excluded(ref start) => *start < item, + std::ops::Bound::Unbounded => true, + }) && (match range.end_bound() { + std::ops::Bound::Included(ref end) => item <= *end, + std::ops::Bound::Excluded(ref end) => item < *end, + std::ops::Bound::Unbounded => true, + }) +} + +/// Parser for a run of decimal digits, that converts them into a `u32` and checks if the result is +/// in the allowed range. +fn digits_range<'a, I: 'a, R: std::ops::RangeBounds<u32>>( + range: R, +) -> impl Parser<Input = I, Output = u32> +where + I: RangeStream<Item = u8, Range = &'a [u8]>, + I::Error: ParseError<I::Item, I::Range, I::Position>, +{ + digits().then(move |v| { + if contains(&range, &v) { + value(v).left() + } else { + unexpected_any("digits out of range").right() + } + }) +} + +/// Parser for a timecode in the form `hh:mm:ss:fs` +fn timecode<'a, I: 'a>() -> impl Parser<Input = I, Output = TimeCode> +where + I: RangeStream<Item = u8, Range = &'a [u8]>, + I::Error: ParseError<I::Item, I::Range, I::Position>, +{ + ( + digits(), + token(b':'), + digits_range(0..60), + token(b':'), + digits_range(0..60), + one_of([b':', b'.', b';', b','].iter().cloned()), + digits(), + ) + .map(|(hours, _, minutes, _, seconds, sep, frames)| TimeCode { + hours, + minutes, + seconds, + frames, + drop_frame: sep == b';' || sep == b',', + }) + .message("while parsing timecode") +} + +/// Parser that checks for EOF and optionally `\n` or `\r\n` before EOF +fn end_of_line<'a, I: 'a>() -> impl Parser<Input = I, Output = ()> + 'a +where + I: RangeStream<Item = u8, Range = &'a [u8]>, + I::Error: ParseError<I::Item, I::Range, I::Position>, +{ + ( + optional(choice((range(b"\n".as_ref()), range(b"\r\n".as_ref())))), + eof(), + ) + .map(|_| ()) + .message("while parsing end of line") +} + +/// Parser for the MCC header +fn header<'a, I: 'a>() -> impl Parser<Input = I, Output = MccLine<'a>> +where + I: RangeStream<Item = u8, Range = &'a [u8]>, + I::Error: ParseError<I::Item, I::Range, I::Position>, +{ + ( + range(b"File Format=MacCaption_MCC V".as_ref()), + choice!(range(b"1.0".as_ref()), range(b"2.0".as_ref())), + end_of_line(), + ) + .map(|_| MccLine::Header) + .message("while parsing header") +} + +/// Parser that accepts only an empty line +fn empty_line<'a, I: 'a>() -> impl Parser<Input = I, Output = MccLine<'a>> +where + I: RangeStream<Item = u8, Range = &'a [u8]>, + I::Error: ParseError<I::Item, I::Range, I::Position>, +{ + end_of_line() + .map(|_| MccLine::Empty) + .message("while parsing empty line") +} + +/// Parser for an MCC comment, i.e. a line starting with `//`. We don't return the actual comment +/// text as it's irrelevant for us. +fn comment<'a, I: 'a>() -> impl Parser<Input = I, Output = MccLine<'a>> +where + I: RangeStream<Item = u8, Range = &'a [u8]>, + I::Error: ParseError<I::Item, I::Range, I::Position>, +{ + (token(b'/'), token(b'/'), skip_many(any())) + .map(|_| MccLine::Comment) + .message("while parsing comment") +} + +/// Parser for the MCC UUID line. +fn uuid<'a, I: 'a>() -> impl Parser<Input = I, Output = MccLine<'a>> +where + I: RangeStream<Item = u8, Range = &'a [u8]>, + I::Error: ParseError<I::Item, I::Range, I::Position>, +{ + ( + range(b"UUID=".as_ref()), + take_while1(|b| b != b'\n' && b != b'\r'), + end_of_line(), + ) + .map(|(_, uuid, _)| MccLine::UUID(uuid)) + .message("while parsing UUID") +} + +/// Parser for the MCC Time Code Rate line. +fn time_code_rate<'a, I: 'a>() -> impl Parser<Input = I, Output = MccLine<'a>> +where + I: RangeStream<Item = u8, Range = &'a [u8]>, + I::Error: ParseError<I::Item, I::Range, I::Position>, +{ + ( + range(b"Time Code Rate=".as_ref()), + digits_range(1..256) + .and(optional(range(b"DF".as_ref()))) + .map(|(v, df)| MccLine::TimeCodeRate(v as u8, df.is_some())), + end_of_line(), + ) + .map(|(_, v, _)| v) + .message("while parsing time code rate") +} + +/// Parser for generic MCC metadata lines in the form `key=value`. +fn metadata<'a, I: 'a>() -> impl Parser<Input = I, Output = MccLine<'a>> +where + I: RangeStream<Item = u8, Range = &'a [u8]>, + I::Error: ParseError<I::Item, I::Range, I::Position>, +{ + ( + take_while1(|b| b != b'='), + token(b'='), + take_while1(|b| b != b'\n' && b != b'\r'), + end_of_line(), + ) + .map(|(name, _, value, _)| MccLine::Metadata(name, value)) + .message("while parsing metadata") +} + +/// A single MCC payload item. This is ASCII hex encoded bytes plus some single-character +/// short-cuts for common byte sequences. +/// +/// It returns an `Either` of the single hex encoded byte or the short-cut byte sequence as a +/// static byte slice. +fn mcc_payload_item<'a, I: 'a>() -> impl Parser<Input = I, Output = Either<u8, &'static [u8]>> +where + I: RangeStream<Item = u8, Range = &'a [u8]>, + I::Error: ParseError<I::Item, I::Range, I::Position>, +{ + choice!( + token(b'G').map(|_| Either::Right([0xfau8, 0x00, 0x00].as_ref())), + token(b'H').map(|_| Either::Right([0xfau8, 0x00, 0x00, 0xfa, 0x00, 0x00].as_ref())), + token(b'I').map(|_| Either::Right( + [0xfau8, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00].as_ref() + )), + token(b'J').map(|_| Either::Right( + [0xfau8, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00].as_ref() + )), + token(b'K').map(|_| Either::Right( + [ + 0xfau8, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, + 0x00, 0x00 + ] + .as_ref() + )), + token(b'L').map(|_| Either::Right( + [ + 0xfau8, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, + 0x00, 0x00, 0xfa, 0x00, 0x00 + ] + .as_ref() + )), + token(b'M').map(|_| Either::Right( + [ + 0xfau8, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, + 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00 + ] + .as_ref() + )), + token(b'N').map(|_| Either::Right( + [ + 0xfau8, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, + 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00 + ] + .as_ref() + )), + token(b'O').map(|_| Either::Right( + [ + 0xfau8, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, + 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00 + ] + .as_ref() + )), + token(b'P').map(|_| Either::Right([0xfbu8, 0x80, 0x80].as_ref())), + token(b'Q').map(|_| Either::Right([0xfcu8, 0x80, 0x80].as_ref())), + token(b'R').map(|_| Either::Right([0xfdu8, 0x80, 0x80].as_ref())), + token(b'S').map(|_| Either::Right([0x96u8, 0x69].as_ref())), + token(b'T').map(|_| Either::Right([0x61u8, 0x01].as_ref())), + token(b'U').map(|_| Either::Right([0xe1u8, 0x00, 0x00].as_ref())), + token(b'Z').map(|_| Either::Left(0x00u8)), + (hex_digit(), hex_digit()).map(|(u, l)| { + let hex_to_u8 = |v: u8| match v { + v if v >= b'0' && v <= b'9' => v - b'0', + v if v >= b'A' && v <= b'F' => 10 + v - b'A', + v if v >= b'a' && v <= b'f' => 10 + v - b'a', + _ => unreachable!(), + }; + let val = (hex_to_u8(u) << 4) | hex_to_u8(l); + Either::Left(val) + }) + ) + .message("while parsing MCC payload") +} + +/// A wrapper around `Vec<u8>` that implements `Extend` in a special way. It can be +/// extended from an iterator of `Either<u8, &[u8]>` while the default `Extend` implementation for +/// `Vec` only allows to extend over vector items. +struct VecExtend(Vec<u8>); + +impl Default for VecExtend { + fn default() -> Self { + VecExtend(Vec::with_capacity(256)) + } +} + +impl<'a> Extend<Either<u8, &'a [u8]>> for VecExtend { + fn extend<T>(&mut self, iter: T) + where + T: IntoIterator<Item = Either<u8, &'a [u8]>>, + { + for item in iter { + match item { + Either::Left(v) => self.0.push(v), + Either::Right(v) => self.0.extend_from_slice(v), + } + } + } +} + +/// Parser for the whole MCC payload with conversion to the underlying byte values. +fn mcc_payload<'a, I: 'a>() -> impl Parser<Input = I, Output = Vec<u8>> +where + I: RangeStream<Item = u8, Range = &'a [u8]>, + I::Error: ParseError<I::Item, I::Range, I::Position>, +{ + many1(mcc_payload_item()) + .map(|v: VecExtend| v.0) + .message("while parsing MCC payloads") +} + +/// Parser for a MCC caption line in the form `timecode\tpayload`. +fn caption<'a, I: 'a>() -> impl Parser<Input = I, Output = MccLine<'a>> +where + I: RangeStream<Item = u8, Range = &'a [u8]>, + I::Error: ParseError<I::Item, I::Range, I::Position>, +{ + ( + timecode(), + optional(( + token(b'.'), + one_of([b'0', b'1'].iter().cloned()), + optional((token(b','), digits())), + )), + token(b'\t'), + mcc_payload(), + end_of_line(), + ) + .map(|(tc, _, _, value, _)| MccLine::Caption(tc, value)) + .message("while parsing caption") +} + +/// MCC parser the parses line-by-line and keeps track of the current state in the file. +impl MccParser { + pub fn new() -> Self { + Self { state: State::Init } + } + + pub fn reset(&mut self) { + self.state = State::Init; + } + + pub fn parse_line<'a>( + &mut self, + line: &'a [u8], + ) -> Result<MccLine<'a>, combine::easy::Errors<u8, &'a [u8], combine::stream::PointerOffset>> + { + match self.state { + State::Init => header() + .message("while in Init state") + .easy_parse(line) + .map(|v| { + self.state = State::Header; + v.0 + }), + State::Header => empty_line() + .message("while in Header state") + .easy_parse(line) + .map(|v| { + self.state = State::Comments; + v.0 + }), + State::Comments => choice!(empty_line(), comment()) + .message("while in Comments state") + .easy_parse(line) + .map(|v| { + if v.0 == MccLine::Empty { + self.state = State::Metadata; + } + + v.0 + }), + State::Metadata => choice!(empty_line(), uuid(), time_code_rate(), metadata()) + .message("while in Metadata state") + .easy_parse(line) + .map(|v| { + if v.0 == MccLine::Empty { + self.state = State::Captions; + } + + v.0 + }), + State::Captions => caption() + .message("while in Captions state") + .easy_parse(line) + .map(|v| v.0), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use combine::error::UnexpectedParse; + + #[test] + fn test_timecode() { + let mut parser = timecode(); + assert_eq!( + parser.parse(b"11:12:13;14".as_ref()), + Ok(( + TimeCode { + hours: 11, + minutes: 12, + seconds: 13, + frames: 14, + drop_frame: true + }, + b"".as_ref() + )) + ); + + assert_eq!( + parser.parse(b"11:12:13:14".as_ref()), + Ok(( + TimeCode { + hours: 11, + minutes: 12, + seconds: 13, + frames: 14, + drop_frame: false + }, + b"".as_ref() + )) + ); + + assert_eq!( + parser.parse(b"11:12:13:14abcd".as_ref()), + Ok(( + TimeCode { + hours: 11, + minutes: 12, + seconds: 13, + frames: 14, + drop_frame: false + }, + b"abcd".as_ref() + )) + ); + + assert_eq!( + parser.parse(b"abcd11:12:13:14".as_ref()), + Err(UnexpectedParse::Unexpected) + ); + } + + #[test] + fn test_header() { + let mut parser = header(); + assert_eq!( + parser.parse(b"File Format=MacCaption_MCC V1.0".as_ref()), + Ok((MccLine::Header, b"".as_ref())) + ); + + assert_eq!( + parser.parse(b"File Format=MacCaption_MCC V1.0\n".as_ref()), + Ok((MccLine::Header, b"".as_ref())) + ); + + assert_eq!( + parser.parse(b"File Format=MacCaption_MCC V1.0\r\n".as_ref()), + Ok((MccLine::Header, b"".as_ref())) + ); + + assert_eq!( + parser.parse(b"File Format=MacCaption_MCC V2.0\r\n".as_ref()), + Ok((MccLine::Header, b"".as_ref())) + ); + + assert_eq!( + parser.parse(b"File Format=MacCaption_MCC V1.1".as_ref()), + Err(UnexpectedParse::Unexpected) + ); + } + + #[test] + fn test_empty_line() { + let mut parser = empty_line(); + assert_eq!( + parser.parse(b"".as_ref()), + Ok((MccLine::Empty, b"".as_ref())) + ); + + assert_eq!( + parser.parse(b"\n".as_ref()), + Ok((MccLine::Empty, b"".as_ref())) + ); + + assert_eq!( + parser.parse(b"\r\n".as_ref()), + Ok((MccLine::Empty, b"".as_ref())) + ); + + assert_eq!( + parser.parse(b" \r\n".as_ref()), + Err(UnexpectedParse::Unexpected) + ); + } + + #[test] + fn test_comment() { + let mut parser = comment(); + assert_eq!( + parser.parse(b"// blabla".as_ref()), + Ok((MccLine::Comment, b"".as_ref())) + ); + + assert_eq!( + parser.parse(b"//\n".as_ref()), + Ok((MccLine::Comment, b"".as_ref())) + ); + + assert_eq!( + parser.parse(b"//".as_ref()), + Ok((MccLine::Comment, b"".as_ref())) + ); + + assert_eq!( + parser.parse(b" //".as_ref()), + Err(UnexpectedParse::Unexpected) + ); + } + + #[test] + fn test_uuid() { + let mut parser = uuid(); + assert_eq!( + parser.parse(b"UUID=1234".as_ref()), + Ok((MccLine::UUID(b"1234".as_ref()), b"".as_ref())) + ); + + assert_eq!( + parser.parse(b"UUID=1234\n".as_ref()), + Ok((MccLine::UUID(b"1234".as_ref()), b"".as_ref())) + ); + + assert_eq!( + parser.parse(b"UUID=1234\r\n".as_ref()), + Ok((MccLine::UUID(b"1234".as_ref()), b"".as_ref())) + ); + + assert_eq!( + parser.parse(b"UUID=".as_ref()), + Err(UnexpectedParse::Unexpected) + ); + + assert_eq!( + parser.parse(b"uUID=1234".as_ref()), + Err(UnexpectedParse::Unexpected) + ); + } + + #[test] + fn test_time_code_rate() { + let mut parser = time_code_rate(); + assert_eq!( + parser.parse(b"Time Code Rate=30".as_ref()), + Ok((MccLine::TimeCodeRate(30, false), b"".as_ref())) + ); + + assert_eq!( + parser.parse(b"Time Code Rate=30DF".as_ref()), + Ok((MccLine::TimeCodeRate(30, true), b"".as_ref())) + ); + + assert_eq!( + parser.parse(b"Time Code Rate=60".as_ref()), + Ok((MccLine::TimeCodeRate(60, false), b"".as_ref())) + ); + + assert_eq!( + parser.parse(b"Time Code Rate=17F".as_ref()), + Err(UnexpectedParse::Unexpected) + ); + + assert_eq!( + parser.parse(b"Time Code Rate=256".as_ref()), + Err(UnexpectedParse::Unexpected) + ); + } + + #[test] + fn test_metadata() { + let mut parser = metadata(); + assert_eq!( + parser.parse(b"Creation Date=Thursday, June 04, 2015".as_ref()), + Ok(( + MccLine::Metadata( + b"Creation Date".as_ref(), + b"Thursday, June 04, 2015".as_ref() + ), + b"".as_ref() + )) + ); + + assert_eq!( + parser.parse(b"Creation Date= ".as_ref()), + Ok(( + MccLine::Metadata(b"Creation Date".as_ref(), b" ".as_ref()), + b"".as_ref() + )) + ); + + assert_eq!( + parser.parse(b"Creation Date".as_ref()), + Err(UnexpectedParse::Eoi) + ); + + assert_eq!( + parser.parse(b"Creation Date\n".as_ref()), + Err(UnexpectedParse::Eoi) + ); + + assert_eq!( + parser.parse(b"Creation Date=".as_ref()), + Err(UnexpectedParse::Unexpected) + ); + + assert_eq!( + parser.parse(b"Creation Date=\n".as_ref()), + Err(UnexpectedParse::Unexpected) + ); + } + + #[test] + fn test_caption() { + let mut parser = caption(); + assert_eq!( + parser.parse(b"00:00:00:00\tT52S524F67ZZ72F4QROO7391UC13FFF74ZZAEB4".as_ref()), + Ok(( + MccLine::Caption( + TimeCode { + hours: 0, + minutes: 0, + seconds: 0, + frames: 0, + drop_frame: false + }, + vec![ + 0x61, 0x01, 0x52, 0x96, 0x69, 0x52, 0x4F, 0x67, 0x00, 0x00, 0x72, 0xF4, + 0xFC, 0x80, 0x80, 0xFD, 0x80, 0x80, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, + 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, + 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, + 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, + 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, + 0x73, 0x91, 0xE1, 0x00, 0x00, 0xC1, 0x3F, 0xFF, 0x74, 0x00, 0x00, 0xAE, + 0xB4 + ] + ), + b"".as_ref() + )) + ); + + assert_eq!( + parser.parse(b"00:00:00:00.0\tT52S524F67ZZ72F4QROO7391UC13FFF74ZZAEB4".as_ref()), + Ok(( + MccLine::Caption( + TimeCode { + hours: 0, + minutes: 0, + seconds: 0, + frames: 0, + drop_frame: false + }, + vec![ + 0x61, 0x01, 0x52, 0x96, 0x69, 0x52, 0x4F, 0x67, 0x00, 0x00, 0x72, 0xF4, + 0xFC, 0x80, 0x80, 0xFD, 0x80, 0x80, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, + 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, + 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, + 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, + 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, + 0x73, 0x91, 0xE1, 0x00, 0x00, 0xC1, 0x3F, 0xFF, 0x74, 0x00, 0x00, 0xAE, + 0xB4 + ] + ), + b"".as_ref() + )) + ); + + assert_eq!( + parser.parse(b"00:00:00:00.0,9\tT52S524F67ZZ72F4QROO7391UC13FFF74ZZAEB4".as_ref()), + Ok(( + MccLine::Caption( + TimeCode { + hours: 0, + minutes: 0, + seconds: 0, + frames: 0, + drop_frame: false + }, + vec![ + 0x61, 0x01, 0x52, 0x96, 0x69, 0x52, 0x4F, 0x67, 0x00, 0x00, 0x72, 0xF4, + 0xFC, 0x80, 0x80, 0xFD, 0x80, 0x80, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, + 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, + 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, + 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, + 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, + 0x73, 0x91, 0xE1, 0x00, 0x00, 0xC1, 0x3F, 0xFF, 0x74, 0x00, 0x00, 0xAE, + 0xB4 + ] + ), + b"".as_ref() + )) + ); + + assert_eq!( + parser.parse(b"Creation Date=\n".as_ref()), + Err(UnexpectedParse::Unexpected) + ); + } + + #[test] + fn test_parser() { + let mcc_file = include_bytes!("../tests/captions-test_708.mcc"); + let mut reader = crate::line_reader::LineReader::new(); + let mut parser = MccParser::new(); + let mut line_cnt = 0; + + reader.push(Vec::from(mcc_file.as_ref())); + + while let Some(line) = reader.get_line() { + let res = match parser.parse_line(line) { + Ok(res) => res, + Err(err) => panic!("Couldn't parse line {}: {:?}", line_cnt, err), + }; + + match line_cnt { + 0 => assert_eq!(res, MccLine::Header), + 1 | 37 | 43 => assert_eq!(res, MccLine::Empty), + x if x >= 2 && x <= 36 => assert_eq!(res, MccLine::Comment), + 38 => assert_eq!( + res, + MccLine::UUID(b"CA8BC94D-9931-4EEE-812F-2D68FA74F287".as_ref()) + ), + 39 => assert_eq!( + res, + MccLine::Metadata( + b"Creation Program".as_ref(), + b"Adobe Premiere Pro CC (Windows)".as_ref() + ) + ), + 40 => assert_eq!( + res, + MccLine::Metadata( + b"Creation Date".as_ref(), + b"Thursday, June 04, 2015".as_ref() + ) + ), + 41 => assert_eq!( + res, + MccLine::Metadata(b"Creation Time".as_ref(), b"13:48:25".as_ref()) + ), + 42 => assert_eq!(res, MccLine::TimeCodeRate(30, true)), + _ => match res { + MccLine::Caption(_, _) => (), + res => panic!("Expected caption at line {}, got {:?}", line_cnt, res), + }, + } + + line_cnt += 1; + } + } +} |