From fea85ff9c8fa9dad2f8a1ecbe65a8279d6ff5f6c Mon Sep 17 00:00:00 2001 From: Matthew Waters Date: Thu, 14 Mar 2024 13:41:15 +1100 Subject: [PATCH] closedcaption: use cea608-types for parsing 608 captions instead of libcaption Part-of: --- Cargo.lock | 14 + video/closedcaption/Cargo.toml | 4 +- video/closedcaption/src/cea608tocea708/imp.rs | 147 ++++--- video/closedcaption/src/cea608tojson/imp.rs | 277 ++++++------ video/closedcaption/src/cea608utils.rs | 396 +++--------------- video/closedcaption/src/cea708utils.rs | 36 +- video/closedcaption/src/tttocea608/imp.rs | 32 +- .../closedcaption/src/tttocea608/translate.rs | 268 +++++------- video/closedcaption/src/tttocea708/imp.rs | 24 +- .../closedcaption/src/tttocea708/translate.rs | 30 +- video/closedcaption/tests/cea608tocea708.rs | 28 +- 11 files changed, 471 insertions(+), 785 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd69693c..d6e793c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1002,6 +1002,18 @@ dependencies = [ "image", ] +[[package]] +name = "cea608-types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf75f36b48b2e9701d278a3ab9081abd1b2881d913ad7b3b0e4d9c6e8c59f86" +dependencies = [ + "env_logger 0.10.2", + "log", + "once_cell", + "thiserror", +] + [[package]] name = "cea708-types" version = "0.3.1" @@ -2272,6 +2284,7 @@ dependencies = [ "byteorder", "cairo-rs", "cc", + "cea608-types", "cea708-types", "chrono", "either", @@ -2287,6 +2300,7 @@ dependencies = [ "rand", "serde", "serde_json", + "smallvec", "uuid", "winnow 0.6.5", ] diff --git a/video/closedcaption/Cargo.toml b/video/closedcaption/Cargo.toml index 6d2d5d9b..24d066e6 100644 --- a/video/closedcaption/Cargo.toml +++ b/video/closedcaption/Cargo.toml @@ -20,12 +20,14 @@ pangocairo.workspace = true byteorder = "1" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["raw_value"] } -cea708-types = "0.3" +cea708-types = "0.3.1" +cea608-types = "0.1.1" once_cell.workspace = true gst = { workspace = true, features = ["v1_16"]} gst-base = { workspace = true, features = ["v1_18"]} gst-video = { workspace = true, features = ["v1_16"]} winnow = "0.6" +smallvec = "1" [dev-dependencies] pretty_assertions = "1" diff --git a/video/closedcaption/src/cea608tocea708/imp.rs b/video/closedcaption/src/cea608tocea708/imp.rs index 7cb12a8a..dcbc8fc3 100644 --- a/video/closedcaption/src/cea608tocea708/imp.rs +++ b/video/closedcaption/src/cea608tocea708/imp.rs @@ -6,6 +6,7 @@ // // SPDX-License-Identifier: MPL-2.0 +use cea608_types::tables::{Channel, PreambleAddressCode}; use cea708_types::{tables::*, DTVCCPacket}; use cea708_types::{CCDataWriter, Framerate}; use gst::glib; @@ -15,7 +16,11 @@ use gst::subclass::prelude::*; use atomic_refcell::AtomicRefCell; use crate::cea608utils::*; -use crate::cea708utils::{textstyle_foreground_color, textstyle_to_pen_color, Cea708ServiceWriter}; +use crate::cea708utils::{ + cea608_color_to_foreground_color, textstyle_to_pen_color, Cea708ServiceWriter, +}; + +use cea608_types::{Cea608, Cea608State as Cea608StateTracker}; use once_cell::sync::Lazy; @@ -56,7 +61,7 @@ impl Default for Cea608ServiceState { fn default() -> Self { Self { mode: None, - base_row: 15, + base_row: 14, } } } @@ -116,8 +121,8 @@ impl Cea708ServiceState { self.pen_attributes.italics = false; } - fn handle_text(&mut self, text: Cea608Text) { - if text.code_space == CodeSpace::WestEU { + fn handle_text(&mut self, text: cea608_types::Text) { + if text.needs_backspace { self.writer.push_codes(&[Code::BS]); } if let Some(c) = text.char1 { @@ -136,18 +141,18 @@ impl Cea708ServiceState { } } - fn handle_preamble(&mut self, preamble: Preamble) { + fn handle_preamble(&mut self, preamble: PreambleAddressCode) { let mut need_pen_location = false; - // TODO: may need a better algorithm then compressing the top four rows - let new_row = std::cmp::max(0, preamble.row) as u8; + // TODO: may need a better algorithm than compressing the top four rows + let new_row = std::cmp::max(0, preamble.row()); if self.pen_location.row != new_row { need_pen_location = true; self.pen_location.row = new_row; } - if self.pen_location.column != preamble.col as u8 { + if self.pen_location.column != preamble.column() { need_pen_location = true; - self.pen_location.column = preamble.col as u8; + self.pen_location.column = preamble.column(); } if need_pen_location { @@ -155,42 +160,44 @@ impl Cea708ServiceState { } let mut need_pen_attributes = false; - if self.pen_attributes.italics != preamble.style.is_italics() { + if self.pen_attributes.italics != preamble.italics() { need_pen_attributes = true; - self.pen_attributes.italics = preamble.style.is_italics(); + self.pen_attributes.italics = preamble.italics(); } - if self.pen_attributes.underline != (preamble.underline > 0) { + if self.pen_attributes.underline != preamble.underline() { need_pen_attributes = true; - self.pen_attributes.underline = preamble.underline > 0; + self.pen_attributes.underline = preamble.underline(); } if need_pen_attributes { self.writer.set_pen_attributes(self.pen_attributes); } - if self.pen_color.foreground_color != textstyle_foreground_color(preamble.style) { - self.pen_color.foreground_color = textstyle_foreground_color(preamble.style); + if self.pen_color.foreground_color != cea608_color_to_foreground_color(preamble.color()) { + self.pen_color.foreground_color = cea608_color_to_foreground_color(preamble.color()); self.writer.set_pen_color(self.pen_color); } } - fn handle_midrowchange(&mut self, midrowchange: MidRowChange) { + fn handle_midrowchange(&mut self, midrowchange: cea608_types::tables::MidRow) { self.writer.write_char(' '); - if self.pen_color.foreground_color != textstyle_foreground_color(midrowchange.style) { - self.pen_color.foreground_color = textstyle_foreground_color(midrowchange.style); - self.writer.set_pen_color(self.pen_color); + if let Some(color) = midrowchange.color() { + if self.pen_color.foreground_color != cea608_color_to_foreground_color(color) { + self.pen_color.foreground_color = cea608_color_to_foreground_color(color); + self.writer.set_pen_color(self.pen_color); + } } let mut need_pen_attributes = false; - if self.pen_attributes.italics != midrowchange.style.is_italics() { + if self.pen_attributes.italics != midrowchange.italics() { need_pen_attributes = true; - self.pen_attributes.italics = midrowchange.style.is_italics(); + self.pen_attributes.italics = midrowchange.italics(); } - if self.pen_attributes.underline != midrowchange.underline { + if self.pen_attributes.underline != midrowchange.underline() { need_pen_attributes = true; - self.pen_attributes.underline = midrowchange.underline; + self.pen_attributes.underline = midrowchange.underline(); } if need_pen_attributes { @@ -287,12 +294,12 @@ enum BufferOrEvent { } impl State { - fn field_channel_to_index(&self, field: u8, channel: i32) -> usize { + fn field_channel_to_index(&self, field: u8, channel: Channel) -> usize { match (field, channel) { - (0, 0 | 2) => 0, - (0, 1 | 3) => 2, - (1, 0 | 2) => 1, - (1, 1 | 3) => 3, + (0, Channel::ONE) => 0, + (0, Channel::TWO) => 2, + (1, Channel::ONE) => 1, + (1, Channel::TWO) => 3, _ => unreachable!(), } } @@ -300,12 +307,12 @@ impl State { fn service_state_from_608_field_channel( &mut self, field: u8, - channel: i32, + channel: Channel, ) -> &mut Cea708ServiceState { &mut self.cea708.service_state[self.field_channel_to_index(field, channel)] } - fn new_mode(&mut self, field: u8, channel: i32, cea608_mode: Cea608Mode) { + fn new_mode(&mut self, field: u8, channel: Channel, cea608_mode: Cea608Mode) { let idx = self.field_channel_to_index(field, channel); if let Some(old_mode) = self.cea608.service[idx].mode { if cea608_mode.is_rollup() @@ -345,27 +352,20 @@ impl State { self.cea708.service_state[idx].new_mode(cea608_mode, base_row); } - fn handle_cc_data(&mut self, imp: &Cea608ToCea708, field: u8, cc_data: u16) { - self.cea608.tracker[field as usize].push_cc_data(cc_data); - - let mut channel = None; - if let Some(cea608) = self.cea608.tracker[field as usize].pop() { + fn handle_cc_data(&mut self, imp: &Cea608ToCea708, field: u8, cc_data: [u8; 2]) { + if let Ok(Some(cea608)) = self.cea608.tracker[field as usize].decode(cc_data) { gst::trace!( CAT, imp: imp, - "have field:{field} channel:{} {cea608:?}", + "have field:{field} channel:{:?} {cea608:?}", cea608.channel() ); - if !matches!(cea608, Cea608::Duplicate) { - channel = Some(cea608.channel()); - } match cea608 { - Cea608::Duplicate => (), Cea608::NewMode(chan, new_mode) => { - self.new_mode(field, chan, new_mode); + self.new_mode(field, chan, new_mode.into()); } Cea608::Text(text) => { - let state = self.service_state_from_608_field_channel(field, text.chan); + let state = self.service_state_from_608_field_channel(field, text.channel); state.handle_text(text); } Cea608::EndOfCaption(chan) => { @@ -373,8 +373,8 @@ impl State { state.writer.end_of_caption(); state.writer.etx(); } - Cea608::Preamble(mut preamble) => { - let idx = self.field_channel_to_index(field, preamble.chan); + Cea608::Preamble(chan, mut preamble) => { + let idx = self.field_channel_to_index(field, chan); let rollup_count = self.cea608.service[idx] .mode .map(|mode| { @@ -388,21 +388,23 @@ impl State { if rollup_count > 0 { // https://www.law.cornell.edu/cfr/text/47/79.101 (f)(1)(ii) let old_base_row = self.cea608.service[idx].base_row; - self.cea608.service[idx].base_row = preamble.row as u8; - let state = self.service_state_from_608_field_channel(field, preamble.chan); - if old_base_row != preamble.row as u8 { - state - .writer - .rollup_preamble(rollup_count, preamble.row as u8); + self.cea608.service[idx].base_row = preamble.row(); + let state = self.service_state_from_608_field_channel(field, chan); + if old_base_row != preamble.row() { + state.writer.rollup_preamble(rollup_count, preamble.row()); } state.pen_location.row = rollup_count - 1; - preamble.row = rollup_count as i32 - 1; + preamble = PreambleAddressCode::new( + rollup_count - 1, + preamble.underline(), + preamble.code(), + ); } - let state = self.service_state_from_608_field_channel(field, preamble.chan); + let state = self.service_state_from_608_field_channel(field, chan); state.handle_preamble(preamble); } - Cea608::MidRowChange(midrowchange) => { - let state = self.service_state_from_608_field_channel(field, midrowchange.chan); + Cea608::MidRowChange(chan, midrowchange) => { + let state = self.service_state_from_608_field_channel(field, chan); state.handle_midrowchange(midrowchange); } Cea608::Backspace(chan) => { @@ -432,24 +434,23 @@ impl State { Cea608::TabOffset(chan, count) => { let state = self.service_state_from_608_field_channel(field, chan); state.pen_location.column = - std::cmp::min(state.pen_location.column + count as u8, 32); + std::cmp::min(state.pen_location.column + count, 32); } + Cea608::DeleteToEndOfRow(_chan) => (), } - if let Some(channel) = channel { - let idx = self.field_channel_to_index(field, channel); - if let Some( - Cea608Mode::RollUp2 - | Cea608Mode::RollUp3 - | Cea608Mode::RollUp4 - | Cea608Mode::PaintOn, - ) = self.cea608.service[idx].mode - { - // FIXME: actually track state for when things have changed - // and we need to send ETX - self.cea708.service_state[idx] - .writer - .push_codes(&[cea708_types::tables::Code::ETX]); - } + let idx = self.field_channel_to_index(field, cea608.channel()); + if let Some( + Cea608Mode::RollUp2 + | Cea608Mode::RollUp3 + | Cea608Mode::RollUp4 + | Cea608Mode::PaintOn, + ) = self.cea608.service[idx].mode + { + // FIXME: actually track state for when things have changed + // and we need to send ETX + self.cea708.service_state[idx] + .writer + .push_codes(&[cea708_types::tables::Code::ETX]); } } } @@ -529,9 +530,8 @@ impl Cea608ToCea708 { return Ok(gst::FlowSuccess::Ok); } for triple in data.chunks_exact(3) { - let cc_data = (triple[1] as u16) << 8 | triple[2] as u16; let field = (triple[0] & 0x80) >> 7; - state.handle_cc_data(self, field, cc_data); + state.handle_cc_data(self, field, [triple[1], triple[2]]); } data.to_vec() } @@ -559,8 +559,7 @@ impl Cea608ToCea708 { }; let mut s334_1a_data = Vec::with_capacity(data.len() / 2 * 3); for pair in data.chunks_exact(2) { - let cc_data = (pair[0] as u16) << 8 | pair[1] as u16; - state.handle_cc_data(self, field, cc_data); + state.handle_cc_data(self, field, [pair[0], pair[1]]); if field == 0 { s334_1a_data.push(0x00); } else { diff --git a/video/closedcaption/src/cea608tojson/imp.rs b/video/closedcaption/src/cea608tojson/imp.rs index 4eda8472..a67766a0 100644 --- a/video/closedcaption/src/cea608tojson/imp.rs +++ b/video/closedcaption/src/cea608tojson/imp.rs @@ -25,10 +25,14 @@ // * The Chunk object could have an "indent" field, that would get translated // to tab offsets for small bandwidth savings +use cea608_types::tables::Channel; +use cea608_types::tables::MidRow; use gst::glib; use gst::prelude::*; use gst::subclass::prelude::*; +use cea608_types::{Cea608, Cea608State as Cea608StateTracker}; + use crate::cea608utils::*; use crate::ttutils::{Chunk, Line, Lines}; @@ -248,21 +252,24 @@ static CAT: Lazy = Lazy::new(|| { fn dump( imp: &Cea608ToJson, - cc_data: u16, + cc_data: [u8; 2], pts: impl Into>, duration: impl Into>, ) { let pts = pts.into(); let end = pts.opt_add(duration.into()); - if cc_data != 0x8080 { + if cc_data != [0x80, 0x80] { + let Ok(code) = cea608_types::tables::Code::from_data(cc_data) else { + return; + }; gst::debug!( CAT, imp: imp, - "{} -> {}: {}", + "{} -> {}: {:?}", pts.display(), end.display(), - eia608_to_text(cc_data) + code ); } else { gst::trace!( @@ -346,7 +353,7 @@ impl State { } self.rows.clear(); - self.cea608_state.flush(); + self.cea608_state.reset(); } else { for row in self.rows.values() { if !row.is_empty() { @@ -402,22 +409,18 @@ impl State { fn handle_preamble( &mut self, imp: &Cea608ToJson, - preamble: Preamble, + preamble: cea608_types::tables::PreambleAddressCode, ) -> Option { - if preamble.chan != 0 { - return None; - } - gst::log!(CAT, imp: imp, "preamble: {:?}", preamble); - let drain_roll_up = self.cursor.row != preamble.row as u32; + let drain_roll_up = self.cursor.row != preamble.row() as u32; // In unbuffered mode, we output the whole roll-up window // and need to move it when the preamble relocates it // https://www.law.cornell.edu/cfr/text/47/79.101 (f)(1)(ii) if self.settings.unbuffered { if let Some(mode) = self.mode { - if mode.is_rollup() && self.cursor.row != preamble.row as u32 { + if mode.is_rollup() && self.cursor.row != preamble.row() as u32 { let offset = match mode { Cea608Mode::RollUp2 => 1, Cea608Mode::RollUp3 => 2, @@ -426,7 +429,7 @@ impl State { }; let current_top_row = self.cursor.row.saturating_sub(offset); - let new_row_offset = preamble.row - self.cursor.row as i32; + let new_row_offset = preamble.row() as i32 - self.cursor.row as i32; for row in current_top_row..self.cursor.row { if let Some(mut row) = self.rows.remove(&row) { @@ -441,10 +444,22 @@ impl State { } } - self.cursor.row = preamble.row as u32; - self.cursor.col = preamble.col as usize; - self.cursor.style = preamble.style; - self.cursor.underline = preamble.underline != 0; + self.cursor.row = preamble.row() as u32; + self.cursor.col = preamble.column() as usize; + self.cursor.underline = preamble.underline(); + self.cursor.style = if preamble.italics() { + TextStyle::ItalicWhite + } else { + match preamble.color() { + cea608_types::tables::Color::White => TextStyle::White, + cea608_types::tables::Color::Green => TextStyle::Green, + cea608_types::tables::Color::Blue => TextStyle::Blue, + cea608_types::tables::Color::Cyan => TextStyle::Cyan, + cea608_types::tables::Color::Red => TextStyle::Red, + cea608_types::tables::Color::Yellow => TextStyle::Yellow, + cea608_types::tables::Color::Magenta => TextStyle::Magenta, + } + }; if let Some(mode) = self.mode { match mode { @@ -487,13 +502,9 @@ impl State { } } - fn handle_text(&mut self, imp: &Cea608ToJson, text: Cea608Text) { - if text.chan != 0 { - return; - } - + fn handle_text(&mut self, imp: &Cea608ToJson, text: cea608_types::Text) { if let Some(row) = self.rows.get_mut(&self.cursor.row) { - if text.code_space == CodeSpace::WestEU { + if text.needs_backspace { row.pop(&mut self.cursor); } @@ -517,11 +528,13 @@ impl State { } } - fn handle_midrowchange(&mut self, midrowchange: MidRowChange) { + fn handle_midrowchange(&mut self, midrowchange: MidRow) { if let Some(row) = self.rows.get_mut(&self.cursor.row) { - if midrowchange.chan == 0 { - row.push_midrow(&mut self.cursor, midrowchange.style, midrowchange.underline); - } + row.push_midrow( + &mut self.cursor, + midrowchange.into(), + midrowchange.underline(), + ); } } @@ -530,129 +543,115 @@ impl State { imp: &Cea608ToJson, pts: Option, duration: Option, - cc_data: u16, + cc_data: [u8; 2], ) -> Option { - self.cea608_state.push_cc_data(cc_data); + let Ok(Some(cea608)) = self.cea608_state.decode(cc_data) else { + return None; + }; - while let Some(cea608) = self.cea608_state.pop() { - if matches!(cea608, Cea608::Duplicate) { - gst::log!(CAT, imp: imp, "Skipping duplicate"); - return None; + self.current_pts = pts; + self.current_duration = duration; + + if cea608.channel() != Channel::ONE { + return None; + } + + match cea608 { + Cea608::EraseDisplay(_chan) => { + return match self.mode { + Some(Cea608Mode::PopOn) => { + self.clear = Some(true); + self.drain_pending(imp) + } + _ => { + let ret = self.drain(imp, true); + self.clear = Some(true); + ret + } + }; } + Cea608::NewMode(_chan, mode) => return self.update_mode(imp, mode.into()), + Cea608::CarriageReturn(_chan) => { + gst::log!(CAT, imp: imp, "carriage return"); - self.current_pts = pts; - self.current_duration = duration; - - match cea608 { - Cea608::Duplicate => unreachable!(), - Cea608::EraseDisplay(chan) => { - if chan == 0 { - return match self.mode { - Some(Cea608Mode::PopOn) => { - self.clear = Some(true); - self.drain_pending(imp) - } - _ => { - let ret = self.drain(imp, true); - self.clear = Some(true); - ret - } - }; - } - } - Cea608::NewMode(chan, mode) => { - if chan == 0 { - return self.update_mode(imp, mode); - } - } - Cea608::CarriageReturn(chan) => { - if chan == 0 { - gst::log!(CAT, imp: imp, "carriage return"); - - if let Some(mode) = self.mode { - // https://www.law.cornell.edu/cfr/text/47/79.101 (f)(2)(i) (f)(3)(i) - if mode.is_rollup() { - let ret = if self.settings.unbuffered { - let offset = match mode { - Cea608Mode::RollUp2 => 1, - Cea608Mode::RollUp3 => 2, - Cea608Mode::RollUp4 => 3, - _ => unreachable!(), - }; - - let top_row = self.cursor.row.saturating_sub(offset); - - // https://www.law.cornell.edu/cfr/text/47/79.101 (f)(1)(iii) - self.rows.remove(&top_row); - - for row in top_row + 1..self.cursor.row + 1 { - if let Some(mut row) = self.rows.remove(&row) { - row.row -= 1; - self.rows.insert(row.row, row); - } - } - - self.rows.insert(self.cursor.row, Row::new(self.cursor.row)); - self.drain(imp, false) - } else { - let ret = self.drain(imp, true); - self.carriage_return = Some(true); - ret - }; - - return ret; - } - } - } - } - Cea608::Backspace(chan) => { - if chan == 0 { - if let Some(row) = self.rows.get_mut(&self.cursor.row) { - row.pop(&mut self.cursor); - } - } - } - Cea608::EraseNonDisplay(chan) => { - if chan == 0 && self.mode == Some(Cea608Mode::PopOn) { - self.rows.clear(); - } - } - Cea608::EndOfCaption(chan) => { - if chan == 0 { - // https://www.law.cornell.edu/cfr/text/47/79.101 (f)(2) - self.update_mode(imp, Cea608Mode::PopOn); - self.first_pts = self.current_pts; + if let Some(mode) = self.mode { + // https://www.law.cornell.edu/cfr/text/47/79.101 (f)(2)(i) (f)(3)(i) + if mode.is_rollup() { let ret = if self.settings.unbuffered { - self.drain(imp, true) + let offset = match mode { + Cea608Mode::RollUp2 => 1, + Cea608Mode::RollUp3 => 2, + Cea608Mode::RollUp4 => 3, + _ => unreachable!(), + }; + + let top_row = self.cursor.row.saturating_sub(offset); + + // https://www.law.cornell.edu/cfr/text/47/79.101 (f)(1)(iii) + self.rows.remove(&top_row); + + for row in top_row + 1..self.cursor.row + 1 { + if let Some(mut row) = self.rows.remove(&row) { + row.row -= 1; + self.rows.insert(row.row, row); + } + } + + self.rows.insert(self.cursor.row, Row::new(self.cursor.row)); + self.drain(imp, false) } else { - let ret = self.drain_pending(imp); - self.pending_lines = self.drain(imp, true); + let ret = self.drain(imp, true); + self.carriage_return = Some(true); ret }; + return ret; } } - Cea608::TabOffset(chan, count) => { - if chan == 0 { - self.cursor.col += count as usize; - // C.13 Right Margin Limitation - self.cursor.col = std::cmp::min(self.cursor.col, 31); - } - } - Cea608::Text(text) => { - if let Some(mode) = self.mode { - self.mode?; - gst::log!(CAT, imp: imp, "text"); - self.handle_text(imp, text); - - if mode.is_rollup() && self.settings.unbuffered { - return self.drain(imp, false); - } - } - } - Cea608::Preamble(preamble) => return self.handle_preamble(imp, preamble), - Cea608::MidRowChange(change) => self.handle_midrowchange(change), } + Cea608::Backspace(_chan) => { + if let Some(row) = self.rows.get_mut(&self.cursor.row) { + row.pop(&mut self.cursor); + } + } + Cea608::EraseNonDisplay(_chan) => { + if self.mode == Some(Cea608Mode::PopOn) { + self.rows.clear(); + } + } + Cea608::EndOfCaption(_chan) => { + // https://www.law.cornell.edu/cfr/text/47/79.101 (f)(2) + self.update_mode(imp, Cea608Mode::PopOn); + self.first_pts = self.current_pts; + let ret = if self.settings.unbuffered { + self.drain(imp, true) + } else { + let ret = self.drain_pending(imp); + self.pending_lines = self.drain(imp, true); + ret + }; + return ret; + } + Cea608::TabOffset(_chan, count) => { + self.cursor.col += count as usize; + // C.13 Right Margin Limitation + self.cursor.col = std::cmp::min(self.cursor.col, 31); + } + Cea608::Text(text) => { + if let Some(mode) = self.mode { + self.mode?; + gst::log!(CAT, imp: imp, "text"); + self.handle_text(imp, text); + + if mode.is_rollup() && self.settings.unbuffered { + return self.drain(imp, false); + } + } + } + // TODO: implement + Cea608::DeleteToEndOfRow(_chan) => (), + Cea608::Preamble(_chan, preamble) => return self.handle_preamble(imp, preamble), + Cea608::MidRowChange(_chan, change) => self.handle_midrowchange(change), } None } @@ -717,7 +716,7 @@ impl Cea608ToJson { return Ok(gst::FlowSuccess::Ok); } - let cc_data = (data[0] as u16) << 8 | data[1] as u16; + let cc_data = [data[0], data[1]]; dump(self, cc_data, pts, duration); diff --git a/video/closedcaption/src/cea608utils.rs b/video/closedcaption/src/cea608utils.rs index caf56601..9da5ede8 100644 --- a/video/closedcaption/src/cea608utils.rs +++ b/video/closedcaption/src/cea608utils.rs @@ -7,19 +7,8 @@ // SPDX-License-Identifier: MPL-2.0 use gst::glib; -use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; -use crate::ffi; - -static CAT: Lazy = Lazy::new(|| { - gst::DebugCategory::new( - "cea608utils", - gst::DebugColorFlags::empty(), - Some("CEA-608 utilities"), - ) -}); - #[derive( Serialize, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum, )] @@ -33,6 +22,18 @@ pub enum Cea608Mode { RollUp4, } +impl From for Cea608Mode { + fn from(value: cea608_types::Mode) -> Self { + match value { + cea608_types::Mode::PopOn => Self::PopOn, + cea608_types::Mode::PaintOn => Self::PaintOn, + cea608_types::Mode::RollUp2 => Self::RollUp2, + cea608_types::Mode::RollUp3 => Self::RollUp3, + cea608_types::Mode::RollUp4 => Self::RollUp4, + } + } +} + #[derive(Clone, Copy, Serialize, Deserialize, Debug, Default, PartialEq, Eq)] pub enum TextStyle { #[default] @@ -50,6 +51,53 @@ impl TextStyle { pub fn is_italics(&self) -> bool { *self == TextStyle::ItalicWhite } + + pub fn to_cea608_color(self) -> Option { + match self { + Self::White => Some(cea608_types::tables::Color::White), + Self::Green => Some(cea608_types::tables::Color::Green), + Self::Blue => Some(cea608_types::tables::Color::Blue), + Self::Cyan => Some(cea608_types::tables::Color::Cyan), + Self::Red => Some(cea608_types::tables::Color::Red), + Self::Yellow => Some(cea608_types::tables::Color::Yellow), + Self::Magenta => Some(cea608_types::tables::Color::Magenta), + Self::ItalicWhite => None, + } + } +} + +impl From for TextStyle { + fn from(value: cea608_types::tables::PreambleAddressCode) -> Self { + value.color().into() + } +} + +impl From for TextStyle { + fn from(value: cea608_types::tables::MidRow) -> Self { + if value.italics() { + Self::ItalicWhite + } else { + // XXX: MidRow may not change color + value + .color() + .unwrap_or(cea608_types::tables::Color::White) + .into() + } + } +} + +impl From for TextStyle { + fn from(value: cea608_types::tables::Color) -> Self { + match value { + cea608_types::tables::Color::White => Self::White, + cea608_types::tables::Color::Green => Self::Green, + cea608_types::tables::Color::Blue => Self::Blue, + cea608_types::tables::Color::Cyan => Self::Cyan, + cea608_types::tables::Color::Red => Self::Red, + cea608_types::tables::Color::Yellow => Self::Yellow, + cea608_types::tables::Color::Magenta => Self::Magenta, + } + } } impl From for TextStyle { @@ -67,329 +115,3 @@ impl From for TextStyle { } } } - -pub(crate) fn is_basicna(cc_data: u16) -> bool { - 0x0000 != (0x6000 & cc_data) -} - -pub(crate) fn is_preamble(cc_data: u16) -> bool { - 0x1040 == (0x7040 & cc_data) -} - -pub(crate) fn is_midrowchange(cc_data: u16) -> bool { - 0x1120 == (0x7770 & cc_data) -} - -pub(crate) fn is_specialna(cc_data: u16) -> bool { - 0x1130 == (0x7770 & cc_data) -} - -pub(crate) fn is_xds(cc_data: u16) -> bool { - 0x0000 == (0x7070 & cc_data) && 0x0000 != (0x0F0F & cc_data) -} - -pub(crate) fn is_westeu(cc_data: u16) -> bool { - 0x1220 == (0x7660 & cc_data) -} - -pub(crate) fn is_control(cc_data: u16) -> bool { - 0x1420 == (0x7670 & cc_data) || 0x1720 == (0x7770 & cc_data) -} - -pub(crate) fn parse_control(cc_data: u16) -> (ffi::eia608_control_t, i32) { - unsafe { - let mut chan = 0; - let cmd = ffi::eia608_parse_control(cc_data, &mut chan); - - (cmd, chan) - } -} - -#[derive(Debug)] -pub(crate) struct Preamble { - pub row: i32, - pub col: i32, - pub style: TextStyle, - pub chan: i32, - pub underline: i32, -} - -pub(crate) fn parse_preamble(cc_data: u16) -> Preamble { - unsafe { - let mut row = 0; - let mut col = 0; - let mut style = 0; - let mut chan = 0; - let mut underline = 0; - - ffi::eia608_parse_preamble( - cc_data, - &mut row, - &mut col, - &mut style, - &mut chan, - &mut underline, - ); - - Preamble { - row, - col, - style: style.into(), - chan, - underline, - } - } -} - -#[derive(Debug)] -pub(crate) struct MidRowChange { - pub chan: i32, - pub style: TextStyle, - pub underline: bool, -} - -pub(crate) fn parse_midrowchange(cc_data: u16) -> MidRowChange { - unsafe { - let mut chan = 0; - let mut style = 0; - let mut underline = 0; - - ffi::eia608_parse_midrowchange(cc_data, &mut chan, &mut style, &mut underline); - - MidRowChange { - chan, - style: style.into(), - underline: underline > 0, - } - } -} - -pub(crate) fn eia608_to_utf8(cc_data: u16) -> (Option, Option, i32) { - unsafe { - let mut chan = 0; - let mut char1 = [0u8; 5usize]; - let mut char2 = [0u8; 5usize]; - - let n_chars = ffi::eia608_to_utf8( - cc_data, - &mut chan, - char1.as_mut_ptr() as *mut _, - char2.as_mut_ptr() as *mut _, - ); - - let char1 = if n_chars > 0 { - Some( - std::ffi::CStr::from_bytes_with_nul_unchecked(&char1) - .to_string_lossy() - .chars() - .next() - .unwrap(), - ) - } else { - None - }; - - let char2 = if n_chars > 1 { - Some( - std::ffi::CStr::from_bytes_with_nul_unchecked(&char2) - .to_string_lossy() - .chars() - .next() - .unwrap(), - ) - } else { - None - }; - - (char1, char2, chan) - } -} - -pub(crate) fn eia608_to_text(cc_data: u16) -> String { - unsafe { - let bufsz = ffi::eia608_to_text(std::ptr::null_mut(), 0, cc_data); - let mut data = Vec::with_capacity((bufsz + 1) as usize); - ffi::eia608_to_text(data.as_ptr() as *mut _, (bufsz + 1) as usize, cc_data); - data.set_len(bufsz as usize); - String::from_utf8_unchecked(data) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum CodeSpace { - BasicNA, - SpecialNA, - WestEU, -} - -impl CodeSpace { - fn from_cc_data(cc_data: u16) -> Self { - if is_basicna(cc_data) { - Self::BasicNA - } else if is_specialna(cc_data) { - Self::SpecialNA - } else if is_westeu(cc_data) { - Self::WestEU - } else { - unreachable!() - } - } -} - -#[derive(Debug, Clone)] -pub(crate) struct Cea608Text { - pub char1: Option, - pub char2: Option, - pub code_space: CodeSpace, - pub chan: i32, -} - -#[derive(Debug)] -pub(crate) enum Cea608 { - Duplicate, - NewMode(i32, Cea608Mode), - EraseDisplay(i32), - EraseNonDisplay(i32), - CarriageReturn(i32), - Backspace(i32), - EndOfCaption(i32), - TabOffset(i32, u32), - Text(Cea608Text), - Preamble(Preamble), - MidRowChange(MidRowChange), -} - -impl Cea608 { - pub fn channel(&self) -> i32 { - match self { - Self::Duplicate => 0, - Self::NewMode(chan, _) => *chan, - Self::EraseDisplay(chan) => *chan, - Self::EraseNonDisplay(chan) => *chan, - Self::CarriageReturn(chan) => *chan, - Self::Backspace(chan) => *chan, - Self::EndOfCaption(chan) => *chan, - Self::TabOffset(chan, _) => *chan, - Self::Text(text) => text.chan, - Self::Preamble(preamble) => preamble.chan, - Self::MidRowChange(midrowchange) => midrowchange.chan, - } - } -} - -fn decode_control(cc_data: u16) -> Option { - let (cmd, chan) = parse_control(cc_data); - - gst::log!(CAT, "Command for CC {}", chan); - - match cmd { - ffi::eia608_control_t_eia608_control_resume_direct_captioning => { - return Some(Cea608::NewMode(chan, Cea608Mode::PaintOn)); - } - ffi::eia608_control_t_eia608_control_erase_display_memory => { - return Some(Cea608::EraseDisplay(chan)); - } - ffi::eia608_control_t_eia608_control_roll_up_2 => { - return Some(Cea608::NewMode(chan, Cea608Mode::RollUp2)); - } - ffi::eia608_control_t_eia608_control_roll_up_3 => { - return Some(Cea608::NewMode(chan, Cea608Mode::RollUp3)); - } - ffi::eia608_control_t_eia608_control_roll_up_4 => { - return Some(Cea608::NewMode(chan, Cea608Mode::RollUp4)); - } - ffi::eia608_control_t_eia608_control_carriage_return => { - return Some(Cea608::CarriageReturn(chan)); - } - ffi::eia608_control_t_eia608_control_backspace => { - return Some(Cea608::Backspace(chan)); - } - ffi::eia608_control_t_eia608_control_resume_caption_loading => { - return Some(Cea608::NewMode(chan, Cea608Mode::PopOn)); - } - ffi::eia608_control_t_eia608_control_erase_non_displayed_memory => { - return Some(Cea608::EraseNonDisplay(chan)); - } - ffi::eia608_control_t_eia608_control_end_of_caption => { - return Some(Cea608::EndOfCaption(chan)); - } - ffi::eia608_control_t_eia608_tab_offset_0 - | ffi::eia608_control_t_eia608_tab_offset_1 - | ffi::eia608_control_t_eia608_tab_offset_2 - | ffi::eia608_control_t_eia608_tab_offset_3 => { - return Some(Cea608::TabOffset( - chan, - cmd - ffi::eia608_control_t_eia608_tab_offset_0, - )); - } - // TODO - ffi::eia608_control_t_eia608_control_alarm_off - | ffi::eia608_control_t_eia608_control_delete_to_end_of_row => {} - ffi::eia608_control_t_eia608_control_alarm_on - | ffi::eia608_control_t_eia608_control_text_restart - | ffi::eia608_control_t_eia608_control_text_resume_text_display => {} - _ => { - return None; - } - } - None -} - -#[derive(Debug, Default)] -pub(crate) struct Cea608StateTracker { - last_cc_data: Option, - last_channel: i32, - pending_output: Vec, -} - -impl Cea608StateTracker { - pub(crate) fn push_cc_data(&mut self, cc_data: u16) { - if (is_specialna(cc_data) || is_control(cc_data)) && Some(cc_data) == self.last_cc_data { - gst::log!(CAT, "Skipping duplicate"); - self.pending_output.push(Cea608::Duplicate); - return; - } - - if is_xds(cc_data) { - gst::log!(CAT, "XDS, ignoring"); - } else if is_control(cc_data) { - if let Some(d) = decode_control(cc_data) { - self.last_channel = d.channel(); - self.pending_output.push(d); - } - } else if is_basicna(cc_data) || is_specialna(cc_data) || is_westeu(cc_data) { - let (char1, char2, chan) = eia608_to_utf8(cc_data); - if char1.is_some() || char2.is_some() { - let chan = if is_basicna(cc_data) { - /* basicna doesn't include a channel indicator so we track the last generated - * one */ - self.last_channel - } else { - chan - }; - self.pending_output.push(Cea608::Text(Cea608Text { - char1, - char2, - code_space: CodeSpace::from_cc_data(cc_data), - chan, - })); - } - } else if is_preamble(cc_data) { - let preamble = parse_preamble(cc_data); - self.last_channel = preamble.chan; - self.pending_output.push(Cea608::Preamble(preamble)); - } else if is_midrowchange(cc_data) { - let midrowchange = parse_midrowchange(cc_data); - self.last_channel = midrowchange.chan; - self.pending_output.push(Cea608::MidRowChange(midrowchange)); - } - } - - pub(crate) fn pop(&mut self) -> Option { - self.pending_output.pop() - } - - pub(crate) fn flush(&mut self) { - self.pending_output = vec![]; - } -} diff --git a/video/closedcaption/src/cea708utils.rs b/video/closedcaption/src/cea708utils.rs index fca2d6df..4c5e5762 100644 --- a/video/closedcaption/src/cea708utils.rs +++ b/video/closedcaption/src/cea708utils.rs @@ -33,39 +33,55 @@ pub enum Cea708Mode { RollUp, } -pub fn textstyle_foreground_color(style: TextStyle) -> Color { - match style { - TextStyle::Red => Color { +pub fn textstyle_foreground_color(style: TextStyle) -> cea708_types::tables::Color { + let color = match style { + TextStyle::Red => cea608_types::tables::Color::Red, + TextStyle::Blue => cea608_types::tables::Color::Blue, + TextStyle::Cyan => cea608_types::tables::Color::Cyan, + TextStyle::White => cea608_types::tables::Color::White, + TextStyle::Green => cea608_types::tables::Color::Green, + TextStyle::Yellow => cea608_types::tables::Color::Yellow, + TextStyle::Magenta => cea608_types::tables::Color::Magenta, + TextStyle::ItalicWhite => cea608_types::tables::Color::White, + }; + cea608_color_to_foreground_color(color) +} + +pub fn cea608_color_to_foreground_color( + color: cea608_types::tables::Color, +) -> cea708_types::tables::Color { + match color { + cea608_types::tables::Color::Red => cea708_types::tables::Color { r: ColorValue::Full, g: ColorValue::None, b: ColorValue::None, }, - TextStyle::Green => Color { + cea608_types::tables::Color::Green => cea708_types::tables::Color { r: ColorValue::None, g: ColorValue::Full, b: ColorValue::None, }, - TextStyle::Blue => Color { + cea608_types::tables::Color::Blue => cea708_types::tables::Color { r: ColorValue::None, g: ColorValue::None, b: ColorValue::Full, }, - TextStyle::Cyan => Color { + cea608_types::tables::Color::Cyan => cea708_types::tables::Color { r: ColorValue::None, g: ColorValue::Full, b: ColorValue::Full, }, - TextStyle::Yellow => Color { + cea608_types::tables::Color::Yellow => cea708_types::tables::Color { r: ColorValue::Full, g: ColorValue::Full, b: ColorValue::None, }, - TextStyle::Magenta => Color { + cea608_types::tables::Color::Magenta => cea708_types::tables::Color { r: ColorValue::Full, g: ColorValue::None, b: ColorValue::Full, }, - TextStyle::White | TextStyle::ItalicWhite => Color { + cea608_types::tables::Color::White => cea708_types::tables::Color { r: ColorValue::Full, g: ColorValue::Full, b: ColorValue::Full, @@ -213,7 +229,7 @@ impl Cea708ServiceWriter { pub fn rollup_preamble(&mut self, rollup_count: u8, base_row: u8) { let base_row = std::cmp::max(rollup_count, base_row); - let anchor_vertical = (base_row as u32 * 100 / 15) as u8; + let anchor_vertical = (base_row as u32 * 100 / 14) as u8; gst::trace!( CAT, "rollup_preamble base {base_row} count {rollup_count}, anchor-v {anchor_vertical}" diff --git a/video/closedcaption/src/tttocea608/imp.rs b/video/closedcaption/src/tttocea608/imp.rs index d6942d4b..8ad98da9 100644 --- a/video/closedcaption/src/tttocea608/imp.rs +++ b/video/closedcaption/src/tttocea608/imp.rs @@ -16,7 +16,6 @@ use std::sync::Mutex; use crate::cea608utils::Cea608Mode; use crate::cea608utils::TextStyle; -use crate::ffi; use crate::ttutils::Chunk; use crate::ttutils::Line; use crate::ttutils::Lines; @@ -75,40 +74,30 @@ static CAT: Lazy = Lazy::new(|| { ) }); -fn eia608_to_text(cc_data: u16) -> String { - unsafe { - let bufsz = ffi::eia608_to_text(std::ptr::null_mut(), 0, cc_data); - let mut data = Vec::with_capacity((bufsz + 1) as usize); - ffi::eia608_to_text(data.as_ptr() as *mut _, (bufsz + 1) as usize, cc_data); - data.set_len(bufsz as usize); - String::from_utf8_unchecked(data) - } -} - fn cc_data_buffer( imp: &TtToCea608, - cc_data: u16, + cc_data: [u8; 2], pts: gst::ClockTime, duration: gst::ClockTime, ) -> gst::Buffer { let mut ret = gst::Buffer::with_size(2).unwrap(); let buf_mut = ret.get_mut().unwrap(); - let data = cc_data.to_be_bytes(); - if cc_data != 0x8080 { + if cc_data != [0x80, 0x80] { + let code = cea608_types::tables::Code::from_data(cc_data); gst::log!( CAT, imp: imp, - "{} -> {}: {}", + "{} -> {}: {:?}", pts, pts + duration, - eia608_to_text(cc_data) + code ); } else { gst::trace!(CAT, imp: imp, "{} -> {}: padding", pts, pts + duration); } - buf_mut.copy_from_slice(0, &data).unwrap(); + buf_mut.copy_from_slice(0, &cc_data).unwrap(); buf_mut.set_pts(pts); buf_mut.set_duration(duration); @@ -301,6 +290,7 @@ impl TtToCea608 { let framerate = s.get::("framerate").unwrap(); state.framerate = framerate; state.translator.set_framerate(framerate); + state.translator.set_caption_id(cea608_types::Id::CC1); let upstream_caps = e.caps(); let s = upstream_caps.structure(0).unwrap(); @@ -362,7 +352,9 @@ impl TtToCea608 { *state = State::default(); state.translator.set_mode(settings.mode); state.translator.set_framerate(framerate); - state.translator.set_origin_column(settings.origin_column); + state + .translator + .set_origin_column(settings.origin_column as u8); state .translator .set_roll_up_timeout(settings.roll_up_timeout); @@ -606,7 +598,9 @@ impl ElementImpl for TtToCea608 { *state = State::default(); state.force_clear = false; state.translator.set_mode(settings.mode); - state.translator.set_origin_column(settings.origin_column); + state + .translator + .set_origin_column(settings.origin_column as u8); state.translator.set_framerate(framerate); state .translator diff --git a/video/closedcaption/src/tttocea608/translate.rs b/video/closedcaption/src/tttocea608/translate.rs index 91a8d631..d2cb557e 100644 --- a/video/closedcaption/src/tttocea608/translate.rs +++ b/video/closedcaption/src/tttocea608/translate.rs @@ -10,9 +10,7 @@ use gst::prelude::*; use once_cell::sync::Lazy; use std::collections::VecDeque; -use crate::ffi; - -use crate::cea608utils::{is_basicna, is_specialna, is_westeu, Cea608Mode, TextStyle}; +use crate::cea608utils::{Cea608Mode, TextStyle}; use crate::ttutils::{Chunk, Lines}; pub const DEFAULT_FPS_N: i32 = 30; @@ -26,65 +24,24 @@ static CAT: Lazy = Lazy::new(|| { ) }); -static SPACE: Lazy = Lazy::new(|| eia608_from_utf8_1(&[0x20, 0, 0, 0, 0])); - fn is_punctuation(word: &str) -> bool { word == "." || word == "," || word == "?" || word == "!" || word == ";" || word == ":" } -#[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, underline: bool) -> u16 { - unsafe { - /* Hardcoded chan */ - ffi::eia608_row_column_pramble(row, col, 0, underline as i32) - } -} - -fn eia608_row_style_preamble(row: i32, style: u32, underline: bool) -> u16 { - unsafe { - /* Hardcoded chan */ - ffi::eia608_row_style_pramble(row, 0, style, underline as i32) - } -} - -fn eia608_midrow_change(style: u32, underline: bool) -> u16 { - unsafe { - /* Hardcoded chan and underline */ - ffi::eia608_midrow_change(0, style, underline as i32) - } -} - -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 erase_non_displayed_memory() -> u16 { - eia608_control_command(ffi::eia608_control_t_eia608_control_erase_non_displayed_memory) -} - -fn peek_word_length(chars: std::iter::Peekable) -> u32 { - chars.take_while(|c| !c.is_ascii_whitespace()).count() as u32 +fn peek_word_length(chars: std::iter::Peekable) -> u8 { + chars.take_while(|c| !c.is_ascii_whitespace()).count() as u8 } #[derive(Debug)] pub struct TimedCea608 { - pub cea608: u16, + pub cea608: [u8; 2], pub frame_no: u64, } #[derive(Debug)] pub struct TextToCea608 { // settings - origin_column: u32, + origin_column: u8, framerate: gst::Fraction, roll_up_timeout: Option, // state @@ -96,6 +53,8 @@ pub struct TextToCea608 { underline: bool, column: u8, mode: Cea608Mode, + writer: cea608_types::Cea608Writer, + caption_id: cea608_types::Id, } impl Default for TextToCea608 { @@ -112,6 +71,8 @@ impl Default for TextToCea608 { style: TextStyle::White, underline: false, mode: Cea608Mode::PopOn, + writer: cea608_types::Cea608Writer::default(), + caption_id: cea608_types::Id::CC1, } } } @@ -121,10 +82,14 @@ impl TextToCea608 { self.mode = mode; if self.mode != Cea608Mode::PopOn { self.send_roll_up_preamble = true; - self.column = self.origin_column as u8; + self.column = self.origin_column; } } + pub fn set_caption_id(&mut self, id: cea608_types::Id) { + self.caption_id = id; + } + pub fn set_framerate(&mut self, framerate: gst::Fraction) { self.framerate = framerate; } @@ -137,7 +102,7 @@ impl TextToCea608 { self.roll_up_timeout = timeout; } - pub fn set_origin_column(&mut self, origin_column: u32) { + pub fn set_origin_column(&mut self, origin_column: u8) { self.origin_column = origin_column } @@ -176,7 +141,7 @@ impl TextToCea608 { false } - fn cc_data(&mut self, cc_data: u16) { + fn cc_data(&mut self, cc_data: [u8; 2]) { self.check_erase_display(); self.output_frames.push_back(TimedCea608 { @@ -190,101 +155,102 @@ impl TextToCea608 { fn pad(&mut self, frame_no: u64) { while self.last_frame_no < frame_no { if !self.check_erase_display() { - self.cc_data(0x8080); + self.cc_data([0x80, 0x80]); } } } + fn control_code(&mut self, control: cea608_types::tables::Control) { + let code = cea608_types::tables::ControlCode::new( + self.caption_id.field(), + self.caption_id.channel(), + control, + ); + let mut vec = smallvec::SmallVec::<[u8; 2]>::new(); + let _ = cea608_types::tables::Code::Control(code).write(&mut vec); + self.cc_data([vec[0], vec[1]]); + } + + fn erase_non_displayed_memory(&mut self) { + self.control_code(cea608_types::tables::Control::EraseNonDisplayedMemory); + } + fn resume_caption_loading(&mut self) { - self.cc_data(eia608_control_command( - ffi::eia608_control_t_eia608_control_resume_caption_loading, - )) + self.control_code(cea608_types::tables::Control::ResumeCaptionLoading); } fn resume_direct_captioning(&mut self) { - self.cc_data(eia608_control_command( - ffi::eia608_control_t_eia608_control_resume_direct_captioning, - )) + self.control_code(cea608_types::tables::Control::ResumeDirectionCaptioning); } fn delete_to_end_of_row(&mut self) { - self.cc_data(eia608_control_command( - ffi::eia608_control_t_eia608_control_delete_to_end_of_row, - )) + self.control_code(cea608_types::tables::Control::DeleteToEndOfRow); } fn roll_up_2(&mut self) { - self.cc_data(eia608_control_command( - ffi::eia608_control_t_eia608_control_roll_up_2, - )) + self.control_code(cea608_types::tables::Control::RollUp2); } fn roll_up_3(&mut self) { - self.cc_data(eia608_control_command( - ffi::eia608_control_t_eia608_control_roll_up_3, - )) + self.control_code(cea608_types::tables::Control::RollUp3); } fn roll_up_4(&mut self) { - self.cc_data(eia608_control_command( - ffi::eia608_control_t_eia608_control_roll_up_4, - )) + self.control_code(cea608_types::tables::Control::RollUp4); } fn carriage_return(&mut self) { - self.cc_data(eia608_control_command( - ffi::eia608_control_t_eia608_control_carriage_return, - )) + self.control_code(cea608_types::tables::Control::CarriageReturn); } fn end_of_caption(&mut self) { - self.cc_data(eia608_control_command( - ffi::eia608_control_t_eia608_control_end_of_caption, - )) + self.control_code(cea608_types::tables::Control::EndOfCaption); } - fn tab_offset(&mut self, offset: u32) { - match offset { - 0 => (), - 1 => self.cc_data(eia608_control_command( - ffi::eia608_control_t_eia608_tab_offset_1, - )), - 2 => self.cc_data(eia608_control_command( - ffi::eia608_control_t_eia608_tab_offset_2, - )), - 3 => self.cc_data(eia608_control_command( - ffi::eia608_control_t_eia608_tab_offset_3, - )), - _ => unreachable!(), - } + fn tab_offset(&mut self, offset: u8) { + let Some(offset) = cea608_types::tables::Control::tab_offset(offset) else { + return; + }; + self.control_code(offset); } - fn preamble_indent(&mut self, row: i32, col: i32, underline: bool) { - self.cc_data(eia608_row_column_preamble(row, col, underline)) + fn preamble_indent(&mut self, row: u8, col: u8, underline: bool) { + let Some(preamble) = cea608_types::tables::PreambleType::from_indent(col) else { + return; + }; + self.control_code(cea608_types::tables::Control::PreambleAddress( + cea608_types::tables::PreambleAddressCode::new(row, underline, preamble), + )); } - fn preamble_style(&mut self, row: i32, style: u32, underline: bool) { - self.cc_data(eia608_row_style_preamble(row, style, underline)) + fn preamble_style(&mut self, row: u8, style: TextStyle, underline: bool) { + let preamble = if style.is_italics() { + cea608_types::tables::PreambleType::WhiteItalics + } else { + cea608_types::tables::PreambleType::Color(style.to_cea608_color().unwrap()) + }; + self.control_code(cea608_types::tables::Control::PreambleAddress( + cea608_types::tables::PreambleAddressCode::new(row, underline, preamble), + )); } - fn midrow_change(&mut self, style: u32, underline: bool) { - self.cc_data(eia608_midrow_change(style, underline)) - } - - fn bna(&mut self, bna1: u16, bna2: u16) { - self.cc_data(eia608_from_basicna(bna1, bna2)) + fn midrow_change(&mut self, style: TextStyle, underline: bool) { + let midrow = if style.is_italics() { + cea608_types::tables::MidRow::new_italics(underline) + } else { + cea608_types::tables::MidRow::new_color(style.to_cea608_color().unwrap(), underline) + }; + self.control_code(cea608_types::tables::Control::MidRow(midrow)); } fn erase_display_memory(&mut self) { self.erase_display_frame_no = None; - self.cc_data(eia608_control_command( - ffi::eia608_control_t_eia608_control_erase_display_memory, - )) + self.control_code(cea608_types::tables::Control::EraseDisplayedMemory); } - fn open_chunk(&mut self, chunk: &Chunk, col: u32) -> bool { + fn open_chunk(&mut self, chunk: &Chunk, col: u8) -> bool { if (chunk.style != self.style || chunk.underline != self.underline) && col < 31 { - self.midrow_change(chunk.style as u32, chunk.underline); + self.midrow_change(chunk.style, chunk.underline); self.style = chunk.style; self.underline = chunk.underline; true @@ -297,8 +263,8 @@ impl TextToCea608 { fn open_line( &mut self, chunk: &Chunk, - col: &mut u32, - row: i32, + col: &mut u8, + row: u8, carriage_return: Option, ) -> bool { let mut ret = true; @@ -332,7 +298,7 @@ impl TextToCea608 { } if chunk.style != TextStyle::White && indent == 0 { - self.preamble_style(row, chunk.style as u32, chunk.underline); + self.preamble_style(row, chunk.style, chunk.underline); self.style = chunk.style; } else { if chunk.style != TextStyle::White { @@ -346,7 +312,7 @@ impl TextToCea608 { } self.style = TextStyle::White; - self.preamble_indent(row, (indent * 4) as i32, chunk.underline); + self.preamble_indent(row, indent * 4, chunk.underline); } if self.mode == Cea608Mode::PaintOn { @@ -393,7 +359,7 @@ impl TextToCea608 { let mut col = if self.mode == Cea608Mode::PopOn || self.mode == Cea608Mode::PaintOn { 0 } else { - self.column as u32 + self.column }; self.pad(frame_no); @@ -430,19 +396,17 @@ impl TextToCea608 { if !lines.lines.is_empty() { if self.mode == Cea608Mode::PopOn { self.resume_caption_loading(); - self.cc_data(erase_non_displayed_memory()); + self.erase_non_displayed_memory(); } else if self.mode == Cea608Mode::PaintOn { self.resume_direct_captioning(); } } - let mut prev_char = 0; - for line in &lines.lines { gst::log!(CAT, "Processing {:?}", line); if let Some(line_row) = line.row { - row = line_row; + row = line_row as u8; } if row > 14 { @@ -454,21 +418,20 @@ impl TextToCea608 { if self.mode != Cea608Mode::PopOn && self.mode != Cea608Mode::PaintOn { self.send_roll_up_preamble = true; } - col = line_column; + col = line_column as u8; } else if self.mode == Cea608Mode::PopOn || self.mode == Cea608Mode::PaintOn { col = origin_column; } for (j, chunk) in line.chunks.iter().enumerate() { let mut prepend_space = true; - if prev_char != 0 { - self.cc_data(prev_char); - prev_char = 0; + while self.writer.n_codes() > 0 { + let data = self.writer.pop(); + self.cc_data(data); } if j == 0 { - prepend_space = - self.open_line(chunk, &mut col, row as i32, line.carriage_return); + prepend_space = self.open_line(chunk, &mut col, row, line.carriage_return); } else if self.open_chunk(chunk, col) { prepend_space = false; col += 1; @@ -495,40 +458,22 @@ impl TextToCea608 { continue; } - let mut encoded = [0; 5]; - c.encode_utf8(&mut encoded); - let mut cc_data = eia608_from_utf8_1(&encoded); - - if cc_data == 0 { + let code = cea608_types::tables::Code::from_char( + c, + cea608_types::tables::Channel::ONE, + ) + .unwrap_or_else(|| { gst::warning!(CAT, "Not translating UTF8: {}", c); - cc_data = *SPACE; - } + cea608_types::tables::Code::Space + }); - if is_basicna(prev_char) { - if is_basicna(cc_data) { - self.bna(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 - self.bna(prev_char, *SPACE); - self.cc_data(cc_data); - } else { - self.cc_data(prev_char); - self.cc_data(cc_data); + self.writer.push(code); + + if let cea608_types::tables::Code::Control(_) = code { + while self.writer.n_codes() > 0 { + let data = self.writer.pop(); + self.cc_data(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 - self.cc_data(*SPACE); - self.cc_data(cc_data); - } else if is_basicna(cc_data) { - prev_char = cc_data; - } else { - self.cc_data(cc_data); - } - - if is_specialna(cc_data) { // adapted from libcaption's generation code: // specialna are treated as control characters. Duplicated control characters are discarded // So we write a resume after a specialna as a noop control command to break repetition detection @@ -560,12 +505,12 @@ impl TextToCea608 { if (next_word_length <= 32 - origin_column && col + next_word_length > 31) || col > 31 { - if prev_char != 0 { - self.cc_data(prev_char); - prev_char = 0; + while self.writer.n_codes() > 0 { + let data = self.writer.pop(); + self.cc_data(data); } - self.open_line(chunk, &mut col, row as i32, Some(true)); + self.open_line(chunk, &mut col, row, Some(true)); } } else if col > 31 { if chars.peek().is_some() { @@ -577,17 +522,18 @@ impl TextToCea608 { } if self.mode == Cea608Mode::PopOn || self.mode == Cea608Mode::PaintOn { - if prev_char != 0 { - self.cc_data(prev_char); - prev_char = 0; + while self.writer.n_codes() > 0 { + let data = self.writer.pop(); + self.cc_data(data); } row += 1; } } if !lines.lines.is_empty() { - if prev_char != 0 { - self.cc_data(prev_char); + while self.writer.n_codes() > 0 { + let data = self.writer.pop(); + self.cc_data(data); } if self.mode == Cea608Mode::PopOn { @@ -597,7 +543,7 @@ impl TextToCea608 { } } - self.column = col as u8; + self.column = col; if self.mode == Cea608Mode::PopOn { self.erase_display_frame_no = diff --git a/video/closedcaption/src/tttocea708/imp.rs b/video/closedcaption/src/tttocea708/imp.rs index 72493d97..08812474 100644 --- a/video/closedcaption/src/tttocea708/imp.rs +++ b/video/closedcaption/src/tttocea708/imp.rs @@ -360,7 +360,13 @@ impl TtToCea708 { .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_cea608_channel(if settings.cea608_channel < 1 { + None + } else { + Some(cea608_types::Id::from_value(settings.cea608_channel as i8)) + }); state.translator.set_service_no(settings.service_no); state.translator.flush(); @@ -526,7 +532,13 @@ impl ObjectImpl for TtToCea708 { 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); + state + .translator + .set_cea608_channel(if settings.cea608_channel < 1 { + None + } else { + Some(cea608_types::Id::from_value(settings.cea608_channel as i8)) + }); } "roll-up-rows" => { let mut state = self.state.lock().unwrap(); @@ -657,7 +669,13 @@ impl ElementImpl for TtToCea708 { .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_cea608_channel(if settings.cea608_channel < 1 { + None + } else { + Some(cea608_types::Id::from_value(settings.cea608_channel as i8)) + }); state.translator.set_service_no(settings.service_no); state.translator.flush(); } diff --git a/video/closedcaption/src/tttocea708/translate.rs b/video/closedcaption/src/tttocea708/translate.rs index bed40102..1a5e4734 100644 --- a/video/closedcaption/src/tttocea708/translate.rs +++ b/video/closedcaption/src/tttocea708/translate.rs @@ -53,7 +53,7 @@ pub struct TextToCea708 { mode: Cea708Mode, roll_up_count: u8, service_no: u8, - cea608_channel: u8, + cea608_channel: Option, origin_column: u32, roll_up_timeout: Option, framerate: gst::Fraction, @@ -78,7 +78,7 @@ impl Default for TextToCea708 { mode: Cea708Mode::RollUp, roll_up_count: 2, service_no: 1, - cea608_channel: 1, + cea608_channel: Some(cea608_types::Id::CC1), origin_column: 0, framerate: gst::Fraction::new(DEFAULT_FPS_N, DEFAULT_FPS_D), roll_up_timeout: None, @@ -117,7 +117,7 @@ impl TextToCea708 { pub fn set_origin_column(&mut self, origin_column: u32) { self.origin_column = origin_column; - self.cea608.set_origin_column(origin_column); + self.cea608.set_origin_column(origin_column as u8); } pub fn set_column(&mut self, column: u8) { @@ -151,10 +151,12 @@ impl TextToCea708 { self.framerate } - pub fn set_cea608_channel(&mut self, channel: u8) { - assert!((0..=4).contains(&channel)); + pub fn set_cea608_channel(&mut self, channel: Option) { if self.cea608_channel != channel { self.cea608.flush(); + if let Some(id) = channel { + self.cea608.set_caption_id(id); + } } self.cea608_channel = channel; } @@ -271,23 +273,19 @@ impl TextToCea708 { } gst::trace!(CAT, "push packet to writer"); self.cc_data_writer.push_packet(packet); - if self.cea608_channel > 0 { + if let Some(cea608_id) = self.cea608_channel { let tcea608 = self.cea608.pop_output().unwrap_or(TimedCea608 { - cea608: 0x8080, + cea608: [0x80, 0x80], 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 + let (byte0, byte1) = (tcea608.cea608[0], tcea608.cea608[1]); + match cea608_id.field() { + cea608_types::tables::Field::ONE => self .cc_data_writer .push_cea608(cea708_types::Cea608::Field1(byte0, byte1)), - 3 | 4 => self + cea608_types::tables::Field::TWO => self .cc_data_writer .push_cea608(cea708_types::Cea608::Field2(byte0, byte1)), - _ => (), } } @@ -331,7 +329,7 @@ impl TextToCea708 { 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 { + if self.cea608_channel.is_some() { self.cea608.generate(frame_no, end_frame_no, lines.clone()); } diff --git a/video/closedcaption/tests/cea608tocea708.rs b/video/closedcaption/tests/cea608tocea708.rs index 4a50cf3c..1dde1d27 100644 --- a/video/closedcaption/tests/cea608tocea708.rs +++ b/video/closedcaption/tests/cea608tocea708.rs @@ -133,29 +133,7 @@ fn test_rollup() { ], ), // RU3 -> DeleteWindows(!0), DefineWindow(0...), SetPenLocation(bottom-row) ([0x94, 0xAD], vec![Code::CR, Code::ETX]), // CR -> CR - ( - [0x94, 0x70], - vec![ - Code::DeleteWindows(!WindowBits::ZERO), - Code::DefineWindow(DefineWindowArgs::new( - 0, - 0, - Anchor::BottomMiddle, - true, - 93, - 50, - 2, - 31, - true, - true, - true, - 1, - 1, - )), - Code::SetPenLocation(SetPenLocationArgs::new(2, 0)), - Code::ETX, - ], - ), // PAC to bottom left -> SetPenLocation(...) + ([0x94, 0x70], vec![Code::ETX]), // PAC to bottom left -> (pen already there) -> nothing to do ( [0xA8, 0x43], vec![Code::LeftParenthesis, Code::LatinCapitalC, Code::ETX], @@ -169,7 +147,7 @@ fn test_rollup() { 0, Anchor::BottomMiddle, true, - 93, + 100, 50, 2, 31, @@ -183,7 +161,7 @@ fn test_rollup() { Code::ETX, ], ), // RU3 - ([0x94, 0xAD], vec![Code::CR, Code::ETX]), // CR -> CR + ([0x94, 0xAD], vec![Code::CR, Code::ETX]), // CR -> CR ([0x94, 0x70], vec![Code::ETX]), // PAC to bottom left -> SetPenLocation(...) ( [0xF2, 0xEF],