mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2025-01-24 09:58:13 +00:00
tttocea708: add support for writing 608 compatibility bytes
608 compatibility bytes are generated using the same functionality as tttocea608. Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1406>
This commit is contained in:
parent
9db4290d2d
commit
55b4de779c
6 changed files with 685 additions and 387 deletions
|
@ -5732,6 +5732,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"properties": {
|
"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": {
|
"mode": {
|
||||||
"blurb": "Which mode to operate in",
|
"blurb": "Which mode to operate in",
|
||||||
"conditionally-available": false,
|
"conditionally-available": false,
|
||||||
|
@ -5772,6 +5786,20 @@
|
||||||
"type": "gint",
|
"type": "gint",
|
||||||
"writable": true
|
"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": {
|
"roll-up-timeout": {
|
||||||
"blurb": "Duration after which to erase display memory in roll-up mode",
|
"blurb": "Duration after which to erase display memory in roll-up mode",
|
||||||
"conditionally-available": false,
|
"conditionally-available": false,
|
||||||
|
|
|
@ -88,6 +88,7 @@ pub fn textstyle_to_pen_color(style: TextStyle) -> SetPenColorArgs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub(crate) struct Cea708ServiceWriter {
|
pub(crate) struct Cea708ServiceWriter {
|
||||||
codes: Vec<Code>,
|
codes: Vec<Code>,
|
||||||
service_no: u8,
|
service_no: u8,
|
||||||
|
|
|
@ -47,7 +47,10 @@ mod ttutils;
|
||||||
|
|
||||||
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||||
#[cfg(feature = "doc")]
|
#[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_parse::register(plugin)?;
|
||||||
mcc_enc::register(plugin)?;
|
mcc_enc::register(plugin)?;
|
||||||
scc_parse::register(plugin)?;
|
scc_parse::register(plugin)?;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// Copyright (C) 2020 Mathieu Duponchelle <mathieu@centricular.com>
|
// Copyright (C) 2020 Mathieu Duponchelle <mathieu@centricular.com>
|
||||||
|
// Copyright (C) 2023 Matthew Waters <matthew@centricular.com>
|
||||||
//
|
//
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
// 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
|
// 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
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use cea708_types::CCDataWriter;
|
|
||||||
use cea708_types::DTVCCPacket;
|
|
||||||
use cea708_types::Framerate;
|
|
||||||
use gst::glib;
|
use gst::glib;
|
||||||
use gst::prelude::*;
|
use gst::prelude::*;
|
||||||
use gst::subclass::prelude::*;
|
use gst::subclass::prelude::*;
|
||||||
|
@ -16,32 +14,31 @@ use gst::subclass::prelude::*;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
use crate::cea608utils::Cea608Mode;
|
use crate::cea608utils::Cea608Mode;
|
||||||
|
use crate::tttocea708::translate::DEFAULT_FPS_D;
|
||||||
|
use crate::tttocea708::translate::DEFAULT_FPS_N;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use cea708_types::tables::*;
|
|
||||||
|
|
||||||
use crate::cea608utils::TextStyle;
|
use crate::cea608utils::TextStyle;
|
||||||
use crate::cea708utils::{
|
use crate::cea708utils::Cea708Mode;
|
||||||
textstyle_foreground_color, textstyle_to_pen_color, Cea708Mode, Cea708ServiceWriter,
|
|
||||||
};
|
|
||||||
use crate::ttutils::{Chunk, Line, Lines};
|
use crate::ttutils::{Chunk, Line, Lines};
|
||||||
|
|
||||||
const DEFAULT_FPS_N: i32 = 30;
|
use super::translate::TextToCea708;
|
||||||
const DEFAULT_FPS_D: i32 = 1;
|
|
||||||
|
|
||||||
const DEFAULT_MODE: Cea708Mode = Cea708Mode::RollUp;
|
const DEFAULT_MODE: Cea708Mode = Cea708Mode::RollUp;
|
||||||
const DEFAULT_ORIGIN_ROW: i32 = -1;
|
const DEFAULT_ORIGIN_ROW: i32 = -1;
|
||||||
const DEFAULT_ORIGIN_COLUMN: u32 = 0;
|
const DEFAULT_ORIGIN_COLUMN: u32 = 0;
|
||||||
const DEFAULT_ROLL_UP_ROWS: u8 = 2;
|
const DEFAULT_ROLL_UP_ROWS: u8 = 2;
|
||||||
const DEFAULT_SERVICE_NO: u8 = 1;
|
const DEFAULT_SERVICE_NO: u8 = 1;
|
||||||
|
const DEFAULT_CEA608_CHANNEL: u8 = 0;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct Settings {
|
struct Settings {
|
||||||
mode: Cea708Mode,
|
mode: Cea708Mode,
|
||||||
service_no: u8,
|
service_no: u8,
|
||||||
|
cea608_channel: u8,
|
||||||
roll_up_rows: u8,
|
roll_up_rows: u8,
|
||||||
origin_row: i32,
|
|
||||||
origin_column: u32,
|
origin_column: u32,
|
||||||
|
origin_row: i32,
|
||||||
roll_up_timeout: Option<gst::ClockTime>,
|
roll_up_timeout: Option<gst::ClockTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,53 +47,31 @@ impl Default for Settings {
|
||||||
Settings {
|
Settings {
|
||||||
mode: DEFAULT_MODE,
|
mode: DEFAULT_MODE,
|
||||||
origin_row: DEFAULT_ORIGIN_ROW,
|
origin_row: DEFAULT_ORIGIN_ROW,
|
||||||
origin_column: DEFAULT_ORIGIN_COLUMN,
|
|
||||||
roll_up_rows: DEFAULT_ROLL_UP_ROWS,
|
roll_up_rows: DEFAULT_ROLL_UP_ROWS,
|
||||||
roll_up_timeout: gst::ClockTime::NONE,
|
roll_up_timeout: gst::ClockTime::NONE,
|
||||||
service_no: DEFAULT_SERVICE_NO,
|
service_no: DEFAULT_SERVICE_NO,
|
||||||
|
cea608_channel: DEFAULT_CEA608_CHANNEL,
|
||||||
|
origin_column: DEFAULT_ORIGIN_COLUMN,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
struct State {
|
struct State {
|
||||||
sequence_no: u8,
|
translator: TextToCea708,
|
||||||
cc_data_writer: CCDataWriter,
|
|
||||||
framerate: gst::Fraction,
|
framerate: gst::Fraction,
|
||||||
service_writer: Cea708ServiceWriter,
|
|
||||||
pen_location: SetPenLocationArgs,
|
|
||||||
pen_color: SetPenColorArgs,
|
|
||||||
pen_attributes: SetPenAttributesArgs,
|
|
||||||
mode: Cea708Mode,
|
|
||||||
erase_display_frame_no: Option<u64>,
|
|
||||||
last_frame_no: u64,
|
last_frame_no: u64,
|
||||||
max_frame_no: u64,
|
max_frame_no: u64,
|
||||||
send_roll_up_preamble: bool,
|
|
||||||
force_clear: bool,
|
force_clear: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for State {
|
impl Default for State {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
sequence_no: 0,
|
translator: TextToCea708::default(),
|
||||||
cc_data_writer: CCDataWriter::default(),
|
|
||||||
framerate: gst::Fraction::new(DEFAULT_FPS_N, DEFAULT_FPS_D),
|
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,
|
last_frame_no: 0,
|
||||||
max_frame_no: 0,
|
max_frame_no: 0,
|
||||||
send_roll_up_preamble: false,
|
|
||||||
mode: Cea708Mode::PopOn,
|
|
||||||
force_clear: false,
|
force_clear: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,80 +96,6 @@ fn cc_data_buffer(data: &[u8], pts: gst::ClockTime, duration: gst::ClockTime) ->
|
||||||
ret
|
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 {
|
pub struct TtToCea708 {
|
||||||
srcpad: gst::Pad,
|
srcpad: gst::Pad,
|
||||||
sinkpad: gst::Pad,
|
sinkpad: gst::Pad,
|
||||||
|
@ -205,263 +106,50 @@ pub struct TtToCea708 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TtToCea708 {
|
impl TtToCea708 {
|
||||||
fn open_line(
|
|
||||||
&self,
|
|
||||||
state: &mut State,
|
|
||||||
settings: &Settings,
|
|
||||||
chunk: &Chunk,
|
|
||||||
carriage_return: Option<bool>,
|
|
||||||
) {
|
|
||||||
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<std::str::Chars>) -> u32 {
|
|
||||||
chars.take_while(|c| !c.is_ascii_whitespace()).count() as u32
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate(
|
fn generate(
|
||||||
&self,
|
&self,
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
settings: &Settings,
|
|
||||||
pts: gst::ClockTime,
|
pts: gst::ClockTime,
|
||||||
duration: gst::ClockTime,
|
duration: gst::ClockTime,
|
||||||
lines: Lines,
|
lines: Lines,
|
||||||
) -> Result<gst::BufferList, gst::FlowError> {
|
) {
|
||||||
let origin_column = settings.origin_column;
|
let (fps_n, fps_d) = {
|
||||||
let mut row = 13;
|
let f = state.translator.framerate();
|
||||||
let mut bufferlist = gst::BufferList::new();
|
(f.numer() as u64, f.denom() as u64)
|
||||||
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) = (
|
|
||||||
state.framerate.numer() as u64,
|
|
||||||
state.framerate.denom() as u64,
|
|
||||||
);
|
|
||||||
|
|
||||||
let frame_no = pts.mul_div_round(fps_n, fps_d).unwrap().seconds();
|
let frame_no = pts.mul_div_round(fps_n, fps_d).unwrap().seconds();
|
||||||
|
|
||||||
if state.last_frame_no == 0 {
|
let max_frame_no = (pts + duration)
|
||||||
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)
|
|
||||||
.mul_div_round(fps_n, fps_d)
|
.mul_div_round(fps_n, fps_d)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.seconds();
|
.seconds();
|
||||||
|
|
||||||
state.pad(self, mut_list, frame_no);
|
state.translator.generate(frame_no, max_frame_no, lines);
|
||||||
|
}
|
||||||
|
|
||||||
let mut cleared = false;
|
fn pop_bufferlist(&self, state: &mut State) -> gst::BufferList {
|
||||||
let mut need_pen_location = false;
|
let (fps_n, fps_d) = {
|
||||||
if let Some(mode) = lines.mode {
|
let f = state.translator.framerate();
|
||||||
if (mode.is_rollup() && state.mode != Cea708Mode::RollUp)
|
(f.numer() as u64, f.denom() as u64)
|
||||||
|| (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;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.mode = match mode {
|
let mut bufferlist = gst::BufferList::new();
|
||||||
Cea608Mode::PopOn => Cea708Mode::PopOn,
|
let mut_list = bufferlist.get_mut().unwrap();
|
||||||
Cea608Mode::PaintOn => Cea708Mode::PaintOn,
|
while let Some(cea708) = state.translator.pop_output() {
|
||||||
Cea608Mode::RollUp2 | Cea608Mode::RollUp3 | Cea608Mode::RollUp4 => {
|
// TODO: handle framerate changes
|
||||||
Cea708Mode::RollUp
|
let pts = cea708
|
||||||
}
|
.frame_no
|
||||||
};
|
.mul_div_round(fps_d * gst::ClockTime::SECOND.nseconds(), fps_n)
|
||||||
match state.mode {
|
.unwrap()
|
||||||
Cea708Mode::RollUp => {
|
.nseconds();
|
||||||
state.send_roll_up_preamble = true;
|
let duration = 1
|
||||||
}
|
.mul_div_round(fps_d * gst::ClockTime::SECOND.nseconds(), fps_n)
|
||||||
_ => {
|
.unwrap()
|
||||||
state.pen_location.column = origin_column as u8;
|
.nseconds();
|
||||||
need_pen_location = true;
|
mut_list.add(cc_data_buffer(&cea708.packet, pts, duration));
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
bufferlist
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sink_chain(
|
fn sink_chain(
|
||||||
|
@ -546,10 +234,11 @@ impl TtToCea708 {
|
||||||
row += 1;
|
row += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let bufferlist = self.generate(&mut state, &settings, pts, duration, lines)?;
|
|
||||||
|
|
||||||
drop(settings);
|
drop(settings);
|
||||||
|
|
||||||
|
self.generate(&mut state, pts, duration, lines);
|
||||||
|
let bufferlist = self.pop_bufferlist(&mut state);
|
||||||
|
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
self.srcpad.push_list(bufferlist)
|
self.srcpad.push_list(bufferlist)
|
||||||
|
@ -584,7 +273,9 @@ impl TtToCea708 {
|
||||||
let caps = gst::Caps::builder_full().structure(s.to_owned()).build();
|
let caps = gst::Caps::builder_full().structure(s.to_owned()).build();
|
||||||
|
|
||||||
let mut state = self.state.lock().unwrap();
|
let mut state = self.state.lock().unwrap();
|
||||||
state.framerate = s.get::<gst::Fraction>("framerate").unwrap();
|
let framerate = s.get::<gst::Fraction>("framerate").unwrap();
|
||||||
|
state.framerate = framerate;
|
||||||
|
state.translator.set_framerate(framerate);
|
||||||
|
|
||||||
gst::debug!(CAT, obj: pad, "Pushing caps {}", caps);
|
gst::debug!(CAT, obj: pad, "Pushing caps {}", caps);
|
||||||
|
|
||||||
|
@ -621,10 +312,11 @@ impl TtToCea708 {
|
||||||
.seconds();
|
.seconds();
|
||||||
state.max_frame_no = frame_no;
|
state.max_frame_no = frame_no;
|
||||||
|
|
||||||
let mut bufferlist = gst::BufferList::new();
|
let last_frame_no = state.last_frame_no;
|
||||||
let mut_list = bufferlist.get_mut().unwrap();
|
state
|
||||||
|
.translator
|
||||||
state.pad(self, mut_list, frame_no);
|
.generate(last_frame_no, frame_no, Lines::new_empty());
|
||||||
|
let bufferlist = self.pop_bufferlist(&mut state);
|
||||||
|
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
|
@ -634,12 +326,15 @@ impl TtToCea708 {
|
||||||
}
|
}
|
||||||
EventView::Eos(_) => {
|
EventView::Eos(_) => {
|
||||||
let mut state = self.state.lock().unwrap();
|
let mut state = self.state.lock().unwrap();
|
||||||
if let Some(erase_display_frame_no) = state.erase_display_frame_no {
|
if let Some(erase_display_frame_no) = state.translator.erase_display_frame_no() {
|
||||||
let mut bufferlist = gst::BufferList::new();
|
|
||||||
let mut_list = bufferlist.get_mut().unwrap();
|
|
||||||
|
|
||||||
state.max_frame_no = 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);
|
drop(state);
|
||||||
|
|
||||||
|
@ -653,14 +348,21 @@ impl TtToCea708 {
|
||||||
EventView::FlushStop(_) => {
|
EventView::FlushStop(_) => {
|
||||||
let mut state = self.state.lock().unwrap();
|
let mut state = self.state.lock().unwrap();
|
||||||
let settings = self.settings.lock().unwrap();
|
let settings = self.settings.lock().unwrap();
|
||||||
|
let framerate = state.framerate;
|
||||||
|
|
||||||
*state = State::default();
|
*state = State::default();
|
||||||
|
|
||||||
state.mode = settings.mode;
|
state.framerate = framerate;
|
||||||
|
state.translator.set_mode(settings.mode);
|
||||||
if state.mode != Cea708Mode::PopOn {
|
state.translator.set_origin_column(settings.origin_column);
|
||||||
state.send_roll_up_preamble = true;
|
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(settings);
|
||||||
drop(state);
|
drop(state);
|
||||||
|
@ -749,6 +451,20 @@ impl ObjectImpl for TtToCea708 {
|
||||||
.minimum(1)
|
.minimum(1)
|
||||||
.maximum(63)
|
.maximum(63)
|
||||||
.build(),
|
.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) {
|
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||||
match pspec.name() {
|
match pspec.name() {
|
||||||
"mode" => {
|
"mode" => {
|
||||||
|
// XXX: Ideally we'd like to not lock the state here
|
||||||
let mut state = self.state.lock().unwrap();
|
let mut state = self.state.lock().unwrap();
|
||||||
let mut settings = self.settings.lock().unwrap();
|
let mut settings = self.settings.lock().unwrap();
|
||||||
settings.mode = value.get::<Cea708Mode>().expect("type checked upstream");
|
settings.mode = value.get::<Cea708Mode>().expect("type checked upstream");
|
||||||
state.force_clear = true;
|
state.force_clear = true;
|
||||||
}
|
}
|
||||||
"origin-row" => {
|
"origin-row" => {
|
||||||
|
// XXX: Ideally we'd like to not lock the state here
|
||||||
let mut state = self.state.lock().unwrap();
|
let mut state = self.state.lock().unwrap();
|
||||||
let mut settings = self.settings.lock().unwrap();
|
let mut settings = self.settings.lock().unwrap();
|
||||||
settings.origin_row = value.get().expect("type checked upstream");
|
settings.origin_row = value.get().expect("type checked upstream");
|
||||||
state.force_clear = true;
|
state.force_clear = true;
|
||||||
}
|
}
|
||||||
"origin-column" => {
|
"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 state = self.state.lock().unwrap();
|
||||||
|
let mut settings = self.settings.lock().unwrap();
|
||||||
settings.origin_column = value.get().expect("type checked upstream");
|
settings.origin_column = value.get().expect("type checked upstream");
|
||||||
state.force_clear = true;
|
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" => {
|
"roll-up-timeout" => {
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
let mut settings = self.settings.lock().unwrap();
|
let mut settings = self.settings.lock().unwrap();
|
||||||
|
|
||||||
let timeout = value.get().expect("type checked upstream");
|
let timeout = match value.get().expect("type checked upstream") {
|
||||||
|
|
||||||
settings.roll_up_timeout = match timeout {
|
|
||||||
u64::MAX => gst::ClockTime::NONE,
|
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" => {
|
"service-number" => {
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
let mut settings = self.settings.lock().unwrap();
|
let mut settings = self.settings.lock().unwrap();
|
||||||
settings.service_no = value.get::<u32>().expect("type checked upstream") as u8;
|
settings.service_no = value.get::<u32>().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::<u32>().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::<u32>().expect("type checked upstream") as u8;
|
||||||
|
state.translator.set_roll_up_count(settings.roll_up_rows);
|
||||||
}
|
}
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
|
@ -829,6 +565,14 @@ impl ObjectImpl for TtToCea708 {
|
||||||
let settings = self.settings.lock().unwrap();
|
let settings = self.settings.lock().unwrap();
|
||||||
(settings.service_no as u32).to_value()
|
(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!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -858,11 +602,6 @@ impl ElementImpl for TtToCea708 {
|
||||||
|
|
||||||
let s = gst::Structure::builder("text/x-raw").build();
|
let s = gst::Structure::builder("text/x-raw").build();
|
||||||
caps.append_structure(s);
|
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(
|
let sink_pad_template = gst::PadTemplate::new(
|
||||||
|
@ -908,13 +647,19 @@ impl ElementImpl for TtToCea708 {
|
||||||
gst::StateChange::ReadyToPaused => {
|
gst::StateChange::ReadyToPaused => {
|
||||||
let mut state = self.state.lock().unwrap();
|
let mut state = self.state.lock().unwrap();
|
||||||
let settings = self.settings.lock().unwrap();
|
let settings = self.settings.lock().unwrap();
|
||||||
|
let framerate = state.framerate;
|
||||||
*state = State::default();
|
*state = State::default();
|
||||||
state.force_clear = false;
|
state.force_clear = false;
|
||||||
state.mode = settings.mode;
|
state.translator.set_mode(settings.mode);
|
||||||
if state.mode != Cea708Mode::PopOn {
|
state.translator.set_origin_column(settings.origin_column);
|
||||||
state.send_roll_up_preamble = true;
|
state.translator.set_framerate(framerate);
|
||||||
state.pen_location.column = settings.origin_column as u8;
|
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();
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ use gst::glib;
|
||||||
use gst::prelude::*;
|
use gst::prelude::*;
|
||||||
|
|
||||||
mod imp;
|
mod imp;
|
||||||
|
mod translate;
|
||||||
|
|
||||||
glib::wrapper! {
|
glib::wrapper! {
|
||||||
pub struct TtToCea708(ObjectSubclass<imp::TtToCea708>) @extends gst::Element, gst::Object;
|
pub struct TtToCea708(ObjectSubclass<imp::TtToCea708>) @extends gst::Element, gst::Object;
|
||||||
|
|
520
video/closedcaption/src/tttocea708/translate.rs
Normal file
520
video/closedcaption/src/tttocea708/translate.rs
Normal file
|
@ -0,0 +1,520 @@
|
||||||
|
// Copyright (C) 2020 Mathieu Duponchelle <mathieu@centricular.com>
|
||||||
|
// Copyright (C) 2023 Matthew Waters <matthew@centricular.com>
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// <https://mozilla.org/MPL/2.0/>.
|
||||||
|
//
|
||||||
|
// 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<gst::DebugCategory> = 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<std::str::Chars>) -> 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<gst::ClockTime>,
|
||||||
|
framerate: gst::Fraction,
|
||||||
|
|
||||||
|
// state
|
||||||
|
service_writer: Cea708ServiceWriter,
|
||||||
|
cc_data_writer: CCDataWriter,
|
||||||
|
output_packets: VecDeque<TimedCea708>,
|
||||||
|
sequence_no: u8,
|
||||||
|
pen_location: SetPenLocationArgs,
|
||||||
|
pen_color: SetPenColorArgs,
|
||||||
|
pen_attributes: SetPenAttributesArgs,
|
||||||
|
send_roll_up_preamble: bool,
|
||||||
|
erase_display_frame_no: Option<u64>,
|
||||||
|
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<u8>,
|
||||||
|
pub frame_no: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextToCea708 {
|
||||||
|
pub fn pop_output(&mut self) -> Option<TimedCea708> {
|
||||||
|
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<gst::ClockTime>) {
|
||||||
|
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<u64> {
|
||||||
|
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<bool>) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue