From 593cb6c7fcee64db4360e20dae77546d9cff45d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Tue, 17 Sep 2024 13:38:58 +0300 Subject: [PATCH] closedcaption: Add Closed Caption to ST2038 element Part-of: --- video/closedcaption/src/cctost2038anc/imp.rs | 351 +++++++++++++++++++ video/closedcaption/src/cctost2038anc/mod.rs | 25 ++ video/closedcaption/src/lib.rs | 2 + video/closedcaption/src/st2038anc_utils.rs | 72 ++++ 4 files changed, 450 insertions(+) create mode 100644 video/closedcaption/src/cctost2038anc/imp.rs create mode 100644 video/closedcaption/src/cctost2038anc/mod.rs diff --git a/video/closedcaption/src/cctost2038anc/imp.rs b/video/closedcaption/src/cctost2038anc/imp.rs new file mode 100644 index 00000000..7f777258 --- /dev/null +++ b/video/closedcaption/src/cctost2038anc/imp.rs @@ -0,0 +1,351 @@ +// Copyright (C) 2024 Sebastian Dröge +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 + +use std::sync::Mutex; + +use gst::glib; +use gst::prelude::*; +use gst::subclass::prelude::*; + +use atomic_refcell::AtomicRefCell; + +use once_cell::sync::Lazy; + +use crate::st2038anc_utils::convert_to_st2038_buffer; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Format { + Cea608, + Cea708, +} + +#[derive(Default)] +struct State { + format: Option, +} + +#[derive(Clone)] +struct Settings { + c_not_y_channel: bool, + line_number: u16, + horizontal_offset: u16, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + c_not_y_channel: false, + line_number: 9, + horizontal_offset: 0, + } + } +} + +pub struct CcToSt2038Anc { + sinkpad: gst::Pad, + srcpad: gst::Pad, + + state: AtomicRefCell, + settings: Mutex, +} + +static CAT: Lazy = Lazy::new(|| { + gst::DebugCategory::new( + "cctost2038anc", + gst::DebugColorFlags::empty(), + Some("Closed Caption to ST-2038 ANC Element"), + ) +}); + +impl CcToSt2038Anc { + fn sink_chain( + &self, + pad: &gst::Pad, + buffer: gst::Buffer, + ) -> Result { + gst::trace!(CAT, obj = pad, "Handling buffer {:?}", buffer); + + let state = self.state.borrow_mut(); + let settings = self.settings.lock().unwrap().clone(); + + let map = buffer.map_readable().map_err(|_| { + gst::error!(CAT, obj = pad, "Can't map buffer readable"); + + gst::FlowError::Error + })?; + + let (did, sdid) = match state.format { + Some(Format::Cea608) => (0x61, 0x02), + Some(Format::Cea708) => (0x61, 0x01), + None => { + gst::error!(CAT, imp = self, "No caps set"); + return Err(gst::FlowError::NotNegotiated); + } + }; + + let mut outbuf = match convert_to_st2038_buffer( + settings.c_not_y_channel, + settings.line_number, + settings.horizontal_offset, + did, + sdid, + &map, + ) { + Ok(outbuf) => outbuf, + Err(err) => { + gst::error!( + CAT, + imp = self, + "Can't convert Closed Caption buffer: {err}" + ); + return Err(gst::FlowError::Error); + } + }; + drop(map); + + { + let outbuf = outbuf.get_mut().unwrap(); + let _ = buffer.copy_into(outbuf, gst::BUFFER_COPY_METADATA, ..); + } + drop(state); + + self.srcpad.push(outbuf) + } + + #[allow(clippy::single_match)] + fn sink_event(&self, pad: &gst::Pad, event: gst::Event) -> bool { + use gst::EventView; + + gst::log!(CAT, obj = pad, "Handling event {:?}", event); + match event.view() { + EventView::Caps(ev) => { + let caps = ev.caps(); + let s = caps.structure(0).unwrap(); + + let format = match s.name().as_str() { + "closedcaption/x-cea-608" => Format::Cea608, + "closedcaption/x-cea-708" => Format::Cea708, + _ => { + gst::error!(CAT, imp = self, "Unsupported caps {caps:?}"); + return false; + } + }; + + gst::debug!(CAT, imp = self, "Configuring format {format:?}"); + + let mut state = self.state.borrow_mut(); + state.format = Some(format); + drop(state); + + return self.srcpad.push_event( + gst::event::Caps::builder(&self.srcpad.pad_template_caps()) + .seqnum(ev.seqnum()) + .build(), + ); + } + _ => (), + } + + gst::Pad::event_default(pad, Some(&*self.obj()), event) + } +} + +#[glib::object_subclass] +impl ObjectSubclass for CcToSt2038Anc { + const NAME: &'static str = "GstCcToSt2038Anc"; + type Type = super::CcToSt2038Anc; + type ParentType = gst::Element; + + fn with_class(klass: &Self::Class) -> Self { + let templ = klass.pad_template("sink").unwrap(); + let sinkpad = gst::Pad::builder_from_template(&templ) + .chain_function(|pad, parent, buffer| { + CcToSt2038Anc::catch_panic_pad_function( + parent, + || Err(gst::FlowError::Error), + |this| this.sink_chain(pad, buffer), + ) + }) + .event_function(|pad, parent, event| { + CcToSt2038Anc::catch_panic_pad_function( + parent, + || false, + |this| this.sink_event(pad, event), + ) + }) + .flags(gst::PadFlags::FIXED_CAPS) + .build(); + + let templ = klass.pad_template("src").unwrap(); + let srcpad = gst::Pad::builder_from_template(&templ) + .flags(gst::PadFlags::FIXED_CAPS) + .build(); + + Self { + sinkpad, + srcpad, + state: AtomicRefCell::new(State::default()), + settings: Mutex::new(Settings::default()), + } + } +} + +impl ObjectImpl for CcToSt2038Anc { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![ + glib::ParamSpecBoolean::builder("c-not-y-channel") + .nick("Y Not C Channel") + .blurb("Set the y_not_c_channel flag in the output") + .default_value(Settings::default().c_not_y_channel) + .mutable_playing() + .build(), + glib::ParamSpecUInt::builder("line-number") + .nick("Line Number") + .blurb("Line Number of the output") + .default_value(Settings::default().line_number as u32) + .maximum(2047) + .mutable_playing() + .build(), + glib::ParamSpecUInt::builder("horizontal-offset") + .nick("Horizontal Offset") + .blurb("Horizontal offset of the output") + .default_value(Settings::default().horizontal_offset as u32) + .maximum(4095) + .mutable_playing() + .build(), + ] + }); + + PROPERTIES.as_ref() + } + + fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + match pspec.name() { + "c-not-y-channel" => { + let mut settings = self.settings.lock().unwrap(); + + settings.c_not_y_channel = value.get().expect("type checked upstream"); + } + "line-number" => { + let mut settings = self.settings.lock().unwrap(); + + settings.line_number = value.get::().expect("type checked upstream") as u16; + } + "horizontal-offset" => { + let mut settings = self.settings.lock().unwrap(); + + settings.horizontal_offset = + value.get::().expect("type checked upstream") as u16; + } + _ => unimplemented!(), + } + } + + fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "c-not-y-channel" => { + let settings = self.settings.lock().unwrap(); + settings.c_not_y_channel.to_value() + } + "line-number" => { + let settings = self.settings.lock().unwrap(); + (settings.line_number as u32).to_value() + } + "horizontal-offset" => { + let settings = self.settings.lock().unwrap(); + (settings.horizontal_offset as u32).to_value() + } + _ => unimplemented!(), + } + } + + fn constructed(&self) { + self.parent_constructed(); + + let obj = self.obj(); + obj.add_pad(&self.sinkpad).unwrap(); + obj.add_pad(&self.srcpad).unwrap(); + } +} + +impl GstObjectImpl for CcToSt2038Anc {} + +impl ElementImpl for CcToSt2038Anc { + fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { + static ELEMENT_METADATA: Lazy = Lazy::new(|| { + gst::subclass::ElementMetadata::new( + "CC to ST-2038 ANC", + "Generic", + "Converts Closed Captions to ST-2038 ANC", + "Sebastian Dröge ", + ) + }); + + Some(&*ELEMENT_METADATA) + } + + fn pad_templates() -> &'static [gst::PadTemplate] { + static PAD_TEMPLATES: Lazy> = Lazy::new(|| { + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Sometimes, + &gst::Caps::builder("meta/x-st-2038").build(), + ) + .unwrap(); + + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &[ + gst::Structure::builder("closedcaption/x-cea-608") + .field("format", "s334-1a") + .build(), + gst::Structure::builder("closedcaption/x-cea-708") + .field("format", "cdp") + .build(), + ] + .into_iter() + .collect::(), + ) + .unwrap(); + + vec![src_pad_template, sink_pad_template] + }); + + PAD_TEMPLATES.as_ref() + } + + #[allow(clippy::single_match)] + fn change_state( + &self, + transition: gst::StateChange, + ) -> Result { + gst::trace!(CAT, imp = self, "Changing state {:?}", transition); + + match transition { + gst::StateChange::ReadyToPaused => { + *self.state.borrow_mut() = State::default(); + } + _ => (), + } + + let ret = self.parent_change_state(transition)?; + + match transition { + gst::StateChange::PausedToReady => { + *self.state.borrow_mut() = State::default(); + } + _ => (), + } + + Ok(ret) + } +} diff --git a/video/closedcaption/src/cctost2038anc/mod.rs b/video/closedcaption/src/cctost2038anc/mod.rs new file mode 100644 index 00000000..4e623a6a --- /dev/null +++ b/video/closedcaption/src/cctost2038anc/mod.rs @@ -0,0 +1,25 @@ +// Copyright (C) 2024 Sebastian Dröge +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 + +use gst::glib; +use gst::prelude::*; + +mod imp; + +glib::wrapper! { + pub struct CcToSt2038Anc(ObjectSubclass) @extends gst::Element, gst::Object; +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "cctost2038anc", + gst::Rank::NONE, + CcToSt2038Anc::static_type(), + ) +} diff --git a/video/closedcaption/src/lib.rs b/video/closedcaption/src/lib.rs index 9937f60c..8f747a00 100644 --- a/video/closedcaption/src/lib.rs +++ b/video/closedcaption/src/lib.rs @@ -18,6 +18,7 @@ use gst::glib; use gst::prelude::*; mod ccdetect; +mod cctost2038anc; mod ccutils; mod cea608overlay; mod cea608tocea708; @@ -69,6 +70,7 @@ fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { st2038ancdemux::register(plugin)?; st2038ancmux::register(plugin)?; st2038anctocc::register(plugin)?; + cctost2038anc::register(plugin)?; Ok(()) } diff --git a/video/closedcaption/src/st2038anc_utils.rs b/video/closedcaption/src/st2038anc_utils.rs index 918cb17b..d8225640 100644 --- a/video/closedcaption/src/st2038anc_utils.rs +++ b/video/closedcaption/src/st2038anc_utils.rs @@ -68,3 +68,75 @@ impl AncDataHeader { }) } } + +fn extend_with_even_odd_parity(v: u8, checksum: &mut u16) -> u16 { + let parity = v.count_ones() & 1; + let res = if parity == 0 { + 0x1_00 | (v as u16) + } else { + 0x2_00 | (v as u16) + }; + + *checksum = checksum.wrapping_add(res); + + res +} + +pub(crate) fn convert_to_st2038_buffer( + c_not_y_channel: bool, + line_number: u16, + horizontal_offset: u16, + did: u8, + sdid: u8, + payload: &[u8], +) -> Result { + if payload.len() > 255 { + anyhow::bail!( + "Payload needs to be less than 256 bytes, got {}", + payload.len() + ); + } + + use anyhow::Context; + use bitstream_io::{BigEndian, BitWrite, BitWriter}; + + let mut output = Vec::with_capacity((70 + payload.len() * 10) / 8 + 1); + + let mut w = BitWriter::endian(&mut output, BigEndian); + + w.write::(6, 0b00_0000).context("zero bits")?; + w.write_bit(c_not_y_channel).context("c_not_y_channel")?; + w.write::(11, line_number).context("line number")?; + w.write::(12, horizontal_offset) + .context("horizontal offset")?; + + let mut checksum = 0u16; + + w.write::(10, extend_with_even_odd_parity(did, &mut checksum)) + .context("DID")?; + w.write::(10, extend_with_even_odd_parity(sdid, &mut checksum)) + .context("SDID")?; + w.write::( + 10, + extend_with_even_odd_parity(payload.len() as u8, &mut checksum), + ) + .context("data count")?; + + for &b in payload { + w.write::(10, extend_with_even_odd_parity(b, &mut checksum)) + .context("payload")?; + } + + checksum &= 0x1_ff; + checksum |= ((!(checksum >> 8)) & 0x0_01) << 9; + + w.write::(10, checksum).context("checksum")?; + + while !w.byte_aligned() { + w.write_bit(true).context("padding")?; + } + + w.flush().context("flushing")?; + + Ok(gst::Buffer::from_mut_slice(output)) +}