diff --git a/docs/plugins/gst_plugins_cache.json b/docs/plugins/gst_plugins_cache.json index cc249033..09c9e8df 100644 --- a/docs/plugins/gst_plugins_cache.json +++ b/docs/plugins/gst_plugins_cache.json @@ -5732,6 +5732,20 @@ } }, "properties": { + "cea608-channel": { + "blurb": "Write CEA 608 compatibility bytes with this channel, 0 = disabled (only 1 and 3 currently supported)", + "conditionally-available": false, + "construct": false, + "construct-only": false, + "controllable": false, + "default": "0", + "max": "4", + "min": "0", + "mutable": "null", + "readable": true, + "type": "guint", + "writable": true + }, "mode": { "blurb": "Which mode to operate in", "conditionally-available": false, @@ -5772,6 +5786,20 @@ "type": "gint", "writable": true }, + "roll-up-rows": { + "blurb": "Number of rows to use in roll up mode", + "conditionally-available": false, + "construct": false, + "construct-only": false, + "controllable": false, + "default": "2", + "max": "31", + "min": "0", + "mutable": "playing", + "readable": true, + "type": "guint", + "writable": true + }, "roll-up-timeout": { "blurb": "Duration after which to erase display memory in roll-up mode", "conditionally-available": false, diff --git a/video/closedcaption/src/cea708utils.rs b/video/closedcaption/src/cea708utils.rs index 001f3ee0..fca2d6df 100644 --- a/video/closedcaption/src/cea708utils.rs +++ b/video/closedcaption/src/cea708utils.rs @@ -88,6 +88,7 @@ pub fn textstyle_to_pen_color(style: TextStyle) -> SetPenColorArgs { } } +#[derive(Debug)] pub(crate) struct Cea708ServiceWriter { codes: Vec, service_no: u8, diff --git a/video/closedcaption/src/lib.rs b/video/closedcaption/src/lib.rs index 6ce3ab5e..99c8ec42 100644 --- a/video/closedcaption/src/lib.rs +++ b/video/closedcaption/src/lib.rs @@ -47,7 +47,10 @@ mod ttutils; fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { #[cfg(feature = "doc")] - cea608utils::Cea608Mode::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty()); + { + cea608utils::Cea608Mode::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty()); + cea708utils::Cea708Mode::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty()); + } mcc_parse::register(plugin)?; mcc_enc::register(plugin)?; scc_parse::register(plugin)?; diff --git a/video/closedcaption/src/tttocea708/imp.rs b/video/closedcaption/src/tttocea708/imp.rs index 95bc384a..72493d97 100644 --- a/video/closedcaption/src/tttocea708/imp.rs +++ b/video/closedcaption/src/tttocea708/imp.rs @@ -1,4 +1,5 @@ // Copyright (C) 2020 Mathieu Duponchelle +// Copyright (C) 2023 Matthew Waters // // This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at @@ -6,9 +7,6 @@ // // SPDX-License-Identifier: MPL-2.0 -use cea708_types::CCDataWriter; -use cea708_types::DTVCCPacket; -use cea708_types::Framerate; use gst::glib; use gst::prelude::*; use gst::subclass::prelude::*; @@ -16,32 +14,31 @@ use gst::subclass::prelude::*; use once_cell::sync::Lazy; use crate::cea608utils::Cea608Mode; +use crate::tttocea708::translate::DEFAULT_FPS_D; +use crate::tttocea708::translate::DEFAULT_FPS_N; use std::sync::Mutex; -use cea708_types::tables::*; - use crate::cea608utils::TextStyle; -use crate::cea708utils::{ - textstyle_foreground_color, textstyle_to_pen_color, Cea708Mode, Cea708ServiceWriter, -}; +use crate::cea708utils::Cea708Mode; use crate::ttutils::{Chunk, Line, Lines}; -const DEFAULT_FPS_N: i32 = 30; -const DEFAULT_FPS_D: i32 = 1; +use super::translate::TextToCea708; const DEFAULT_MODE: Cea708Mode = Cea708Mode::RollUp; const DEFAULT_ORIGIN_ROW: i32 = -1; const DEFAULT_ORIGIN_COLUMN: u32 = 0; const DEFAULT_ROLL_UP_ROWS: u8 = 2; const DEFAULT_SERVICE_NO: u8 = 1; +const DEFAULT_CEA608_CHANNEL: u8 = 0; #[derive(Debug, Clone)] struct Settings { mode: Cea708Mode, service_no: u8, + cea608_channel: u8, roll_up_rows: u8, - origin_row: i32, origin_column: u32, + origin_row: i32, roll_up_timeout: Option, } @@ -50,53 +47,31 @@ impl Default for Settings { Settings { mode: DEFAULT_MODE, origin_row: DEFAULT_ORIGIN_ROW, - origin_column: DEFAULT_ORIGIN_COLUMN, roll_up_rows: DEFAULT_ROLL_UP_ROWS, roll_up_timeout: gst::ClockTime::NONE, service_no: DEFAULT_SERVICE_NO, + cea608_channel: DEFAULT_CEA608_CHANNEL, + origin_column: DEFAULT_ORIGIN_COLUMN, } } } +#[derive(Debug)] struct State { - sequence_no: u8, - cc_data_writer: CCDataWriter, + translator: TextToCea708, framerate: gst::Fraction, - service_writer: Cea708ServiceWriter, - pen_location: SetPenLocationArgs, - pen_color: SetPenColorArgs, - pen_attributes: SetPenAttributesArgs, - mode: Cea708Mode, - erase_display_frame_no: Option, last_frame_no: u64, max_frame_no: u64, - send_roll_up_preamble: bool, force_clear: bool, } impl Default for State { fn default() -> Self { Self { - sequence_no: 0, - cc_data_writer: CCDataWriter::default(), + translator: TextToCea708::default(), framerate: gst::Fraction::new(DEFAULT_FPS_N, DEFAULT_FPS_D), - pen_color: textstyle_to_pen_color(TextStyle::White), - pen_attributes: SetPenAttributesArgs::new( - PenSize::Standard, - FontStyle::Default, - TextTag::Dialog, - TextOffset::Normal, - false, - false, - EdgeType::None, - ), - pen_location: SetPenLocationArgs::new(0, 0), - service_writer: Cea708ServiceWriter::new(0), - erase_display_frame_no: None, last_frame_no: 0, max_frame_no: 0, - send_roll_up_preamble: false, - mode: Cea708Mode::PopOn, force_clear: false, } } @@ -121,80 +96,6 @@ fn cc_data_buffer(data: &[u8], pts: gst::ClockTime, duration: gst::ClockTime) -> ret } -fn fraction_to_framerate(fraction: gst::Fraction) -> Framerate { - Framerate::new(fraction.numer() as u32, fraction.denom() as u32) -} - -impl State { - fn check_erase_display(&mut self) -> bool { - if let Some(erase_display_frame_no) = self.erase_display_frame_no { - if self.last_frame_no == erase_display_frame_no - 1 { - self.erase_display_frame_no = None; - self.send_roll_up_preamble = true; - self.service_writer.clear_current_window(); - return true; - } - } - - false - } - - fn cc_data(&mut self, imp: &TtToCea708, bufferlist: &mut gst::BufferListRef) { - self.check_erase_display(); - - let (fps_n, fps_d) = (self.framerate.numer() as u64, self.framerate.denom() as u64); - - let pts = self - .last_frame_no - .seconds() - .mul_div_round(fps_d, fps_n) - .unwrap(); - - if self.last_frame_no < self.max_frame_no { - self.last_frame_no += 1; - } else { - gst::debug!(CAT, imp: imp, "More text than bandwidth!"); - } - - let next_pts = self - .last_frame_no - .seconds() - .mul_div_round(fps_d, fps_n) - .unwrap(); - - let duration = next_pts - pts; - - let seq_no = self.sequence_no; - self.sequence_no = (self.sequence_no + 1) & 0x3; - - let mut packet = DTVCCPacket::new(seq_no); - gst::trace!(CAT, "New packet {}", packet.sequence_no()); - while let Some(service) = self.service_writer.take_service(packet.free_space()) { - gst::trace!(CAT, "adding service {service:?} to packet"); - packet.push_service(service).unwrap(); - } - gst::trace!(CAT, "push packet to writer"); - self.cc_data_writer.push_packet(packet); - - let mut cc_data = vec![]; - gst::trace!(CAT, "write packet to data"); - self.cc_data_writer - .write(fraction_to_framerate(self.framerate), &mut cc_data) - .unwrap(); - - gst::trace!(CAT, "add data to buffer list"); - bufferlist.insert(-1, cc_data_buffer(&cc_data[2..], pts, duration)); - } - - fn pad(&mut self, imp: &TtToCea708, bufferlist: &mut gst::BufferListRef, frame_no: u64) { - while self.last_frame_no < frame_no { - if !self.check_erase_display() { - self.cc_data(imp, bufferlist); - } - } - } -} - pub struct TtToCea708 { srcpad: gst::Pad, sinkpad: gst::Pad, @@ -205,263 +106,50 @@ pub struct TtToCea708 { } impl TtToCea708 { - fn open_line( - &self, - state: &mut State, - settings: &Settings, - chunk: &Chunk, - carriage_return: Option, - ) { - let do_preamble = match state.mode { - Cea708Mode::PopOn | Cea708Mode::PaintOn => true, - Cea708Mode::RollUp => { - if let Some(carriage_return) = carriage_return { - if carriage_return { - state.service_writer.push_codes(&[Code::CR]); - state.pen_location.column = settings.origin_column as u8; - true - } else { - state.send_roll_up_preamble - } - } else { - state.send_roll_up_preamble - } - } - }; - - if do_preamble { - if state.mode == Cea708Mode::RollUp { - state - .service_writer - .rollup_preamble(settings.roll_up_rows, 15); - } - - state.send_roll_up_preamble = false; - } - - let mut need_pen_attributes = false; - if state.pen_attributes.italics != chunk.style.is_italics() { - need_pen_attributes = true; - state.pen_attributes.italics = chunk.style.is_italics(); - } - - if state.pen_attributes.underline != (chunk.underline) { - need_pen_attributes = true; - state.pen_attributes.underline = chunk.underline; - } - - if need_pen_attributes { - state - .service_writer - .set_pen_attributes(state.pen_attributes); - } - - if state.pen_color.foreground_color != textstyle_foreground_color(chunk.style) { - state.pen_color.foreground_color = textstyle_foreground_color(chunk.style); - state.service_writer.set_pen_color(state.pen_color); - } - } - - fn peek_word_length(&self, chars: std::iter::Peekable) -> u32 { - chars.take_while(|c| !c.is_ascii_whitespace()).count() as u32 - } - fn generate( &self, state: &mut State, - settings: &Settings, pts: gst::ClockTime, duration: gst::ClockTime, lines: Lines, - ) -> Result { - let origin_column = settings.origin_column; - let mut row = 13; - let mut bufferlist = gst::BufferList::new(); - let mut_list = bufferlist.get_mut().unwrap(); - - state.service_writer = Cea708ServiceWriter::new(settings.service_no); - - if state.mode == Cea708Mode::PopOn || state.mode == Cea708Mode::PaintOn { - state.pen_location.column = 0; + ) { + let (fps_n, fps_d) = { + let f = state.translator.framerate(); + (f.numer() as u64, f.denom() as u64) }; - let (fps_n, fps_d) = ( - state.framerate.numer() as u64, - state.framerate.denom() as u64, - ); - let frame_no = pts.mul_div_round(fps_n, fps_d).unwrap().seconds(); - if state.last_frame_no == 0 { - gst::debug!(CAT, imp: self, "Initial skip to frame no {}", frame_no); - state.last_frame_no = pts.mul_div_floor(fps_n, fps_d).unwrap().seconds(); - } - - state.max_frame_no = (pts + duration) + let max_frame_no = (pts + duration) .mul_div_round(fps_n, fps_d) .unwrap() .seconds(); - state.pad(self, mut_list, frame_no); + state.translator.generate(frame_no, max_frame_no, lines); + } - let mut cleared = false; - let mut need_pen_location = false; - if let Some(mode) = lines.mode { - if (mode.is_rollup() && state.mode != Cea708Mode::RollUp) - || (mode == Cea608Mode::PaintOn && state.mode != Cea708Mode::PaintOn) - || (mode == Cea608Mode::PopOn && state.mode == Cea708Mode::PopOn) - { - /* Always erase the display when going to or from pop-on */ - if state.mode == Cea708Mode::PopOn || mode == Cea608Mode::PopOn { - state.erase_display_frame_no = None; - state.service_writer.clear_current_window(); - cleared = true; - } + fn pop_bufferlist(&self, state: &mut State) -> gst::BufferList { + let (fps_n, fps_d) = { + let f = state.translator.framerate(); + (f.numer() as u64, f.denom() as u64) + }; - state.mode = match mode { - Cea608Mode::PopOn => Cea708Mode::PopOn, - Cea608Mode::PaintOn => Cea708Mode::PaintOn, - Cea608Mode::RollUp2 | Cea608Mode::RollUp3 | Cea608Mode::RollUp4 => { - Cea708Mode::RollUp - } - }; - match state.mode { - Cea708Mode::RollUp => { - state.send_roll_up_preamble = true; - } - _ => { - state.pen_location.column = origin_column as u8; - need_pen_location = true; - } - } - } + let mut bufferlist = gst::BufferList::new(); + let mut_list = bufferlist.get_mut().unwrap(); + while let Some(cea708) = state.translator.pop_output() { + // TODO: handle framerate changes + let pts = cea708 + .frame_no + .mul_div_round(fps_d * gst::ClockTime::SECOND.nseconds(), fps_n) + .unwrap() + .nseconds(); + let duration = 1 + .mul_div_round(fps_d * gst::ClockTime::SECOND.nseconds(), fps_n) + .unwrap() + .nseconds(); + mut_list.add(cc_data_buffer(&cea708.packet, pts, duration)); } - - if let Some(clear) = lines.clear { - if clear && !cleared { - state.erase_display_frame_no = None; - state.service_writer.clear_current_window(); - if state.mode != Cea708Mode::PopOn && state.mode != Cea708Mode::PaintOn { - state.send_roll_up_preamble = true; - } - state.pen_location.column = origin_column as u8; - need_pen_location = true; - } - } - - if state.mode == Cea708Mode::PopOn { - state.service_writer.popon_preamble(); - } else if state.mode == Cea708Mode::PaintOn { - state.service_writer.paint_on_preamble(); - } - - for line in &lines.lines { - gst::log!(CAT, imp: self, "Processing {:?}", line); - - if let Some(line_row) = line.row { - row = line_row; - } - - if row > 14 { - gst::warning!(CAT, imp: self, "Dropping line after 15th row: {:?}", line); - continue; - } - - if let Some(line_column) = line.column { - if state.mode != Cea708Mode::PopOn && state.mode != Cea708Mode::PaintOn { - state.send_roll_up_preamble = true; - } - state.pen_location.column = line_column as u8; - need_pen_location = true; - } else if state.mode == Cea708Mode::PopOn || state.mode == Cea708Mode::PaintOn { - state.pen_location.column = origin_column as u8; - need_pen_location = true; - } - - if state.pen_location.row != row as u8 { - need_pen_location = true; - state.pen_location.row = row as u8; - } - - if need_pen_location { - state.service_writer.set_pen_location(state.pen_location); - } - - for (i, chunk) in line.chunks.iter().enumerate() { - let cr = if i == 0 { Some(true) } else { Some(false) }; - self.open_line(state, settings, chunk, cr); - - let mut chars = chunk.text.chars().peekable(); - - while let Some(c) = chars.next() { - if c == '\r' { - continue; - } - - let code = Code::from_char(c).unwrap_or(Code::Space); - state.service_writer.push_codes(&[code]); - state.pen_location.column += 1; - - if state.mode == Cea708Mode::RollUp { - /* In roll-up mode, we introduce carriage returns automatically. - * Instead of always wrapping once the last column is reached, we - * want to look ahead and check whether the following word will fit - * on the current row. If it won't, we insert a carriage return, - * unless it won't fit on a full row either, in which case it will need - * to be broken up. - */ - let next_word_length = if c.is_ascii_whitespace() { - self.peek_word_length(chars.clone()) - } else { - 0 - }; - - if (next_word_length <= 32 - origin_column - && state.pen_location.column as u32 + next_word_length > 31) - || state.pen_location.column > 31 - { - state.pen_location.column = settings.origin_column as u8; - state.service_writer.push_codes(&[Code::CR]); - } - } else if state.pen_location.column > 31 { - if chars.peek().is_some() { - gst::warning!( - CAT, - imp: self, - "Dropping characters after 32nd column: {}", - c - ); - } - break; - } - } - } - - if state.mode == Cea708Mode::PopOn || state.mode == Cea708Mode::PaintOn { - row += 1; - } - need_pen_location = false; - } - - if state.mode == Cea708Mode::PopOn { - /* No need to erase the display at this point, end_of_caption will be equivalent */ - state.erase_display_frame_no = None; - state.service_writer.end_of_caption(); - } - - if state.mode == Cea708Mode::PopOn { - state.erase_display_frame_no = - Some(state.last_frame_no + duration.mul_div_round(fps_n, fps_d).unwrap().seconds()); - } else if let Some(timeout) = settings.roll_up_timeout { - state.erase_display_frame_no = - Some(state.last_frame_no + timeout.mul_div_round(fps_n, fps_d).unwrap().seconds()); - } - state.service_writer.push_codes(&[Code::ETX]); - - state.cc_data(self, mut_list); - state.pad(self, mut_list, state.max_frame_no); - - Ok(bufferlist) + bufferlist } fn sink_chain( @@ -546,10 +234,11 @@ impl TtToCea708 { row += 1; } } - - let bufferlist = self.generate(&mut state, &settings, pts, duration, lines)?; - drop(settings); + + self.generate(&mut state, pts, duration, lines); + let bufferlist = self.pop_bufferlist(&mut state); + drop(state); self.srcpad.push_list(bufferlist) @@ -584,7 +273,9 @@ impl TtToCea708 { let caps = gst::Caps::builder_full().structure(s.to_owned()).build(); let mut state = self.state.lock().unwrap(); - state.framerate = s.get::("framerate").unwrap(); + let framerate = s.get::("framerate").unwrap(); + state.framerate = framerate; + state.translator.set_framerate(framerate); gst::debug!(CAT, obj: pad, "Pushing caps {}", caps); @@ -621,10 +312,11 @@ impl TtToCea708 { .seconds(); state.max_frame_no = frame_no; - let mut bufferlist = gst::BufferList::new(); - let mut_list = bufferlist.get_mut().unwrap(); - - state.pad(self, mut_list, frame_no); + let last_frame_no = state.last_frame_no; + state + .translator + .generate(last_frame_no, frame_no, Lines::new_empty()); + let bufferlist = self.pop_bufferlist(&mut state); drop(state); @@ -634,12 +326,15 @@ impl TtToCea708 { } EventView::Eos(_) => { let mut state = self.state.lock().unwrap(); - if let Some(erase_display_frame_no) = state.erase_display_frame_no { - let mut bufferlist = gst::BufferList::new(); - let mut_list = bufferlist.get_mut().unwrap(); - + if let Some(erase_display_frame_no) = state.translator.erase_display_frame_no() { state.max_frame_no = erase_display_frame_no; - state.pad(self, mut_list, erase_display_frame_no); + let last_frame_no = state.translator.last_frame_no(); + state.translator.generate( + last_frame_no, + erase_display_frame_no, + Lines::new_empty(), + ); + let bufferlist = self.pop_bufferlist(&mut state); drop(state); @@ -653,14 +348,21 @@ impl TtToCea708 { EventView::FlushStop(_) => { let mut state = self.state.lock().unwrap(); let settings = self.settings.lock().unwrap(); + let framerate = state.framerate; *state = State::default(); - state.mode = settings.mode; - - if state.mode != Cea708Mode::PopOn { - state.send_roll_up_preamble = true; - } + state.framerate = framerate; + state.translator.set_mode(settings.mode); + state.translator.set_origin_column(settings.origin_column); + state.translator.set_framerate(framerate); + state + .translator + .set_roll_up_timeout(settings.roll_up_timeout); + state.translator.set_roll_up_count(settings.roll_up_rows); + state.translator.set_cea608_channel(settings.cea608_channel); + state.translator.set_service_no(settings.service_no); + state.translator.flush(); drop(settings); drop(state); @@ -749,6 +451,20 @@ impl ObjectImpl for TtToCea708 { .minimum(1) .maximum(63) .build(), + glib::ParamSpecUInt::builder("cea608-channel") + .nick("CEA-608 channel") + .blurb("Write CEA 608 compatibility bytes with this channel, 0 = disabled (only 1 and 3 currently supported)") + .default_value(DEFAULT_CEA608_CHANNEL as u32) + .minimum(0) + .maximum(4) + .build(), + glib::ParamSpecUInt::builder("roll-up-rows") + .nick("Roll Up Rows") + .blurb("Number of rows to use in roll up mode") + .maximum(31) + .default_value(DEFAULT_ORIGIN_COLUMN) + .mutable_playing() + .build(), ] }); @@ -766,37 +482,57 @@ impl ObjectImpl for TtToCea708 { fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { match pspec.name() { "mode" => { + // XXX: Ideally we'd like to not lock the state here let mut state = self.state.lock().unwrap(); let mut settings = self.settings.lock().unwrap(); settings.mode = value.get::().expect("type checked upstream"); state.force_clear = true; } "origin-row" => { + // XXX: Ideally we'd like to not lock the state here let mut state = self.state.lock().unwrap(); let mut settings = self.settings.lock().unwrap(); settings.origin_row = value.get().expect("type checked upstream"); state.force_clear = true; } "origin-column" => { - let mut settings = self.settings.lock().unwrap(); + // XXX: Ideally we'd like to not lock the state here let mut state = self.state.lock().unwrap(); + let mut settings = self.settings.lock().unwrap(); settings.origin_column = value.get().expect("type checked upstream"); state.force_clear = true; - state.pen_location.column = settings.origin_column as u8; + state.translator.set_origin_column(settings.origin_column); + state.translator.set_column(settings.origin_column as u8); } "roll-up-timeout" => { + let mut state = self.state.lock().unwrap(); let mut settings = self.settings.lock().unwrap(); - let timeout = value.get().expect("type checked upstream"); - - settings.roll_up_timeout = match timeout { + let timeout = match value.get().expect("type checked upstream") { u64::MAX => gst::ClockTime::NONE, - _ => Some(timeout.nseconds()), + timeout => Some(timeout.nseconds()), }; + settings.roll_up_timeout = timeout; + state.translator.set_roll_up_timeout(timeout); } "service-number" => { + let mut state = self.state.lock().unwrap(); let mut settings = self.settings.lock().unwrap(); settings.service_no = value.get::().expect("type checked upstream") as u8; + state.translator.set_service_no(settings.service_no); + } + "cea608-channel" => { + let mut state = self.state.lock().unwrap(); + let mut settings = self.settings.lock().unwrap(); + let channel = value.get::().expect("type checked upstream") as u8; + settings.cea608_channel = channel; + state.translator.set_cea608_channel(channel); + } + "roll-up-rows" => { + let mut state = self.state.lock().unwrap(); + let mut settings = self.settings.lock().unwrap(); + settings.roll_up_rows = value.get::().expect("type checked upstream") as u8; + state.translator.set_roll_up_count(settings.roll_up_rows); } _ => unimplemented!(), } @@ -829,6 +565,14 @@ impl ObjectImpl for TtToCea708 { let settings = self.settings.lock().unwrap(); (settings.service_no as u32).to_value() } + "cea608-channel" => { + let settings = self.settings.lock().unwrap(); + (settings.cea608_channel as u32).to_value() + } + "roll-up-rows" => { + let settings = self.settings.lock().unwrap(); + (settings.roll_up_rows as u32).to_value() + } _ => unimplemented!(), } } @@ -858,11 +602,6 @@ impl ElementImpl for TtToCea708 { let s = gst::Structure::builder("text/x-raw").build(); caps.append_structure(s); - /* - let s = gst::Structure::builder("application/x-json") - .field("format", "cea608") - .build(); - caps.append_structure(s);*/ } let sink_pad_template = gst::PadTemplate::new( @@ -908,13 +647,19 @@ impl ElementImpl for TtToCea708 { gst::StateChange::ReadyToPaused => { let mut state = self.state.lock().unwrap(); let settings = self.settings.lock().unwrap(); + let framerate = state.framerate; *state = State::default(); state.force_clear = false; - state.mode = settings.mode; - if state.mode != Cea708Mode::PopOn { - state.send_roll_up_preamble = true; - state.pen_location.column = settings.origin_column as u8; - } + state.translator.set_mode(settings.mode); + state.translator.set_origin_column(settings.origin_column); + state.translator.set_framerate(framerate); + state + .translator + .set_roll_up_timeout(settings.roll_up_timeout); + state.translator.set_column(settings.origin_column as u8); + state.translator.set_cea608_channel(settings.cea608_channel); + state.translator.set_service_no(settings.service_no); + state.translator.flush(); } _ => (), } diff --git a/video/closedcaption/src/tttocea708/mod.rs b/video/closedcaption/src/tttocea708/mod.rs index a4389869..0240d210 100644 --- a/video/closedcaption/src/tttocea708/mod.rs +++ b/video/closedcaption/src/tttocea708/mod.rs @@ -10,6 +10,7 @@ use gst::glib; use gst::prelude::*; mod imp; +mod translate; glib::wrapper! { pub struct TtToCea708(ObjectSubclass) @extends gst::Element, gst::Object; diff --git a/video/closedcaption/src/tttocea708/translate.rs b/video/closedcaption/src/tttocea708/translate.rs new file mode 100644 index 00000000..bed40102 --- /dev/null +++ b/video/closedcaption/src/tttocea708/translate.rs @@ -0,0 +1,520 @@ +// Copyright (C) 2020 Mathieu Duponchelle +// Copyright (C) 2023 Matthew Waters +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 + +use std::collections::VecDeque; + +use cea708_types::tables::*; +use cea708_types::*; + +use gst::prelude::*; +use once_cell::sync::Lazy; + +use crate::cea608utils::{Cea608Mode, TextStyle}; +use crate::cea708utils::{ + textstyle_foreground_color, textstyle_to_pen_color, Cea708Mode, Cea708ServiceWriter, +}; +use crate::tttocea608::translate::{TextToCea608, TimedCea608}; +use crate::ttutils::{Chunk, Lines}; + +pub const DEFAULT_FPS_N: i32 = 30; +pub const DEFAULT_FPS_D: i32 = 1; + +static CAT: Lazy = Lazy::new(|| { + gst::DebugCategory::new( + "tttocea708translator", + gst::DebugColorFlags::empty(), + Some("TT CEA 608 translator"), + ) +}); + +fn fraction_to_framerate(fraction: gst::Fraction) -> Framerate { + Framerate::new(fraction.numer() as u32, fraction.denom() as u32) +} + +fn is_punctuation(word: &str) -> bool { + word == "." || word == "," || word == "?" || word == "!" || word == ";" || word == ":" +} + +fn peek_word_length(chars: std::iter::Peekable) -> u32 { + chars.take_while(|c| !c.is_ascii_whitespace()).count() as u32 +} + +#[derive(Debug)] +pub struct TextToCea708 { + cea608: TextToCea608, + + // settings + mode: Cea708Mode, + roll_up_count: u8, + service_no: u8, + cea608_channel: u8, + origin_column: u32, + roll_up_timeout: Option, + framerate: gst::Fraction, + + // state + service_writer: Cea708ServiceWriter, + cc_data_writer: CCDataWriter, + output_packets: VecDeque, + sequence_no: u8, + pen_location: SetPenLocationArgs, + pen_color: SetPenColorArgs, + pen_attributes: SetPenAttributesArgs, + send_roll_up_preamble: bool, + erase_display_frame_no: Option, + last_frame_no: u64, +} + +impl Default for TextToCea708 { + fn default() -> Self { + Self { + cea608: TextToCea608::default(), + mode: Cea708Mode::RollUp, + roll_up_count: 2, + service_no: 1, + cea608_channel: 1, + origin_column: 0, + framerate: gst::Fraction::new(DEFAULT_FPS_N, DEFAULT_FPS_D), + roll_up_timeout: None, + output_packets: VecDeque::new(), + sequence_no: 0, + service_writer: Cea708ServiceWriter::new(1), + cc_data_writer: CCDataWriter::default(), + pen_location: SetPenLocationArgs::new(0, 0), + pen_color: textstyle_to_pen_color(TextStyle::White), + pen_attributes: SetPenAttributesArgs::new( + PenSize::Standard, + FontStyle::Default, + TextTag::Dialog, + TextOffset::Normal, + false, + false, + EdgeType::None, + ), + send_roll_up_preamble: false, + erase_display_frame_no: None, + last_frame_no: 0, + } + } +} + +#[derive(Debug)] +pub struct TimedCea708 { + pub packet: Vec, + pub frame_no: u64, +} + +impl TextToCea708 { + pub fn pop_output(&mut self) -> Option { + self.output_packets.pop_front() + } + + pub fn set_origin_column(&mut self, origin_column: u32) { + self.origin_column = origin_column; + self.cea608.set_origin_column(origin_column); + } + + pub fn set_column(&mut self, column: u8) { + self.pen_location.column = column; + self.cea608.set_column(column); + } + + pub fn set_roll_up_timeout(&mut self, timeout: Option) { + self.roll_up_timeout = timeout; + self.cea608.set_roll_up_timeout(timeout); + } + + pub fn set_roll_up_count(&mut self, roll_up_count: u8) { + self.roll_up_count = roll_up_count; + if self.mode == Cea708Mode::RollUp { + let cea608_mode = match self.roll_up_count { + 0..=2 => Cea608Mode::RollUp2, + 3 => Cea608Mode::RollUp3, + _ => Cea608Mode::RollUp4, + }; + self.cea608.set_mode(cea608_mode); + } + } + + pub fn set_framerate(&mut self, framerate: gst::Fraction) { + self.framerate = framerate; + self.cea608.set_framerate(framerate); + } + + pub fn framerate(&self) -> gst::Fraction { + self.framerate + } + + pub fn set_cea608_channel(&mut self, channel: u8) { + assert!((0..=4).contains(&channel)); + if self.cea608_channel != channel { + self.cea608.flush(); + } + self.cea608_channel = channel; + } + + pub fn set_mode(&mut self, mode: Cea708Mode) { + self.mode = mode; + if self.mode != Cea708Mode::PopOn { + self.send_roll_up_preamble = true; + } + let cea608_mode = match mode { + Cea708Mode::PopOn => Cea608Mode::PopOn, + Cea708Mode::PaintOn => Cea608Mode::PaintOn, + Cea708Mode::RollUp => match self.roll_up_count { + 0..=2 => Cea608Mode::RollUp2, + 3 => Cea608Mode::RollUp3, + _ => Cea608Mode::RollUp4, + }, + }; + self.cea608.set_mode(cea608_mode); + } + + pub fn set_service_no(&mut self, service_no: u8) { + self.service_no = service_no; + } + + pub fn last_frame_no(&self) -> u64 { + self.last_frame_no + } + + pub fn erase_display_frame_no(&self) -> Option { + self.erase_display_frame_no + } + + pub fn flush(&mut self) { + self.erase_display_frame_no = None; + self.output_packets.clear(); + self.send_roll_up_preamble = true; + self.cea608.flush(); + } + + fn open_line(&mut self, chunk: &Chunk, carriage_return: Option) { + let do_preamble = match self.mode { + Cea708Mode::PopOn | Cea708Mode::PaintOn => true, + Cea708Mode::RollUp => { + if let Some(carriage_return) = carriage_return { + if carriage_return { + self.service_writer.push_codes(&[Code::CR]); + self.pen_location.column = self.origin_column as u8; + true + } else { + self.send_roll_up_preamble + } + } else { + self.send_roll_up_preamble + } + } + }; + + if do_preamble { + if self.mode == Cea708Mode::RollUp { + self.service_writer.rollup_preamble(self.roll_up_count, 15); + } + + self.send_roll_up_preamble = false; + } + + let mut need_pen_attributes = false; + if self.pen_attributes.italics != chunk.style.is_italics() { + need_pen_attributes = true; + self.pen_attributes.italics = chunk.style.is_italics(); + } + + if self.pen_attributes.underline != (chunk.underline) { + need_pen_attributes = true; + self.pen_attributes.underline = chunk.underline; + } + + if need_pen_attributes { + self.service_writer.set_pen_attributes(self.pen_attributes); + } + + if self.pen_color.foreground_color != textstyle_foreground_color(chunk.style) { + self.pen_color.foreground_color = textstyle_foreground_color(chunk.style); + self.service_writer.set_pen_color(self.pen_color); + } + } + + fn check_erase_display(&mut self) -> bool { + if let Some(erase_display_frame_no) = self.erase_display_frame_no { + if self.last_frame_no == erase_display_frame_no - 1 { + self.erase_display_frame_no = None; + self.send_roll_up_preamble = true; + self.service_writer.clear_current_window(); + return true; + } + } + + false + } + + fn cc_data(&mut self) { + self.check_erase_display(); + + self.last_frame_no += 1; + + let seq_no = self.sequence_no; + self.sequence_no = (self.sequence_no + 1) & 0x3; + + let mut packet = DTVCCPacket::new(seq_no); + gst::trace!(CAT, "New packet {}", packet.sequence_no()); + while let Some(service) = self.service_writer.take_service(packet.free_space()) { + gst::trace!(CAT, "adding service {service:?} to packet"); + packet.push_service(service).unwrap(); + } + gst::trace!(CAT, "push packet to writer"); + self.cc_data_writer.push_packet(packet); + if self.cea608_channel > 0 { + let tcea608 = self.cea608.pop_output().unwrap_or(TimedCea608 { + cea608: 0x8080, + frame_no: self.last_frame_no, + }); + let (byte0, byte1) = ( + ((tcea608.cea608 & 0xff00) >> 8) as u8, + (tcea608.cea608 & 0xff) as u8, + ); + match self.cea608_channel { + 1 | 2 => self + .cc_data_writer + .push_cea608(cea708_types::Cea608::Field1(byte0, byte1)), + 3 | 4 => self + .cc_data_writer + .push_cea608(cea708_types::Cea608::Field2(byte0, byte1)), + _ => (), + } + } + + let mut cc_data = vec![]; + gst::trace!(CAT, "write packet to data"); + self.cc_data_writer + .write(fraction_to_framerate(self.framerate), &mut cc_data) + .unwrap(); + + gst::trace!(CAT, "add data to buffer list"); + self.output_packets.push_back(TimedCea708 { + packet: cc_data[2..].to_vec(), + frame_no: self.last_frame_no, + }); + } + + fn pad(&mut self, frame_no: u64) { + while self.last_frame_no < frame_no { + if !self.check_erase_display() { + self.cc_data(); + } + } + } + + // XXX: range for frame_no? + pub fn generate(&mut self, frame_no: u64, end_frame_no: u64, lines: Lines) { + let origin_column = self.origin_column; + let mut row = 13; + + if self.last_frame_no == 0 { + gst::debug!(CAT, "Initial skip to frame no {}", frame_no); + self.last_frame_no = frame_no; + } + + gst::trace!( + CAT, + "generate from frame {frame_no} to {end_frame_no}, erase frame no: {:?}", + self.erase_display_frame_no + ); + + let frame_no = frame_no.max(self.last_frame_no); + let end_frame_no = end_frame_no.max(frame_no); + + if self.cea608_channel > 0 { + self.cea608.generate(frame_no, end_frame_no, lines.clone()); + } + + self.service_writer = Cea708ServiceWriter::new(self.service_no); + + if self.mode == Cea708Mode::PopOn || self.mode == Cea708Mode::PaintOn { + self.pen_location.column = 0; + }; + + let (fps_n, fps_d) = (self.framerate.numer() as u64, self.framerate.denom() as u64); + + self.pad(frame_no); + + let mut cleared = false; + let mut need_pen_location = false; + if let Some(mode) = lines.mode { + if (mode.is_rollup() && self.mode != Cea708Mode::RollUp) + || (mode == Cea608Mode::PaintOn && self.mode != Cea708Mode::PaintOn) + || (mode == Cea608Mode::PopOn && self.mode == Cea708Mode::PopOn) + { + /* Always erase the display when going to or from pop-on */ + if self.mode == Cea708Mode::PopOn || mode == Cea608Mode::PopOn { + self.erase_display_frame_no = None; + self.service_writer.clear_current_window(); + cleared = true; + } + + self.mode = match mode { + Cea608Mode::PopOn => Cea708Mode::PopOn, + Cea608Mode::PaintOn => Cea708Mode::PaintOn, + Cea608Mode::RollUp2 | Cea608Mode::RollUp3 | Cea608Mode::RollUp4 => { + Cea708Mode::RollUp + } + }; + match self.mode { + Cea708Mode::RollUp => { + self.send_roll_up_preamble = true; + } + _ => { + self.pen_location.column = origin_column as u8; + need_pen_location = true; + } + } + } + } + + if let Some(clear) = lines.clear { + if clear && !cleared { + self.erase_display_frame_no = None; + self.service_writer.clear_current_window(); + if self.mode != Cea708Mode::PopOn && self.mode != Cea708Mode::PaintOn { + self.send_roll_up_preamble = true; + } + self.pen_location.column = origin_column as u8; + need_pen_location = true; + } + } + + if !lines.lines.is_empty() { + if self.mode == Cea708Mode::PopOn { + self.service_writer.popon_preamble(); + } else if self.mode == Cea708Mode::PaintOn { + self.service_writer.paint_on_preamble(); + } + } + + for line in &lines.lines { + gst::log!(CAT, "Processing {:?}", line); + + if let Some(line_row) = line.row { + row = line_row; + } + + if row > 14 { + gst::warning!(CAT, "Dropping line after 15th row: {:?}", line); + continue; + } + + if let Some(line_column) = line.column { + if self.mode != Cea708Mode::PopOn && self.mode != Cea708Mode::PaintOn { + self.send_roll_up_preamble = true; + } + self.pen_location.column = line_column as u8; + need_pen_location = true; + } else if self.mode == Cea708Mode::PopOn || self.mode == Cea708Mode::PaintOn { + self.pen_location.column = origin_column as u8; + need_pen_location = true; + } + + if self.pen_location.row != row as u8 { + need_pen_location = true; + self.pen_location.row = row as u8; + } + + if need_pen_location { + self.service_writer.set_pen_location(self.pen_location); + } + + for (i, chunk) in line.chunks.iter().enumerate() { + let (cr, mut prepend_space) = if i == 0 { + (line.carriage_return, true) + } else { + (Some(false), false) + }; + self.open_line(chunk, cr); + + if is_punctuation(&chunk.text) { + prepend_space = false; + } + + let text = { + if prepend_space { + let mut text = " ".to_string(); + text.push_str(&chunk.text); + text + } else { + chunk.text.clone() + } + }; + let mut chars = text.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '\r' { + continue; + } + + let code = Code::from_char(c).unwrap_or(Code::Space); + self.service_writer.push_codes(&[code]); + self.pen_location.column += 1; + + if self.mode == Cea708Mode::RollUp { + /* In roll-up mode, we introduce carriage returns automatically. + * Instead of always wrapping once the last column is reached, we + * want to look ahead and check whether the following word will fit + * on the current row. If it won't, we insert a carriage return, + * unless it won't fit on a full row either, in which case it will need + * to be broken up. + */ + let next_word_length = if c.is_ascii_whitespace() { + peek_word_length(chars.clone()) + } else { + 0 + }; + + if (next_word_length <= 32 - origin_column + && self.pen_location.column as u32 + next_word_length > 31) + || self.pen_location.column > 31 + { + self.pen_location.column = self.origin_column as u8; + self.service_writer.push_codes(&[Code::CR]); + } + } else if self.pen_location.column > 31 { + if chars.peek().is_some() { + gst::warning!(CAT, "Dropping characters after 32nd column: {}", c); + } + break; + } + } + } + + if self.mode == Cea708Mode::PopOn || self.mode == Cea708Mode::PaintOn { + row += 1; + } + need_pen_location = false; + } + + if !lines.lines.is_empty() { + if self.mode == Cea708Mode::PopOn { + /* No need to erase the display at this point, end_of_caption will be equivalent */ + self.erase_display_frame_no = None; + self.service_writer.end_of_caption(); + } + self.service_writer.push_codes(&[Code::ETX]); + } + self.cc_data(); + + if self.mode == Cea708Mode::PopOn { + self.erase_display_frame_no = + Some(self.last_frame_no + end_frame_no.saturating_sub(frame_no)); + } else if let Some(timeout) = self.roll_up_timeout { + self.erase_display_frame_no = + Some(self.last_frame_no + timeout.mul_div_round(fps_n, fps_d).unwrap().seconds()); + } + self.pad(end_frame_no); + } +}