diff options
Diffstat (limited to 'video/closedcaption/src/tttocea608/imp.rs')
-rw-r--r-- | video/closedcaption/src/tttocea608/imp.rs | 939 |
1 files changed, 939 insertions, 0 deletions
diff --git a/video/closedcaption/src/tttocea608/imp.rs b/video/closedcaption/src/tttocea608/imp.rs new file mode 100644 index 000000000..beb05e82a --- /dev/null +++ b/video/closedcaption/src/tttocea608/imp.rs @@ -0,0 +1,939 @@ +// Copyright (C) 2020 Mathieu Duponchelle <mathieu@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 glib::prelude::*; +use glib::subclass; +use glib::subclass::prelude::*; +use gst::prelude::*; +use gst::subclass::prelude::*; + +use crate::ffi; +use std::sync::Mutex; + +use super::Mode; + +fn decrement_pts( + min_frame_no: u64, + frame_no: &mut u64, + fps_n: u64, + fps_d: u64, +) -> (gst::ClockTime, gst::ClockTime) { + let old_pts = (*frame_no * gst::SECOND) + .mul_div_round(fps_d, fps_n) + .unwrap(); + + if *frame_no > min_frame_no { + *frame_no -= 1; + } + + let new_pts = (*frame_no * gst::SECOND) + .mul_div_round(fps_d, fps_n) + .unwrap(); + + let duration = old_pts - new_pts; + + (new_pts, duration) +} + +fn increment_pts( + frame_no: &mut u64, + max_frame_no: u64, + fps_n: u64, + fps_d: u64, +) -> (gst::ClockTime, gst::ClockTime) { + let pts = (*frame_no * gst::SECOND) + .mul_div_round(fps_d, fps_n) + .unwrap(); + + if *frame_no < max_frame_no { + *frame_no += 1; + } + + let next_pts = (*frame_no * gst::SECOND) + .mul_div_round(fps_d, fps_n) + .unwrap(); + + let duration = next_pts - pts; + (pts, duration) +} + +fn is_basicna(cc_data: u16) -> bool { + 0x0000 != (0x6000 & cc_data) +} + +fn is_westeu(cc_data: u16) -> bool { + 0x1220 == (0x7660 & cc_data) +} + +fn is_specialna(cc_data: u16) -> bool { + 0x1130 == (0x7770 & cc_data) +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn eia608_from_utf8_1(c: &[u8; 5]) -> u16 { + assert!(c[4] == 0); + unsafe { ffi::eia608_from_utf8_1(c.as_ptr() as *const _, 0) } +} + +fn eia608_row_column_preamble(row: i32, col: i32) -> u16 { + unsafe { + /* Hardcoded chan and underline */ + ffi::eia608_row_column_pramble(row, col, 0, 0) + } +} + +fn eia608_control_command(cmd: ffi::eia608_control_t) -> u16 { + unsafe { ffi::eia608_control_command(cmd, 0) } +} + +fn eia608_from_basicna(bna1: u16, bna2: u16) -> u16 { + unsafe { ffi::eia608_from_basicna(bna1, bna2) } +} + +fn buffer_from_cc_data(cc_data: u16) -> gst::buffer::Buffer { + let mut ret = gst::Buffer::with_size(2).unwrap(); + { + let buf_mut = ret.get_mut().unwrap(); + + let cc_data = cc_data.to_be_bytes(); + + gst_trace!(CAT, "CC data: {:x} {:x}", cc_data[0], cc_data[1]); + + buf_mut.copy_from_slice(0, &cc_data).unwrap(); + } + + ret +} + +fn control_command_buffer(buffers: &mut Vec<gst::Buffer>, cmd: ffi::eia608_control_t) { + let cc_data = eia608_control_command(cmd); + buffers.push(buffer_from_cc_data(cc_data)); + buffers.push(buffer_from_cc_data(cc_data)); +} + +fn erase_non_displayed_memory(buffers: &mut Vec<gst::Buffer>) { + control_command_buffer( + buffers, + ffi::eia608_control_t_eia608_control_erase_non_displayed_memory, + ); +} + +fn erase_display_memory(buffers: &mut Vec<gst::Buffer>) { + control_command_buffer( + buffers, + ffi::eia608_control_t_eia608_control_erase_display_memory, + ); +} + +fn erase_display_memory_with_pts( + bufferlist: &mut gst::BufferListRef, + pts: gst::ClockTime, + duration: gst::ClockTime, +) { + let cc_data = eia608_control_command(ffi::eia608_control_t_eia608_control_erase_display_memory); + + let mut buffer = buffer_from_cc_data(cc_data); + { + let buf_mut = buffer.get_mut().unwrap(); + buf_mut.set_pts(pts); + buf_mut.set_duration(duration); + } + bufferlist.insert(0, buffer); +} + +fn resume_caption_loading(buffers: &mut Vec<gst::Buffer>) { + control_command_buffer( + buffers, + ffi::eia608_control_t_eia608_control_resume_caption_loading, + ); +} + +fn roll_up_2(buffers: &mut Vec<gst::Buffer>) { + control_command_buffer(buffers, ffi::eia608_control_t_eia608_control_roll_up_2); +} + +fn roll_up_3(buffers: &mut Vec<gst::Buffer>) { + control_command_buffer(buffers, ffi::eia608_control_t_eia608_control_roll_up_3); +} + +fn roll_up_4(buffers: &mut Vec<gst::Buffer>) { + control_command_buffer(buffers, ffi::eia608_control_t_eia608_control_roll_up_4); +} + +fn carriage_return(buffers: &mut Vec<gst::Buffer>) { + control_command_buffer( + buffers, + ffi::eia608_control_t_eia608_control_carriage_return, + ); +} + +fn end_of_caption(buffers: &mut Vec<gst::Buffer>) { + control_command_buffer(buffers, ffi::eia608_control_t_eia608_control_end_of_caption); +} + +fn preamble_buffer(buffers: &mut Vec<gst::Buffer>, row: i32, col: i32) { + let cc_data = eia608_row_column_preamble(row, col); + buffers.push(buffer_from_cc_data(cc_data)); + buffers.push(buffer_from_cc_data(cc_data)); +} + +fn bna_buffer(buffers: &mut Vec<gst::Buffer>, bna1: u16, bna2: u16) { + let cc_data = eia608_from_basicna(bna1, bna2); + + buffers.push(buffer_from_cc_data(cc_data)); +} + +const DEFAULT_FPS_N: i32 = 30; +const DEFAULT_FPS_D: i32 = 1; + +/* 74 is quite the magic number: + * 2 byte pairs for resume_caption_loading + * 2 byte pairs for erase_non_displayed_memory + * At most 4 byte pairs for the preambles (one per line, at most 2 lines) + * At most 64 byte pairs for the text if it's made up of 64 westeu characters + * At most 2 byte pairs if we need to splice in an erase_display_memory + */ +const LATENCY_BUFFERS: u64 = 74; + +const DEFAULT_MODE: Mode = Mode::RollUp2; + +static PROPERTIES: [subclass::Property; 1] = [subclass::Property("mode", |name| { + glib::ParamSpec::enum_( + name, + "Mode", + "Which mode to operate in, roll-up modes introduce no latency", + Mode::static_type(), + DEFAULT_MODE as i32, + glib::ParamFlags::READWRITE, + ) +})]; + +#[derive(Debug, Clone)] +struct Settings { + mode: Mode, +} + +impl Default for Settings { + fn default() -> Self { + Settings { mode: DEFAULT_MODE } + } +} + +struct State { + settings: Settings, + framerate: gst::Fraction, + erase_display_frame_no: Option<u64>, + last_frame_no: u64, + roll_up_column: u32, + send_roll_up: bool, +} + +impl Default for State { + fn default() -> Self { + Self { + settings: Settings::default(), + framerate: gst::Fraction::new(DEFAULT_FPS_N, DEFAULT_FPS_D), + erase_display_frame_no: None, + last_frame_no: 0, + roll_up_column: 0, + send_roll_up: false, + } + } +} + +pub struct TtToCea608 { + srcpad: gst::Pad, + sinkpad: gst::Pad, + + state: Mutex<State>, + settings: Mutex<Settings>, +} + +lazy_static! { + static ref CAT: gst::DebugCategory = gst::DebugCategory::new( + "tttocea608", + gst::DebugColorFlags::empty(), + Some("TT CEA 608 Element"), + ); + static ref SPACE: u16 = eia608_from_utf8_1(&[0x20, 0, 0, 0, 0]); +} + +impl TtToCea608 { + fn push_gap(&self, last_frame_no: u64, new_frame_no: u64) { + if last_frame_no < new_frame_no { + let state = self.state.lock().unwrap(); + let (fps_n, fps_d) = ( + *state.framerate.numer() as u64, + *state.framerate.denom() as u64, + ); + let start = (last_frame_no * gst::SECOND) + .mul_div_round(fps_d, fps_n) + .unwrap(); + let end = (new_frame_no * gst::SECOND) + .mul_div_round(fps_d, fps_n) + .unwrap(); + + let event = gst::event::Gap::new(start, end - start); + + drop(state); + + let _ = self.srcpad.push_event(event); + } + } + + fn push_list( + &self, + bufferlist: gst::BufferList, + last_frame_no: u64, + new_frame_no: u64, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + self.push_gap(last_frame_no, new_frame_no); + self.srcpad.push_list(bufferlist) + } + + fn do_erase_display( + &self, + min_frame_no: u64, + mut erase_display_frame_no: u64, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + let mut state = self.state.lock().unwrap(); + + let (fps_n, fps_d) = ( + *state.framerate.numer() as u64, + *state.framerate.denom() as u64, + ); + + let mut bufferlist = gst::BufferList::new(); + + state.last_frame_no = erase_display_frame_no; + + let (pts, duration) = + decrement_pts(min_frame_no, &mut erase_display_frame_no, fps_n, fps_d); + erase_display_memory_with_pts(bufferlist.get_mut().unwrap(), pts, duration); + let (pts, duration) = + decrement_pts(min_frame_no, &mut erase_display_frame_no, fps_n, fps_d); + erase_display_memory_with_pts(bufferlist.get_mut().unwrap(), pts, duration); + + drop(state); + + self.push_list(bufferlist, min_frame_no, erase_display_frame_no) + } + + #[allow(clippy::cognitive_complexity)] + fn sink_chain( + &self, + pad: &gst::Pad, + element: &super::TtToCea608, + buffer: gst::Buffer, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + let pts = match buffer.get_pts() { + gst::CLOCK_TIME_NONE => { + gst_element_error!( + element, + gst::StreamError::Format, + ["Stream with timestamped buffers required"] + ); + Err(gst::FlowError::Error) + } + pts => Ok(pts), + }?; + + let duration = match buffer.get_duration() { + gst::CLOCK_TIME_NONE => { + gst_element_error!( + element, + gst::StreamError::Format, + ["Buffers of stream need to have a duration"] + ); + Err(gst::FlowError::Error) + } + duration => Ok(duration), + }?; + + let mut state = self.state.lock().unwrap(); + let mut buffers = vec![]; + + if state.send_roll_up { + erase_display_memory(&mut buffers); + match state.settings.mode { + Mode::RollUp2 => roll_up_2(&mut buffers), + Mode::RollUp3 => roll_up_3(&mut buffers), + Mode::RollUp4 => roll_up_4(&mut buffers), + _ => (), + } + preamble_buffer(&mut buffers, 14, 0); + state.send_roll_up = false; + state.roll_up_column = 0; + } + + let mut row = 13; + let mut col = if state.settings.mode == Mode::PopOn { + 0 + } else { + state.roll_up_column + }; + + if state.settings.mode == Mode::PopOn { + resume_caption_loading(&mut buffers); + erase_non_displayed_memory(&mut buffers); + preamble_buffer(&mut buffers, row, 0); + } + + let data = buffer.map_readable().map_err(|_| { + gst_error!(CAT, obj: pad, "Can't map buffer readable"); + + gst::FlowError::Error + })?; + + let data = std::str::from_utf8(&data).map_err(|err| { + gst_error!(CAT, obj: pad, "Can't decode utf8: {}", err); + + gst::FlowError::Error + })?; + + let mut prev_char: u16 = if state.settings.mode == Mode::PopOn || col == 0 { + 0 + } else if col >= 31 { + match state.settings.mode { + Mode::RollUp2 => roll_up_2(&mut buffers), + Mode::RollUp3 => roll_up_3(&mut buffers), + Mode::RollUp4 => roll_up_4(&mut buffers), + _ => (), + } + carriage_return(&mut buffers); + preamble_buffer(&mut buffers, 14, 0); + col = 0; + 0 + } else { + // In roll-up mode, the typical input will not have surrounding + // whitespaces. This could be improved by detecting whether the + // last character that was output was some sort of whitespace, + // and we could avoid the white space before punctuation, but + // this is complicated by the fact that in some languages, + // some punctuation must be preceded by a white space, eg in + // French that is the case for '?' and '!', but not for '.' or + // ';'. Let's not go down that rabbit hole. + col += 1; + *SPACE + }; + + for mut c in data.chars() { + if c == '\n' && state.settings.mode == Mode::PopOn { + if prev_char != 0 { + buffers.push(buffer_from_cc_data(prev_char)); + prev_char = 0; + } + + row += 1; + + if row > 14 { + break; + } + + preamble_buffer(&mut buffers, row, 0); + + col = 0; + continue; + } else if c == '\n' { + c = ' '; + } else if c == '\r' { + continue; + } + + let mut encoded = [0; 5]; + c.encode_utf8(&mut encoded); + let mut cc_data = eia608_from_utf8_1(&encoded); + + if cc_data == 0 { + gst_warning!(CAT, obj: element, "Not translating UTF8: {}", c); + cc_data = *SPACE; + } + + if is_basicna(prev_char) { + if is_basicna(cc_data) { + bna_buffer(&mut buffers, prev_char, cc_data); + } else if is_westeu(cc_data) { + // extended characters overwrite the previous character, + // so insert a dummy char then write the extended char + bna_buffer(&mut buffers, prev_char, *SPACE); + buffers.push(buffer_from_cc_data(cc_data)); + } else { + buffers.push(buffer_from_cc_data(prev_char)); + buffers.push(buffer_from_cc_data(cc_data)); + } + prev_char = 0; + } else if is_westeu(cc_data) { + // extended characters overwrite the previous character, + // so insert a dummy char then write the extended char + buffers.push(buffer_from_cc_data(*SPACE)); + buffers.push(buffer_from_cc_data(cc_data)); + } else if is_basicna(cc_data) { + prev_char = cc_data; + } else { + buffers.push(buffer_from_cc_data(cc_data)); + } + + if is_specialna(cc_data) { + resume_caption_loading(&mut buffers); + } + + col += 1; + + if col > 32 && state.settings.mode == Mode::PopOn { + gst_warning!( + CAT, + obj: element, + "Dropping character after 32nd column: {}", + c + ); + continue; + } else if col == 32 && state.settings.mode != Mode::PopOn { + if prev_char != 0 { + buffers.push(buffer_from_cc_data(prev_char)); + prev_char = 0; + } + + match state.settings.mode { + Mode::RollUp2 => roll_up_2(&mut buffers), + Mode::RollUp3 => roll_up_3(&mut buffers), + Mode::RollUp4 => roll_up_4(&mut buffers), + _ => (), + } + + carriage_return(&mut buffers); + preamble_buffer(&mut buffers, 14, 0); + col = 0; + } + } + + if prev_char != 0 { + buffers.push(buffer_from_cc_data(prev_char)); + } + + if state.settings.mode == Mode::PopOn { + end_of_caption(&mut buffers); + } else { + state.roll_up_column = col; + } + + let mut bufferlist = gst::BufferList::new(); + + let (fps_n, fps_d) = ( + *state.framerate.numer() as u64, + *state.framerate.denom() as u64, + ); + + /* Calculate the frame for which we want the first of our + * (doubled) end_of_caption control codes to be output + */ + let mut frame_no = (pts.mul_div_round(fps_n, fps_d).unwrap() / gst::SECOND).unwrap(); + + if state.settings.mode == Mode::PopOn { + /* Add 2: One for our second end_of_caption control + * code, another to calculate its duration */ + frame_no += 2; + + /* Store that frame number, so we can make sure not to output + * overlapped timestamps, outputting multiple buffers with + * a 0 duration will break strict line-21 encoding, but + * we should be fine with 608 over 708, as we can encode + * multiple byte pairs into a single frame */ + let mut min_frame_no = state.last_frame_no; + state.last_frame_no = frame_no; + + let mut erase_display_frame_no = { + if state.erase_display_frame_no < Some(frame_no) { + state.erase_display_frame_no + } else { + None + } + }; + + state.erase_display_frame_no = Some( + ((pts + duration).mul_div_round(fps_n, fps_d).unwrap() / gst::SECOND).unwrap() + 2, + ); + + for mut buffer in buffers.drain(..).rev() { + /* Insert display erasure at the correct moment */ + if erase_display_frame_no == Some(frame_no) { + let (pts, duration) = decrement_pts(min_frame_no, &mut frame_no, fps_n, fps_d); + erase_display_memory_with_pts(bufferlist.get_mut().unwrap(), pts, duration); + let (pts, duration) = decrement_pts(min_frame_no, &mut frame_no, fps_n, fps_d); + erase_display_memory_with_pts(bufferlist.get_mut().unwrap(), pts, duration); + + erase_display_frame_no = None; + } + + let (pts, duration) = decrement_pts(min_frame_no, &mut frame_no, fps_n, fps_d); + + let buf_mut = buffer.get_mut().unwrap(); + buf_mut.set_pts(pts); + buf_mut.set_duration(duration); + bufferlist.get_mut().unwrap().insert(0, buffer); + } + drop(state); + + if let Some(erase_display_frame_no) = erase_display_frame_no { + self.do_erase_display(min_frame_no, erase_display_frame_no)?; + min_frame_no = erase_display_frame_no; + } + self.push_list(bufferlist, min_frame_no, frame_no) + .map_err(|err| { + gst_error!(CAT, obj: &self.srcpad, "Pushing buffer returned {:?}", err); + err + }) + } else { + // Make sure our first buffer doesn't overlap with the last + // gap / buffer we pushed + frame_no = std::cmp::max(frame_no, state.last_frame_no); + let start_frame_no = frame_no; + let max_frame_no = + ((pts + duration).mul_div_round(fps_n, fps_d).unwrap() / gst::SECOND).unwrap(); + for mut buffer in buffers.drain(..) { + let (pts, duration) = increment_pts(&mut frame_no, max_frame_no, fps_n, fps_d); + let buf_mut = buffer.get_mut().unwrap(); + buf_mut.set_pts(pts); + buf_mut.set_duration(duration); + bufferlist.get_mut().unwrap().insert(-1, buffer); + } + let last_frame_no = state.last_frame_no; + state.last_frame_no = max_frame_no; + drop(state); + let ret = self.push_list(bufferlist, last_frame_no, start_frame_no); + self.push_gap(frame_no, max_frame_no); + ret + } + } + + fn src_query( + &self, + pad: &gst::Pad, + element: &super::TtToCea608, + query: &mut gst::QueryRef, + ) -> bool { + use gst::QueryView; + + gst_log!(CAT, obj: pad, "Handling query {:?}", query); + + match query.view_mut() { + QueryView::Latency(ref mut q) => { + let mut peer_query = gst::query::Latency::new(); + + let ret = self.sinkpad.peer_query(&mut peer_query); + + if ret { + let state = self.state.lock().unwrap(); + let (live, mut min, mut max) = peer_query.get_result(); + let (fps_n, fps_d) = ( + *state.framerate.numer() as u64, + *state.framerate.denom() as u64, + ); + + if state.settings.mode == Mode::PopOn { + let our_latency: gst::ClockTime = (LATENCY_BUFFERS * gst::SECOND) + .mul_div_round(fps_d, fps_n) + .unwrap(); + + min += our_latency; + max += our_latency; + } else { + /* We introduce at most a one-frame latency due to rounding */ + let our_latency: gst::ClockTime = + gst::SECOND.mul_div_round(fps_d, fps_n).unwrap(); + + min += our_latency; + max += our_latency; + } + + q.set(live, min, max); + } + ret + } + _ => pad.query_default(Some(element), query), + } + } + + fn sink_event(&self, pad: &gst::Pad, element: &super::TtToCea608, event: gst::Event) -> bool { + gst_log!(CAT, obj: pad, "Handling event {:?}", event); + + use gst::EventView; + + match event.view() { + EventView::Caps(..) => { + let mut downstream_caps = match self.srcpad.get_allowed_caps() { + None => self.srcpad.get_pad_template_caps().unwrap(), + Some(caps) => caps, + }; + + if downstream_caps.is_empty() { + gst_error!(CAT, obj: pad, "Empty downstream caps"); + return false; + } + + let caps = downstream_caps.make_mut(); + let s = caps.get_mut_structure(0).unwrap(); + + s.fixate_field_nearest_fraction( + "framerate", + gst::Fraction::new(DEFAULT_FPS_N, DEFAULT_FPS_D), + ); + s.fixate(); + + let mut state = self.state.lock().unwrap(); + state.framerate = s.get_some::<gst::Fraction>("framerate").unwrap(); + + gst_debug!(CAT, obj: pad, "Pushing caps {}", caps); + + let new_event = gst::event::Caps::new(&downstream_caps); + + drop(state); + + self.srcpad.push_event(new_event) + } + EventView::Gap(e) => { + let mut state = self.state.lock().unwrap(); + let (fps_n, fps_d) = ( + *state.framerate.numer() as u64, + *state.framerate.denom() as u64, + ); + + let (timestamp, duration) = e.get(); + let mut frame_no = ((timestamp + duration).mul_div_round(fps_n, fps_d).unwrap() + / gst::SECOND) + .unwrap(); + + if state.settings.mode == Mode::PopOn { + if frame_no < LATENCY_BUFFERS { + return true; + } + + frame_no -= LATENCY_BUFFERS; + + if let Some(erase_display_frame_no) = state.erase_display_frame_no { + if erase_display_frame_no <= frame_no { + let min_frame_no = state.last_frame_no; + state.erase_display_frame_no = None; + + drop(state); + + /* Ignore return value, we may be flushing here and can't + * communicate that through a boolean + */ + let _ = self.do_erase_display(min_frame_no, erase_display_frame_no); + } + } else { + let last_frame_no = state.last_frame_no; + state.last_frame_no = frame_no; + drop(state); + self.push_gap(last_frame_no, frame_no); + } + } else { + let last_frame_no = state.last_frame_no; + state.last_frame_no = frame_no; + drop(state); + self.push_gap(last_frame_no, frame_no); + } + + true + } + EventView::Eos(_) => { + let mut state = self.state.lock().unwrap(); + if let Some(erase_display_frame_no) = state.erase_display_frame_no { + let min_frame_no = state.last_frame_no; + state.erase_display_frame_no = None; + + drop(state); + + /* Ignore return value, we may be flushing here and can't + * communicate that through a boolean + */ + let _ = self.do_erase_display(min_frame_no, erase_display_frame_no); + } + pad.event_default(Some(element), event) + } + EventView::FlushStop(_) => { + let mut state = self.state.lock().unwrap(); + + if state.settings.mode != Mode::PopOn { + state.send_roll_up = true; + } + + pad.event_default(Some(element), event) + } + _ => pad.event_default(Some(element), event), + } + } +} + +impl ObjectSubclass for TtToCea608 { + const NAME: &'static str = "TtToCea608"; + type Type = super::TtToCea608; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn with_class(klass: &Self::Class) -> Self { + let templ = klass.get_pad_template("sink").unwrap(); + let sinkpad = gst::Pad::builder_with_template(&templ, Some("sink")) + .chain_function(|pad, parent, buffer| { + TtToCea608::catch_panic_pad_function( + parent, + || Err(gst::FlowError::Error), + |this, element| this.sink_chain(pad, element, buffer), + ) + }) + .event_function(|pad, parent, event| { + TtToCea608::catch_panic_pad_function( + parent, + || false, + |this, element| this.sink_event(pad, element, event), + ) + }) + .flags(gst::PadFlags::FIXED_CAPS) + .build(); + + let templ = klass.get_pad_template("src").unwrap(); + let srcpad = gst::Pad::builder_with_template(&templ, Some("src")) + .query_function(|pad, parent, query| { + TtToCea608::catch_panic_pad_function( + parent, + || false, + |this, element| this.src_query(pad, element, query), + ) + }) + .flags(gst::PadFlags::FIXED_CAPS) + .build(); + + Self { + srcpad, + sinkpad, + state: Mutex::new(State::default()), + settings: Mutex::new(Settings::default()), + } + } + + fn class_init(klass: &mut Self::Class) { + klass.set_metadata( + "TT to CEA-608", + "Generic", + "Converts timed text to CEA-608 Closed Captions", + "Mathieu Duponchelle <mathieu@centricular.com>", + ); + + let caps = gst::Caps::builder("text/x-raw").build(); + + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + + let framerate = gst::FractionRange::new( + gst::Fraction::new(1, std::i32::MAX), + gst::Fraction::new(std::i32::MAX, 1), + ); + + let caps = gst::Caps::builder("closedcaption/x-cea-608") + .field("format", &"raw") + .field("framerate", &framerate) + .build(); + + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + + klass.install_properties(&PROPERTIES); + } +} + +impl ObjectImpl for TtToCea608 { + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + + obj.add_pad(&self.sinkpad).unwrap(); + obj.add_pad(&self.srcpad).unwrap(); + } + + fn set_property(&self, _obj: &Self::Type, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("mode", ..) => { + let mut settings = self.settings.lock().unwrap(); + settings.mode = value.get_some::<Mode>().expect("type checked upstream"); + } + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &Self::Type, id: usize) -> Result<glib::Value, ()> { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("mode", ..) => { + let settings = self.settings.lock().unwrap(); + Ok(settings.mode.to_value()) + } + _ => unimplemented!(), + } + } +} + +impl ElementImpl for TtToCea608 { + fn change_state( + &self, + element: &Self::Type, + transition: gst::StateChange, + ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { + gst_trace!(CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::ReadyToPaused => { + let mut state = self.state.lock().unwrap(); + let settings = self.settings.lock().unwrap(); + *state = State::default(); + state.settings = settings.clone(); + if state.settings.mode != Mode::PopOn { + state.send_roll_up = true; + } + } + _ => (), + } + + let ret = self.parent_change_state(element, transition)?; + + match transition { + gst::StateChange::PausedToReady => { + let mut state = self.state.lock().unwrap(); + *state = State::default(); + } + _ => (), + } + + Ok(ret) + } +} |