From dc8b722f72a82e5c0819f9f67cc59c7a491d4110 Mon Sep 17 00:00:00 2001 From: Matthew Waters Date: Wed, 29 Jul 2020 19:54:15 +1000 Subject: [PATCH] video/closedcaption: add a ccdetect element Detects whether valid closed caption data is available in CC708 data. --- video/closedcaption/Cargo.toml | 3 +- video/closedcaption/src/ccdetect.rs | 587 ++++++++++++++++++++++++++ video/closedcaption/src/lib.rs | 2 + video/closedcaption/tests/ccdetect.rs | 348 +++++++++++++++ 4 files changed, 939 insertions(+), 1 deletion(-) create mode 100644 video/closedcaption/src/ccdetect.rs create mode 100644 video/closedcaption/tests/ccdetect.rs diff --git a/video/closedcaption/Cargo.toml b/video/closedcaption/Cargo.toml index edb76f39..2e6ad5c6 100644 --- a/video/closedcaption/Cargo.toml +++ b/video/closedcaption/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "gst-plugin-closedcaption" version = "0.6.0" -authors = ["Sebastian Dröge ", "Jordan Petridis "] +authors = ["Sebastian Dröge ", "Jordan Petridis ", "Matthew Waters "] license = "LGPL-2.1-or-later" edition = "2018" description = "Rust Closed Caption Plugin" @@ -19,6 +19,7 @@ cairo-rs = { git = "https://github.com/gtk-rs/cairo", features=["use_glib"] } cairo-sys-rs = { git = "https://github.com/gtk-rs/cairo" } pango = { git = "https://github.com/gtk-rs/pango" } pangocairo = { git = "https://github.com/gtk-rs/pangocairo" } +byteorder = "1" [dependencies.gst] git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" diff --git a/video/closedcaption/src/ccdetect.rs b/video/closedcaption/src/ccdetect.rs new file mode 100644 index 00000000..6c51c538 --- /dev/null +++ b/video/closedcaption/src/ccdetect.rs @@ -0,0 +1,587 @@ +// Copyright (C) 2020 Matthew Waters +// +// 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 glib::subclass; +use glib::subclass::prelude::*; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst_base::subclass::prelude::*; + +use byteorder::{BigEndian, ByteOrder}; + +use std::fmt; +use std::sync::Mutex; +use std::u64; + +lazy_static! { + static ref CAT: gst::DebugCategory = gst::DebugCategory::new( + "ccdetect", + gst::DebugColorFlags::empty(), + Some("Closed Caption Detection"), + ); +} + +const DEFAULT_WINDOW: u64 = 10 * gst::SECOND_VAL; +const DEFAULT_CC608: bool = false; +const DEFAULT_CC708: bool = false; + +#[derive(Debug, Clone, Copy)] +struct Settings { + pub window: u64, + pub cc608: bool, + pub cc708: bool, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + window: DEFAULT_WINDOW, + cc608: DEFAULT_CC608, + cc708: DEFAULT_CC708, + } + } +} + +#[derive(Debug, Clone, Copy)] +enum CCFormat { + Cc708Cdp, + Cc708CcData, +} + +#[derive(Debug, Clone, Copy)] +struct State { + format: CCFormat, + last_cc608_change: gst::ClockTime, + last_cc708_change: gst::ClockTime, +} + +struct CCDetect { + settings: Mutex, + state: Mutex>, +} + +static PROPERTIES: [subclass::Property; 3] = [ + subclass::Property("window", |name| { + glib::ParamSpec::uint64( + name, + "Window", + "Window of time (in ns) to determine if captions exist in the stream", + 0, + u64::MAX, + DEFAULT_WINDOW, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("cc608", |name| { + glib::ParamSpec::boolean( + name, + "cc608", + "Whether CEA608 captions (CC1/CC3) have been detected", + DEFAULT_CC608, + glib::ParamFlags::READABLE, + ) + }), + subclass::Property("cc708", |name| { + glib::ParamSpec::boolean( + name, + "cc608", + "Whether CEA708 captions (cc_data) have been detected", + DEFAULT_CC708, + glib::ParamFlags::READABLE, + ) + }), +]; + +#[derive(Debug, Clone, Copy)] +struct CCPacketContents { + cc608: bool, + cc708: bool, +} + +#[allow(clippy::enum_variant_names)] +#[derive(Debug, Clone, Copy)] +enum ParseErrorCode { + WrongLength, + WrongMagicSequence, + WrongLayout, +} + +#[derive(Debug, Clone)] +struct ParseError { + code: ParseErrorCode, + byte: usize, + msg: String, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?} at byte {}: {}", self.code, self.byte, self.msg) + } +} + +impl std::error::Error for ParseError {} + +impl CCDetect { + fn detect_cc_data(data: &[u8]) -> Result { + if data.len() % 3 != 0 { + gst_warning!(CAT, "cc_data length is not a multiple of 3, truncating"); + } + + /* logic from ccconverter */ + let mut started_ccp = false; + let mut have_cc608 = false; + let mut have_cc708 = false; + for (i, triple) in data.chunks_exact(3).enumerate() { + let cc_valid = (triple[0] & 0x04) == 0x04; + let cc_type = triple[0] & 0x03; + gst_trace!( + CAT, + "triple:{} have ccp:{} 608:{} 708:{} data:{:02x},{:02x},{:02x} cc_valid:{} cc_type:{:02b}", + i * 3, + started_ccp, + have_cc608, + have_cc708, + triple[0], + triple[1], + triple[2], + cc_valid, + cc_type + ); + + if !started_ccp && cc_valid { + if cc_type == 0x00 { + if triple[1] != 0x80 || triple[2] != 0x80 { + have_cc608 = true; + } + continue; + } else if cc_type == 0x01 { + if triple[1] != 0x80 || triple[2] != 0x80 { + have_cc708 = true; + } + continue; + } + } + + if cc_type & 0b10 == 0b10 { + started_ccp = true; + } + + if !cc_valid { + continue; + } + + if cc_type == 0x00 || cc_type == 0x01 { + return Err(ParseError { + code: ParseErrorCode::WrongLayout, + byte: data.len() - i * 3, + msg: String::from("Invalid cc_data. cea608 bytes after cea708"), + }); + } + + have_cc708 = true; + } + + Ok(CCPacketContents { + cc608: have_cc608, + cc708: have_cc708, + }) + } + + fn detect_cdp(mut data: &[u8]) -> Result { + /* logic from ccconverter */ + let data_len = data.len(); + + if data.len() < 11 { + return Err(ParseError { + code: ParseErrorCode::WrongLength, + byte: data_len - data.len(), + msg: format!( + "cdp packet too short {}. expected at least {}", + data.len(), + 11 + ), + }); + } + + if 0x9669 != BigEndian::read_u16(&data[..2]) { + return Err(ParseError { + code: ParseErrorCode::WrongMagicSequence, + byte: data_len - data.len(), + msg: String::from("cdp packet does not have initial magic bytes of 0x9669"), + }); + } + data = &data[2..]; + + if (data[0] as usize) != data_len { + return Err(ParseError { + code: ParseErrorCode::WrongLength, + byte: data_len - data.len(), + msg: format!( + "advertised cdp packet length {} does not match length of data {}", + data[0], data_len + ), + }); + } + data = &data[1..]; + + /* skip framerate value */ + data = &data[1..]; + + let flags = data[0]; + data = &data[1..]; + + if flags & 0x40 == 0 { + /* no cc_data */ + return Ok(CCPacketContents { + cc608: false, + cc708: false, + }); + } + + /* skip sequence counter */ + data = &data[2..]; + + /* timecode present? */ + if flags & 0x80 == 0x80 { + if data.len() < 5 { + return Err(ParseError { + code: ParseErrorCode::WrongLength, + byte: data_len - data.len(), + msg: String::from("cdp packet signals a timecode but is not large enough to contain a timecode") + }); + } + data = &data[5..]; + } + + /* cc_data */ + if data.len() < 2 { + return Err(ParseError { + code: ParseErrorCode::WrongLength, + byte: data_len - data.len(), + msg: String::from( + "cdp packet signals cc_data but is not large enough to contain cc_data", + ), + }); + } + + if data[0] != 0x72 { + return Err(ParseError { + code: ParseErrorCode::WrongMagicSequence, + byte: data_len - data.len(), + msg: String::from("ccp is missing start code 0x72"), + }); + } + data = &data[1..]; + + let cc_count = data[0]; + data = &data[1..]; + if cc_count & 0xe0 != 0xe0 { + return Err(ParseError { + code: ParseErrorCode::WrongMagicSequence, + byte: data_len - data.len(), + msg: format!("reserved bits are not 0xe0, found {:02x}", cc_count & 0xe0), + }); + } + let cc_count = cc_count & 0x1f; + let len = 3 * cc_count as usize; + + if len > data.len() { + return Err(ParseError { + code: ParseErrorCode::WrongLength, + byte: data_len - data.len(), + msg: String::from("cc_data length extends past the end of the cdp packet"), + }); + } + + /* TODO: validate checksum */ + + Self::detect_cc_data(&data[..len]) + } + + fn detect(format: CCFormat, data: &[u8]) -> Result { + match format { + CCFormat::Cc708CcData => Self::detect_cc_data(data), + CCFormat::Cc708Cdp => Self::detect_cdp(data), + } + } + + fn maybe_update_properties( + &self, + element: &gst_base::BaseTransform, + ts: gst::ClockTime, + cc_packet: CCPacketContents, + ) -> Result<(), gst::FlowError> { + let mut notify_cc608 = false; + let mut notify_cc708 = false; + + { + let mut settings = self.settings.lock().unwrap(); + + let mut state_guard = self.state.lock().unwrap(); + let state = state_guard.as_mut().ok_or(gst::FlowError::NotNegotiated)?; + + gst_trace!( + CAT, + "packet contains {:?} current settings {:?} and state {:?}", + cc_packet, + settings, + state + ); + + if cc_packet.cc608 != settings.cc608 { + if state.last_cc608_change.is_none() + || ts - state.last_cc608_change > settings.window.into() + { + settings.cc608 = cc_packet.cc608; + state.last_cc608_change = ts; + notify_cc608 = true; + } + } else { + state.last_cc608_change = ts; + } + + if cc_packet.cc708 != settings.cc708 { + if state.last_cc708_change.is_none() + || ts - state.last_cc708_change > settings.window.into() + { + settings.cc708 = cc_packet.cc708; + state.last_cc708_change = ts; + notify_cc708 = true; + } + } else { + state.last_cc708_change = ts; + } + + gst_trace!(CAT, "changed to settings {:?} state {:?}", settings, state); + } + + if notify_cc608 { + element.notify("cc608"); + } + if notify_cc708 { + element.notify("cc708"); + } + + Ok(()) + } +} + +impl ObjectSubclass for CCDetect { + const NAME: &'static str = "CCDetect"; + type ParentType = gst_base::BaseTransform; + type Instance = gst::subclass::ElementInstanceStruct; + type Class = subclass::simple::ClassStruct; + + glib_object_subclass!(); + + fn new() -> Self { + Self { + settings: Mutex::new(Default::default()), + state: Mutex::new(None), + } + } + + fn class_init(klass: &mut subclass::simple::ClassStruct) { + klass.set_metadata( + "Closed Caption Detect", + "Filter/Video/ClosedCaption/Detect", + "Detect if valid closed captions are present in a stream", + "Matthew Waters ", + ); + + let mut caps = gst::Caps::new_empty(); + { + let caps = caps.get_mut().unwrap(); + let s = gst::Structure::builder("closedcaption/x-cea-708") + .field("format", &gst::List::new(&[&"cc_data", &"cdp"])) + .build(); + caps.append_structure(s); + } + + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + + klass.install_properties(&PROPERTIES); + + klass.configure( + gst_base::subclass::BaseTransformMode::AlwaysInPlace, + true, + true, + ); + } +} + +impl ObjectImpl for CCDetect { + fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("window", ..) => { + let mut settings = self.settings.lock().unwrap(); + settings.window = value.get_some().expect("type checked upstream"); + } + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("window", ..) => { + let settings = self.settings.lock().unwrap(); + Ok(settings.window.to_value()) + } + subclass::Property("cc608", ..) => { + let settings = self.settings.lock().unwrap(); + Ok(settings.cc608.to_value()) + } + subclass::Property("cc708", ..) => { + let settings = self.settings.lock().unwrap(); + Ok(settings.cc708.to_value()) + } + _ => unimplemented!(), + } + } +} + +impl ElementImpl for CCDetect {} + +impl BaseTransformImpl for CCDetect { + fn transform_ip_passthrough( + &self, + element: &gst_base::BaseTransform, + buf: &gst::Buffer, + ) -> Result { + let map = buf.map_readable().map_err(|_| gst::FlowError::Error)?; + + if buf.get_pts().is_none() { + gst_element_error!( + element, + gst::ResourceError::Read, + ["Input buffers must have valid timestamps"] + ); + return Err(gst::FlowError::Error); + } + + let format = { + let mut state_guard = self.state.lock().unwrap(); + let state = state_guard.as_mut().ok_or(gst::FlowError::NotNegotiated)?; + state.format + }; + + let cc_packet = match Self::detect(format, map.as_slice()) { + Ok(v) => v, + Err(e) => { + gst_warning!(CAT, "{}", &e.to_string()); + gst_element_warning!(element, gst::StreamError::Decode, [&e.to_string()]); + CCPacketContents { + cc608: false, + cc708: false, + } + } + }; + + self.maybe_update_properties(element, buf.get_pts(), cc_packet) + .map_err(|_| gst::FlowError::Error)?; + + Ok(gst::FlowSuccess::Ok) + } + + fn sink_event(&self, element: &gst_base::BaseTransform, event: gst::Event) -> bool { + match event.view() { + gst::event::EventView::Gap(gap) => { + let _ = self.maybe_update_properties( + element, + gap.get().0, + CCPacketContents { + cc608: false, + cc708: false, + }, + ); + self.parent_sink_event(element, event) + } + _ => self.parent_sink_event(element, event), + } + } + + fn set_caps( + &self, + _element: &gst_base::BaseTransform, + incaps: &gst::Caps, + outcaps: &gst::Caps, + ) -> Result<(), gst::LoggableError> { + if incaps != outcaps { + return Err(gst_loggable_error!( + CAT, + "Input and output caps are not the same" + )); + } + + let s = incaps + .get_structure(0) + .ok_or_else(|| gst_loggable_error!(CAT, "Failed to parse input caps"))?; + let format_str = s + .get::<&str>("format") + .map_err(|_| gst_loggable_error!(CAT, "Failed to parse input caps"))? + .ok_or_else(|| gst_loggable_error!(CAT, "Failed to parse input caps"))?; + let cc_format = match format_str { + "cdp" => CCFormat::Cc708Cdp, + "cc_data" => CCFormat::Cc708CcData, + _ => return Err(gst_loggable_error!(CAT, "Failed to parse input caps")), + }; + + *self.state.lock().unwrap() = Some(State { + format: cc_format, + last_cc608_change: gst::ClockTime::none(), + last_cc708_change: gst::ClockTime::none(), + }); + + Ok(()) + } + + fn stop(&self, _element: &gst_base::BaseTransform) -> Result<(), gst::ErrorMessage> { + // Drop state + let _ = self.state.lock().unwrap().take(); + + Ok(()) + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "ccdetect", + gst::Rank::None, + CCDetect::get_type(), + ) +} diff --git a/video/closedcaption/src/lib.rs b/video/closedcaption/src/lib.rs index ad855fb7..9ba22abf 100644 --- a/video/closedcaption/src/lib.rs +++ b/video/closedcaption/src/lib.rs @@ -34,6 +34,7 @@ extern crate lazy_static; extern crate pretty_assertions; mod caption_frame; +mod ccdetect; mod cea608overlay; mod cea608tott; #[allow(non_camel_case_types, non_upper_case_globals)] @@ -57,6 +58,7 @@ fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { cea608tott::register(plugin)?; tttocea608::register(plugin)?; cea608overlay::register(plugin)?; + ccdetect::register(plugin)?; Ok(()) } diff --git a/video/closedcaption/tests/ccdetect.rs b/video/closedcaption/tests/ccdetect.rs new file mode 100644 index 00000000..0123a4dd --- /dev/null +++ b/video/closedcaption/tests/ccdetect.rs @@ -0,0 +1,348 @@ +// Copyright (C) 2020 Matthew Waters +// +// 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. + +#[macro_use] +extern crate pretty_assertions; + +use gst::prelude::*; + +use std::sync::{Arc, Mutex}; + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstrsclosedcaption::plugin_register_static().unwrap(); + }); +} + +struct NotifyState { + cc608_count: u32, + cc708_count: u32, +} + +impl Default for NotifyState { + fn default() -> Self { + NotifyState { + cc608_count: 0, + cc708_count: 0, + } + } +} + +macro_rules! assert_push_data { + ($h:expr, $state:expr, $data:expr, $ts:expr, $cc608_count:expr, $cc708_count:expr) => { + let mut buf = gst::Buffer::from_mut_slice($data); + buf.get_mut().unwrap().set_pts($ts); + + assert_eq!($h.push(buf), Ok(gst::FlowSuccess::Ok)); + { + let state_guard = $state.lock().unwrap(); + assert_eq!(state_guard.cc608_count, $cc608_count); + assert_eq!(state_guard.cc708_count, $cc708_count); + } + }; +} + +#[test] +fn test_have_cc_data_notify() { + init(); + let valid_cc608_data = vec![0xfc, 0x80, 0x81]; + let invalid_cc608_data = vec![0xf8, 0x80, 0x81]; + let valid_cc708_data = vec![0xfe, 0x80, 0x81]; + let invalid_cc708_data = vec![0xfa, 0x80, 0x81]; + + let mut h = gst_check::Harness::new("ccdetect"); + h.set_src_caps_str("closedcaption/x-cea-708,format=cc_data"); + h.set_sink_caps_str("closedcaption/x-cea-708,format=cc_data"); + h.get_element() + .unwrap() + .set_property("window", &(500_000_000 as u64)) + .unwrap(); + + let state = Arc::new(Mutex::new(NotifyState::default())); + let state_c = state.clone(); + h.get_element() + .unwrap() + .connect_notify(Some("cc608"), move |o, _pspec| { + let mut state_guard = state_c.lock().unwrap(); + state_guard.cc608_count += 1; + o.get_property("cc608").unwrap(); + }); + let state_c = state.clone(); + h.get_element() + .unwrap() + .connect_notify(Some("cc708"), move |o, _pspec| { + let mut state_guard = state_c.lock().unwrap(); + state_guard.cc708_count += 1; + o.get_property("cc708").unwrap(); + }); + + /* valid cc608 data moves cc608 property to true */ + assert_push_data!(h, state, valid_cc608_data, 0.into(), 1, 0); + + /* invalid cc608 data moves cc608 property to false */ + assert_push_data!(h, state, invalid_cc608_data, 1_000_000_000.into(), 2, 0); + + /* valid cc708 data moves cc708 property to true */ + assert_push_data!(h, state, valid_cc708_data, 2_000_000_000.into(), 2, 1); + + /* invalid cc708 data moves cc708 property to false */ + assert_push_data!(h, state, invalid_cc708_data, 3_000_000_000.into(), 2, 2); +} + +#[test] +fn test_cc_data_window() { + init(); + let valid_cc608_data = vec![0xfc, 0x80, 0x81]; + let invalid_cc608_data = vec![0xf8, 0x80, 0x81]; + + let mut h = gst_check::Harness::new("ccdetect"); + h.set_src_caps_str("closedcaption/x-cea-708,format=cc_data"); + h.set_sink_caps_str("closedcaption/x-cea-708,format=cc_data"); + h.get_element() + .unwrap() + .set_property("window", &500_000_000u64) + .unwrap(); + + let state = Arc::new(Mutex::new(NotifyState::default())); + let state_c = state.clone(); + h.get_element() + .unwrap() + .connect_notify(Some("cc608"), move |_o, _pspec| { + let mut state_guard = state_c.lock().unwrap(); + state_guard.cc608_count += 1; + }); + let state_c = state.clone(); + h.get_element() + .unwrap() + .connect_notify(Some("cc708"), move |_o, _pspec| { + let mut state_guard = state_c.lock().unwrap(); + state_guard.cc708_count += 1; + }); + + /* valid cc608 data moves cc608 property to true */ + assert_push_data!(h, state, valid_cc608_data.clone(), 0.into(), 1, 0); + + /* valid cc608 data moves within window */ + assert_push_data!(h, state, valid_cc608_data.clone(), 300_000_000.into(), 1, 0); + + /* invalid cc608 data before window expires, no change */ + assert_push_data!( + h, + state, + invalid_cc608_data.clone(), + 600_000_000.into(), + 1, + 0 + ); + + /* invalid cc608 data after window expires, cc608 changes to false */ + assert_push_data!(h, state, invalid_cc608_data, 1_000_000_000.into(), 2, 0); + + /* valid cc608 data before window expires, no change */ + assert_push_data!( + h, + state, + valid_cc608_data.clone(), + 1_300_000_000.into(), + 2, + 0 + ); + + /* valid cc608 data after window expires, property changes */ + assert_push_data!(h, state, valid_cc608_data, 1_600_000_000.into(), 3, 0); +} + +#[test] +fn test_have_cdp_notify() { + init(); + let valid_cc608_data = vec![ + 0x96, 0x69, /* cdp magic bytes */ + 0x10, /* length of cdp packet */ + 0x8f, /* framerate */ + 0x43, /* flags */ + 0x00, 0x00, /* sequence counter */ + 0x72, /* cc_data byte header */ + 0xe1, /* n cc_data triples with 0xe0 as reserved bits */ + 0xfc, 0x80, 0x81, /* cc_data triple */ + 0x74, /* cdp end of frame byte header */ + 0x00, 0x00, /* sequence counter */ + 0x60, /* checksum */ + ]; + let invalid_cc608_data = vec![ + 0x96, 0x69, 0x10, 0x8f, 0x43, 0x00, 0x00, 0x72, 0xe1, 0xf8, 0x81, 0x82, 0x74, 0x00, 0x00, + 0x60, + ]; + + let mut h = gst_check::Harness::new("ccdetect"); + h.set_src_caps_str("closedcaption/x-cea-708,format=cdp"); + h.set_sink_caps_str("closedcaption/x-cea-708,format=cdp"); + h.get_element() + .unwrap() + .set_property("window", &500_000_000u64) + .unwrap(); + + let state = Arc::new(Mutex::new(NotifyState::default())); + let state_c = state.clone(); + h.get_element() + .unwrap() + .connect_notify(Some("cc608"), move |_o, _pspec| { + let mut state_guard = state_c.lock().unwrap(); + state_guard.cc608_count += 1; + }); + let state_c = state.clone(); + h.get_element() + .unwrap() + .connect_notify(Some("cc708"), move |_o, _pspec| { + let mut state_guard = state_c.lock().unwrap(); + state_guard.cc708_count += 1; + }); + + /* valid cc608 data moves cc608 property to true */ + assert_push_data!(h, state, valid_cc608_data, 0.into(), 1, 0); + + /* invalid cc608 data moves cc608 property to false */ + assert_push_data!(h, state, invalid_cc608_data, 1_000_000_000.into(), 2, 0); +} + +#[test] +fn test_malformed_cdp_notify() { + init(); + let too_short = vec![0x96, 0x69]; + let wrong_magic = vec![ + 0x00, 0x00, 0x10, 0x8f, 0x43, 0x00, 0x00, 0x72, 0xe1, 0xfc, 0x81, 0x82, 0x74, 0x00, 0x00, + 0x60, + ]; + let length_too_long = vec![ + 0x96, 0x69, 0x20, 0x8f, 0x43, 0x00, 0x00, 0x72, 0xe1, 0xfc, 0x81, 0x82, 0x74, 0x00, 0x00, + 0x60, + ]; + let length_too_short = vec![ + 0x96, 0x69, 0x00, 0x8f, 0x43, 0x00, 0x00, 0x72, 0xe1, 0xfc, 0x81, 0x82, 0x74, 0x00, 0x00, + 0x60, + ]; + let wrong_cc_data_header_byte = vec![ + 0x96, 0x69, 0x10, 0x8f, 0x43, 0x00, 0x00, 0xff, 0xe1, 0xfc, 0x81, 0x82, 0x74, 0x00, 0x00, + 0x60, + ]; + let big_cc_count = vec![ + 0x96, 0x69, 0x10, 0x8f, 0x43, 0x00, 0x00, 0x72, 0xef, 0xfc, 0x81, 0x82, 0x74, 0x00, 0x00, + 0x60, + ]; + let wrong_cc_count_reserved_bits = vec![ + 0x96, 0x69, 0x10, 0x8f, 0x43, 0x00, 0x00, 0x72, 0x01, 0xfc, 0x81, 0x82, 0x74, 0x00, 0x00, + 0x60, + ]; + let cc608_after_cc708 = vec![ + 0x96, 0x69, 0x13, 0x8f, 0x43, 0x00, 0x00, 0x72, 0xe2, 0xfe, 0x81, 0x82, 0xfc, 0x83, 0x84, + 0x74, 0x00, 0x00, 0x60, + ]; + + let mut h = gst_check::Harness::new("ccdetect"); + h.set_src_caps_str("closedcaption/x-cea-708,format=cdp"); + h.set_sink_caps_str("closedcaption/x-cea-708,format=cdp"); + h.get_element() + .unwrap() + .set_property("window", &0u64) + .unwrap(); + + let state = Arc::new(Mutex::new(NotifyState::default())); + let state_c = state.clone(); + h.get_element() + .unwrap() + .connect_notify(Some("cc608"), move |_o, _pspec| { + let mut state_guard = state_c.lock().unwrap(); + state_guard.cc608_count += 1; + }); + let state_c = state.clone(); + h.get_element() + .unwrap() + .connect_notify(Some("cc708"), move |_o, _pspec| { + let mut state_guard = state_c.lock().unwrap(); + state_guard.cc708_count += 1; + }); + + /* all invalid data does not change properties */ + assert_push_data!(h, state, too_short, 0.into(), 0, 0); + assert_push_data!(h, state, wrong_magic, 1_000.into(), 0, 0); + assert_push_data!(h, state, length_too_long, 2_000.into(), 0, 0); + assert_push_data!(h, state, length_too_short, 3_000.into(), 0, 0); + assert_push_data!(h, state, wrong_cc_data_header_byte, 4_000.into(), 0, 0); + assert_push_data!(h, state, big_cc_count, 5_000.into(), 0, 0); + assert_push_data!(h, state, wrong_cc_count_reserved_bits, 6_000.into(), 0, 0); + assert_push_data!(h, state, cc608_after_cc708, 7_000.into(), 0, 0); +} + +#[test] +fn test_gap_events() { + init(); + let valid_cc608_data = vec![0xfc, 0x80, 0x81]; + + let mut h = gst_check::Harness::new("ccdetect"); + h.set_src_caps_str("closedcaption/x-cea-708,format=cc_data"); + h.set_sink_caps_str("closedcaption/x-cea-708,format=cc_data"); + h.get_element() + .unwrap() + .set_property("window", &500_000_000u64) + .unwrap(); + + let state = Arc::new(Mutex::new(NotifyState::default())); + let state_c = state.clone(); + h.get_element() + .unwrap() + .connect_notify(Some("cc608"), move |_o, _pspec| { + let mut state_guard = state_c.lock().unwrap(); + state_guard.cc608_count += 1; + }); + let state_c = state.clone(); + h.get_element() + .unwrap() + .connect_notify(Some("cc708"), move |_o, _pspec| { + let mut state_guard = state_c.lock().unwrap(); + state_guard.cc708_count += 1; + }); + + /* valid cc608 data moves cc608 property to true */ + assert_push_data!(h, state, valid_cc608_data, 0.into(), 1, 0); + + /* pushing gap event within the window changes nothing */ + assert_eq!( + h.push_event(gst::event::Gap::builder(100_000_000.into(), 1.into()).build()), + true + ); + + { + let state_guard = state.lock().unwrap(); + assert_eq!(state_guard.cc608_count, 1); + assert_eq!(state_guard.cc708_count, 0); + } + + /* pushing gap event outside the window moves cc608 property to false */ + assert_eq!( + h.push_event(gst::event::Gap::builder(1_000_000_000.into(), 1.into()).build()), + true + ); + + { + let state_guard = state.lock().unwrap(); + assert_eq!(state_guard.cc608_count, 2); + assert_eq!(state_guard.cc708_count, 0); + } +}