mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2025-01-03 07:48:48 +00:00
closedcaption: Add ST2038 muxer element
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1777>
This commit is contained in:
parent
a4dcb52ca7
commit
b2e37d3c98
6 changed files with 680 additions and 7 deletions
|
@ -22,7 +22,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||||
cea708-types = "0.3.2"
|
cea708-types = "0.3.2"
|
||||||
cea608-types = "0.1.1"
|
cea608-types = "0.1.1"
|
||||||
gst = { workspace = true, features = ["v1_16"]}
|
gst = { workspace = true, features = ["v1_20"]}
|
||||||
gst-base = { workspace = true, features = ["v1_18"]}
|
gst-base = { workspace = true, features = ["v1_18"]}
|
||||||
gst-video = { workspace = true, features = ["v1_16"]}
|
gst-video = { workspace = true, features = ["v1_16"]}
|
||||||
winnow = "0.6"
|
winnow = "0.6"
|
||||||
|
@ -47,6 +47,7 @@ gst-plugin-version-helper.workspace = true
|
||||||
static = []
|
static = []
|
||||||
capi = []
|
capi = []
|
||||||
doc = ["gst/v1_18"]
|
doc = ["gst/v1_18"]
|
||||||
|
v1_26 = ["gst-base/v1_26"]
|
||||||
|
|
||||||
[package.metadata.capi]
|
[package.metadata.capi]
|
||||||
min_version = "0.9.21"
|
min_version = "0.9.21"
|
||||||
|
|
|
@ -36,6 +36,7 @@ mod scc_enc;
|
||||||
mod scc_parse;
|
mod scc_parse;
|
||||||
mod st2038anc_utils;
|
mod st2038anc_utils;
|
||||||
mod st2038ancdemux;
|
mod st2038ancdemux;
|
||||||
|
mod st2038ancmux;
|
||||||
mod transcriberbin;
|
mod transcriberbin;
|
||||||
mod tttocea608;
|
mod tttocea608;
|
||||||
mod tttocea708;
|
mod tttocea708;
|
||||||
|
@ -65,6 +66,7 @@ fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||||
tttocea708::register(plugin)?;
|
tttocea708::register(plugin)?;
|
||||||
cea708overlay::register(plugin)?;
|
cea708overlay::register(plugin)?;
|
||||||
st2038ancdemux::register(plugin)?;
|
st2038ancdemux::register(plugin)?;
|
||||||
|
st2038ancmux::register(plugin)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,13 +8,14 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub(crate) struct AncDataHeader {
|
pub(crate) struct AncDataHeader {
|
||||||
pub(crate) c_not_y_channel_flag: bool,
|
pub(crate) c_not_y_channel_flag: bool,
|
||||||
pub(crate) did: u8,
|
pub(crate) did: u8,
|
||||||
pub(crate) sdid: u8,
|
pub(crate) sdid: u8,
|
||||||
pub(crate) line_number: u16,
|
pub(crate) line_number: u16,
|
||||||
pub(crate) horizontal_offset: u16,
|
pub(crate) horizontal_offset: u16,
|
||||||
|
pub(crate) data_count: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AncDataHeader {
|
impl AncDataHeader {
|
||||||
|
@ -34,7 +35,7 @@ impl AncDataHeader {
|
||||||
// Top two bits are parity bits and can be stripped off
|
// Top two bits are parity bits and can be stripped off
|
||||||
let did = (r.read::<u16>(10).context("DID")? & 0xff) as u8;
|
let did = (r.read::<u16>(10).context("DID")? & 0xff) as u8;
|
||||||
let sdid = (r.read::<u16>(10).context("SDID")? & 0xff) as u8;
|
let sdid = (r.read::<u16>(10).context("SDID")? & 0xff) as u8;
|
||||||
let _data_count = (r.read::<u16>(10).context("data count")? & 0xff) as u8;
|
let data_count = (r.read::<u16>(10).context("data count")? & 0xff) as u8;
|
||||||
|
|
||||||
Ok(AncDataHeader {
|
Ok(AncDataHeader {
|
||||||
c_not_y_channel_flag,
|
c_not_y_channel_flag,
|
||||||
|
@ -42,6 +43,7 @@ impl AncDataHeader {
|
||||||
horizontal_offset,
|
horizontal_offset,
|
||||||
did,
|
did,
|
||||||
sdid,
|
sdid,
|
||||||
|
data_count,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,12 +37,33 @@ pub struct St2038AncDemux {
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct State {
|
struct State {
|
||||||
streams: HashMap<AncDataHeader, AncStream>,
|
streams: HashMap<AncDataId, AncStream>,
|
||||||
flow_combiner: UniqueFlowCombiner,
|
flow_combiner: UniqueFlowCombiner,
|
||||||
segment: gst::FormattedSegment<gst::ClockTime>,
|
segment: gst::FormattedSegment<gst::ClockTime>,
|
||||||
last_inactivity_check: Option<gst::ClockTime>,
|
last_inactivity_check: Option<gst::ClockTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct AncDataId {
|
||||||
|
c_not_y_channel_flag: bool,
|
||||||
|
did: u8,
|
||||||
|
sdid: u8,
|
||||||
|
line_number: u16,
|
||||||
|
horizontal_offset: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AncDataHeader> for AncDataId {
|
||||||
|
fn from(value: AncDataHeader) -> Self {
|
||||||
|
AncDataId {
|
||||||
|
c_not_y_channel_flag: value.c_not_y_channel_flag,
|
||||||
|
did: value.did,
|
||||||
|
sdid: value.sdid,
|
||||||
|
line_number: value.line_number,
|
||||||
|
horizontal_offset: value.horizontal_offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct AncStream {
|
struct AncStream {
|
||||||
pad: gst::Pad,
|
pad: gst::Pad,
|
||||||
last_used: Option<gst::ClockTime>,
|
last_used: Option<gst::ClockTime>,
|
||||||
|
@ -71,7 +92,7 @@ impl St2038AncDemux {
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let stream = match state.streams.get_mut(&anc_hdr) {
|
let stream = match state.streams.get_mut(&AncDataId::from(anc_hdr)) {
|
||||||
Some(stream) => stream,
|
Some(stream) => stream,
|
||||||
None => {
|
None => {
|
||||||
let pad_name = format!(
|
let pad_name = format!(
|
||||||
|
@ -104,14 +125,17 @@ impl St2038AncDemux {
|
||||||
state.flow_combiner.add_pad(&anc_srcpad);
|
state.flow_combiner.add_pad(&anc_srcpad);
|
||||||
|
|
||||||
state.streams.insert(
|
state.streams.insert(
|
||||||
anc_hdr.clone(),
|
AncDataId::from(anc_hdr),
|
||||||
AncStream {
|
AncStream {
|
||||||
pad: anc_srcpad,
|
pad: anc_srcpad,
|
||||||
last_used: running_time,
|
last_used: running_time,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
state.streams.get_mut(&anc_hdr).expect("stream")
|
state
|
||||||
|
.streams
|
||||||
|
.get_mut(&AncDataId::from(anc_hdr))
|
||||||
|
.expect("stream")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
612
video/closedcaption/src/st2038ancmux/imp.rs
Normal file
612
video/closedcaption/src/st2038ancmux/imp.rs
Normal file
|
@ -0,0 +1,612 @@
|
||||||
|
// Copyright (C) 2024 Sebastian Dröge <sebastian@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::BTreeMap;
|
||||||
|
use std::ops::ControlFlow;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use gst::glib;
|
||||||
|
use gst::prelude::*;
|
||||||
|
use gst::subclass::prelude::*;
|
||||||
|
use gst_base::prelude::*;
|
||||||
|
use gst_base::subclass::prelude::*;
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use crate::st2038anc_utils::AncDataHeader;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct State {
|
||||||
|
downstream_framerate: Option<gst::Fraction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct St2038AncMux {
|
||||||
|
state: Mutex<State>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||||
|
gst::DebugCategory::new(
|
||||||
|
"st2038ancmux",
|
||||||
|
gst::DebugColorFlags::empty(),
|
||||||
|
Some("ST2038 Anc Mux Element"),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
impl AggregatorImpl for St2038AncMux {
|
||||||
|
fn aggregate(&self, timeout: bool) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||||
|
let state = self.state.lock().unwrap();
|
||||||
|
let src_segment = self
|
||||||
|
.obj()
|
||||||
|
.src_pad()
|
||||||
|
.segment()
|
||||||
|
.downcast::<gst::ClockTime>()
|
||||||
|
.expect("Non-TIME segment");
|
||||||
|
|
||||||
|
let start_running_time =
|
||||||
|
if src_segment.position().is_none() || src_segment.position() < src_segment.start() {
|
||||||
|
src_segment.start().unwrap()
|
||||||
|
} else {
|
||||||
|
src_segment.position().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only if downstream framerate provided, otherwise we output as we go
|
||||||
|
let duration = if let Some(framerate) = state.downstream_framerate {
|
||||||
|
gst::ClockTime::SECOND
|
||||||
|
.nseconds()
|
||||||
|
.mul_div_round(framerate.denom() as u64, framerate.numer() as u64)
|
||||||
|
.unwrap()
|
||||||
|
.nseconds()
|
||||||
|
} else {
|
||||||
|
gst::ClockTime::ZERO
|
||||||
|
};
|
||||||
|
let end_running_time = start_running_time + duration;
|
||||||
|
drop(state);
|
||||||
|
|
||||||
|
gst::trace!(
|
||||||
|
CAT,
|
||||||
|
imp = self,
|
||||||
|
"Aggregating for start time {} end {} timeout {}",
|
||||||
|
start_running_time.display(),
|
||||||
|
end_running_time.display(),
|
||||||
|
timeout
|
||||||
|
);
|
||||||
|
|
||||||
|
let sinkpads = self.obj().sink_pads();
|
||||||
|
|
||||||
|
// Collect buffers from all pads. We can start outputting for this frame on timeout,
|
||||||
|
// or otherwise all pads are either EOS or have a buffer for a future frame.
|
||||||
|
let mut all_pads_done = true;
|
||||||
|
let mut all_pads_eos = true;
|
||||||
|
let mut min_next_buffer_running_time = None;
|
||||||
|
|
||||||
|
for pad in sinkpads
|
||||||
|
.iter()
|
||||||
|
.map(|pad| pad.downcast_ref::<super::St2038AncMuxSinkPad>().unwrap())
|
||||||
|
{
|
||||||
|
let mut pad_state = pad.imp().pad_state.lock().unwrap();
|
||||||
|
|
||||||
|
if pad.is_eos() {
|
||||||
|
// This pad is done
|
||||||
|
gst::trace!(CAT, obj = pad, "Pad is EOS");
|
||||||
|
if !pad_state.queued_buffers.is_empty() {
|
||||||
|
all_pads_eos = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
all_pads_eos = false;
|
||||||
|
|
||||||
|
let buffer = if let Some(buffer) = pad.peek_buffer() {
|
||||||
|
buffer
|
||||||
|
} else {
|
||||||
|
all_pads_done = false;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let segment = pad.segment().downcast::<gst::ClockTime>().unwrap();
|
||||||
|
let Some(buffer_start_ts) = segment.to_running_time(buffer.pts()) else {
|
||||||
|
gst::warning!(CAT, obj = pad, "Buffer without valid PTS, dropping");
|
||||||
|
pad.drop_buffer();
|
||||||
|
all_pads_done = false;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if buffer_start_ts > end_running_time
|
||||||
|
|| (end_running_time > start_running_time && buffer_start_ts == end_running_time)
|
||||||
|
{
|
||||||
|
gst::trace!(
|
||||||
|
CAT,
|
||||||
|
obj = pad,
|
||||||
|
"Buffer starting at {buffer_start_ts} >= {end_running_time}"
|
||||||
|
);
|
||||||
|
|
||||||
|
if min_next_buffer_running_time.map_or(true, |next_buffer_min_running_time| {
|
||||||
|
next_buffer_min_running_time > buffer_start_ts
|
||||||
|
}) {
|
||||||
|
min_next_buffer_running_time = Some(buffer_start_ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// buffer is not for this frame so we're not interested in it yet
|
||||||
|
// and this pad is done for this frame.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store buffers on the pad
|
||||||
|
gst::trace!(
|
||||||
|
CAT,
|
||||||
|
obj = pad,
|
||||||
|
"Queueing buffer starting at {buffer_start_ts}"
|
||||||
|
);
|
||||||
|
pad_state.queued_buffers.push(buffer);
|
||||||
|
pad.drop_buffer();
|
||||||
|
|
||||||
|
// Check again if there's another buffer on this pad for this frame
|
||||||
|
all_pads_done = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !all_pads_done && !timeout {
|
||||||
|
gst::trace!(CAT, imp = self, "Not all pads ready yet");
|
||||||
|
return Err(gst_base::AGGREGATOR_FLOW_NEED_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if all_pads_eos {
|
||||||
|
gst::debug!(CAT, imp = self, "All pads EOS");
|
||||||
|
return Err(gst::FlowError::Eos);
|
||||||
|
}
|
||||||
|
|
||||||
|
gst::trace!(CAT, imp = self, "Ready for outputting");
|
||||||
|
|
||||||
|
self.obj()
|
||||||
|
.selected_samples(start_running_time, None, duration, None);
|
||||||
|
|
||||||
|
// Remove all overlapping anc buffers from the queued buffers. The latest pad, latest
|
||||||
|
// buffer of that pad wins.
|
||||||
|
let mut lines =
|
||||||
|
BTreeMap::<u16, BTreeMap<u16, (u16, super::St2038AncMuxSinkPad, gst::Buffer)>>::new();
|
||||||
|
for pad in sinkpads
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.map(|pad| pad.downcast_ref::<super::St2038AncMuxSinkPad>().unwrap())
|
||||||
|
{
|
||||||
|
let mut pad_state = pad.imp().pad_state.lock().unwrap();
|
||||||
|
|
||||||
|
for buffer in pad_state.queued_buffers.drain(..).rev() {
|
||||||
|
if buffer.size() == 0
|
||||||
|
&& buffer.flags().contains(gst::BufferFlags::GAP)
|
||||||
|
&& gst::meta::CustomMeta::from_buffer(&buffer, "GstAggregatorMissingDataMeta")
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
gst::trace!(CAT, obj = pad, "Dropping gap buffer");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let header = match AncDataHeader::from_buffer(&buffer) {
|
||||||
|
Ok(header) => header,
|
||||||
|
Err(err) => {
|
||||||
|
gst::warning!(
|
||||||
|
CAT,
|
||||||
|
obj = pad,
|
||||||
|
"Dropping buffer with invalid ST2038 data ({err})"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
gst::trace!(CAT, obj = pad, "Parsed ST2038 header {header:?}");
|
||||||
|
|
||||||
|
// FIXME: One pixel per word of data? ADF header needs to be included in the
|
||||||
|
// calculation? Two words per pixel because 4:2:2 YUV? Nobody knows!
|
||||||
|
let buffer_clone = buffer.clone(); // FIXME: To appease the borrow checker
|
||||||
|
lines
|
||||||
|
.entry(header.line_number)
|
||||||
|
.and_modify(|line| {
|
||||||
|
let new_offset = header.horizontal_offset;
|
||||||
|
let new_offset_end = header.horizontal_offset + header.data_count as u16;
|
||||||
|
|
||||||
|
for (offset, (offset_end, _pad, _buffer)) in &*line {
|
||||||
|
// If one of the range starts is between the start/end of the other
|
||||||
|
// then the two ranges are overlapping.
|
||||||
|
if (new_offset >= *offset && new_offset < *offset_end)
|
||||||
|
|| (*offset >= new_offset && *offset < new_offset_end)
|
||||||
|
{
|
||||||
|
gst::trace!(
|
||||||
|
CAT,
|
||||||
|
obj = pad,
|
||||||
|
"Not including ST2038 packet at {}x{}",
|
||||||
|
header.line_number,
|
||||||
|
header.horizontal_offset
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gst::trace!(
|
||||||
|
CAT,
|
||||||
|
obj = pad,
|
||||||
|
"Including ST2038 packet at {}x{}",
|
||||||
|
header.line_number,
|
||||||
|
header.horizontal_offset
|
||||||
|
);
|
||||||
|
|
||||||
|
line.insert(new_offset, (new_offset_end, pad.clone(), buffer));
|
||||||
|
})
|
||||||
|
.or_insert_with(|| {
|
||||||
|
gst::trace!(
|
||||||
|
CAT,
|
||||||
|
obj = pad,
|
||||||
|
"Including ST2038 packet at {}x{}",
|
||||||
|
header.line_number,
|
||||||
|
header.horizontal_offset
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut line = BTreeMap::new();
|
||||||
|
line.insert(
|
||||||
|
header.horizontal_offset,
|
||||||
|
(
|
||||||
|
header.horizontal_offset + header.data_count as u16,
|
||||||
|
pad.clone(),
|
||||||
|
buffer_clone,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
line
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all anc buffers for this frame and output them as a single buffer list,
|
||||||
|
// sorted by line. Multiple anc in a single line are merged into a single buffer.
|
||||||
|
let ret = if !lines.is_empty() {
|
||||||
|
let mut buffers = gst::BufferList::new();
|
||||||
|
|
||||||
|
let buffers_ref = buffers.get_mut().unwrap();
|
||||||
|
|
||||||
|
for (line_idx, line) in lines {
|
||||||
|
// If there are multiple buffers for a line then merge them into a single buffer
|
||||||
|
if line.len() == 1 {
|
||||||
|
for (horizontal_offset, (_, _pad, buffer)) in line {
|
||||||
|
gst::trace!(
|
||||||
|
CAT,
|
||||||
|
imp = self,
|
||||||
|
"Outputting ST2038 packet at {line_idx}x{horizontal_offset}"
|
||||||
|
);
|
||||||
|
buffers_ref.add(buffer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gst::trace!(
|
||||||
|
CAT,
|
||||||
|
imp = self,
|
||||||
|
"Outputting multiple ST2038 packets at line {line_idx}"
|
||||||
|
);
|
||||||
|
let mut new_buffer = gst::Buffer::new();
|
||||||
|
for (horizontal_offset, (_, _pad, buffer)) in line {
|
||||||
|
gst::trace!(CAT, imp = self, "Horizontal offset {horizontal_offset}");
|
||||||
|
// Copy over metadata of the first buffer for this line
|
||||||
|
if new_buffer.size() == 0 {
|
||||||
|
let new_buffer_ref = new_buffer.get_mut().unwrap();
|
||||||
|
let _ = buffer.copy_into(new_buffer_ref, gst::BUFFER_COPY_METADATA, ..);
|
||||||
|
}
|
||||||
|
new_buffer.append(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gst::trace!(CAT, imp = self, "Outputting {} buffers", buffers_ref.len());
|
||||||
|
|
||||||
|
// Unset marker flag on all buffers, and set PTS/duration if there is a downstream
|
||||||
|
// framerate. Otherwise we leave them as-is.
|
||||||
|
if duration > gst::ClockTime::ZERO {
|
||||||
|
buffers_ref.foreach_mut(|mut buffer, _idx| {
|
||||||
|
let buffer_ref = buffer.make_mut();
|
||||||
|
buffer_ref.set_pts(start_running_time);
|
||||||
|
buffer_ref.set_duration(duration);
|
||||||
|
buffer_ref.unset_flags(gst::BufferFlags::MARKER);
|
||||||
|
|
||||||
|
ControlFlow::Continue(Some(buffer))
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
buffers_ref.foreach_mut(|mut buffer, _idx| {
|
||||||
|
if buffer.flags().contains(gst::BufferFlags::MARKER) {
|
||||||
|
let buffer_ref = buffer.make_mut();
|
||||||
|
buffer_ref.unset_flags(gst::BufferFlags::MARKER);
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlFlow::Continue(Some(buffer))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set marker flag on last buffer
|
||||||
|
{
|
||||||
|
let last = buffers_ref.get_mut(buffers_ref.len() - 1).unwrap();
|
||||||
|
last.set_flags(gst::BufferFlags::MARKER);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.finish_buffer_list(buffers)
|
||||||
|
} else {
|
||||||
|
let mut duration = duration;
|
||||||
|
|
||||||
|
if let Some(min_next_buffer_running_time) = min_next_buffer_running_time {
|
||||||
|
gst::trace!(
|
||||||
|
CAT,
|
||||||
|
imp = self,
|
||||||
|
"Next buffer at {min_next_buffer_running_time}"
|
||||||
|
);
|
||||||
|
if duration == gst::ClockTime::ZERO {
|
||||||
|
duration = min_next_buffer_running_time - start_running_time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gst::trace!(
|
||||||
|
CAT,
|
||||||
|
imp = self,
|
||||||
|
"Outputting gap event at {start_running_time} with duration {duration}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Nothing to be output for this frame
|
||||||
|
|
||||||
|
#[cfg(feature = "v1_26")]
|
||||||
|
{
|
||||||
|
self.obj().push_src_event(
|
||||||
|
gst::event::Gap::builder(start_running_time)
|
||||||
|
.duration(duration)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "v1_26"))]
|
||||||
|
{
|
||||||
|
self.obj().src_pad().push_event(
|
||||||
|
gst::event::Gap::builder(start_running_time)
|
||||||
|
.duration(duration)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(gst::FlowSuccess::Ok)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Advance position to the next frame if there is a downstream framerate, or otherwise
|
||||||
|
// to the start of the next buffer.
|
||||||
|
if duration > gst::ClockTime::ZERO {
|
||||||
|
self.obj().set_position(end_running_time);
|
||||||
|
} else if let Some(min_next_buffer_running_time) = min_next_buffer_running_time {
|
||||||
|
self.obj().set_position(min_next_buffer_running_time);
|
||||||
|
} else {
|
||||||
|
self.obj()
|
||||||
|
.set_position(src_segment.position().opt_add(40.mseconds()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek_next_sample(&self, pad: &gst_base::AggregatorPad) -> Option<gst::Sample> {
|
||||||
|
let pad = pad.downcast_ref::<super::St2038AncMuxSinkPad>().unwrap();
|
||||||
|
|
||||||
|
let pad_state = pad.imp().pad_state.lock().unwrap();
|
||||||
|
let caps = pad.current_caps()?;
|
||||||
|
if pad_state.queued_buffers.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(
|
||||||
|
gst::Sample::builder()
|
||||||
|
.buffer_list(
|
||||||
|
&pad_state
|
||||||
|
.queued_buffers
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.collect::<gst::BufferList>(),
|
||||||
|
)
|
||||||
|
.segment(&pad.segment())
|
||||||
|
.caps(&caps)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_time(&self) -> Option<gst::ClockTime> {
|
||||||
|
self.obj().simple_get_next_time()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&self) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
*state = State::default();
|
||||||
|
|
||||||
|
self.obj()
|
||||||
|
.src_pad()
|
||||||
|
.segment()
|
||||||
|
.set_position(None::<gst::ClockTime>);
|
||||||
|
|
||||||
|
Ok(gst::FlowSuccess::Ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn negotiate(&self) -> bool {
|
||||||
|
let templ_caps = self.obj().src_pad().pad_template_caps();
|
||||||
|
let mut peer_caps = self.obj().src_pad().peer_query_caps(Some(&templ_caps));
|
||||||
|
gst::debug!(CAT, imp = self, "Downstream caps {peer_caps:?}");
|
||||||
|
|
||||||
|
if peer_caps.is_empty() {
|
||||||
|
gst::warning!(CAT, imp = self, "Downstream returned EMPTY caps");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
peer_caps.fixate();
|
||||||
|
let framerate = peer_caps
|
||||||
|
.structure(0)
|
||||||
|
.unwrap()
|
||||||
|
.get::<gst::Fraction>("framerate")
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
if let Some(framerate) = framerate {
|
||||||
|
gst::debug!(
|
||||||
|
CAT,
|
||||||
|
imp = self,
|
||||||
|
"Configuring downstream requested framerate {framerate}"
|
||||||
|
);
|
||||||
|
state.downstream_framerate = Some(framerate);
|
||||||
|
drop(state);
|
||||||
|
|
||||||
|
let duration = gst::ClockTime::SECOND
|
||||||
|
.nseconds()
|
||||||
|
.mul_div_round(framerate.denom() as u64, framerate.numer() as u64)
|
||||||
|
.unwrap()
|
||||||
|
.nseconds();
|
||||||
|
|
||||||
|
self.obj().set_latency(duration, duration);
|
||||||
|
} else {
|
||||||
|
gst::debug!(CAT, imp = self, "Downstream requested no framerate");
|
||||||
|
state.downstream_framerate = None;
|
||||||
|
drop(state);
|
||||||
|
|
||||||
|
// Assume 25fps as a worst case
|
||||||
|
self.obj().set_latency(40.mseconds(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.obj().set_src_caps(&peer_caps);
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sink_event(&self, aggregator_pad: &gst_base::AggregatorPad, event: gst::Event) -> bool {
|
||||||
|
#[allow(clippy::single_match)]
|
||||||
|
match event.view() {
|
||||||
|
gst::EventView::Segment(ev) => {
|
||||||
|
let segment = ev.segment();
|
||||||
|
if segment.format() != gst::Format::Time {
|
||||||
|
gst::error!(CAT, imp = self, "Non-TIME segments not supported");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.parent_sink_event(aggregator_pad, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clip(
|
||||||
|
&self,
|
||||||
|
aggregator_pad: &gst_base::AggregatorPad,
|
||||||
|
buffer: gst::Buffer,
|
||||||
|
) -> Option<gst::Buffer> {
|
||||||
|
let Some(pts) = buffer.pts() else {
|
||||||
|
return Some(buffer);
|
||||||
|
};
|
||||||
|
let segment = aggregator_pad.segment();
|
||||||
|
segment
|
||||||
|
.downcast_ref::<gst::ClockTime>()
|
||||||
|
.map(|segment| segment.clip(pts, pts))
|
||||||
|
.map(|_| buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(&self) -> Result<(), gst::ErrorMessage> {
|
||||||
|
gst::trace!(CAT, imp = self, "Starting");
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
*state = State::default();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop(&self) -> Result<(), gst::ErrorMessage> {
|
||||||
|
gst::trace!(CAT, imp = self, "Stopping");
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
*state = State::default();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ElementImpl for St2038AncMux {
|
||||||
|
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||||
|
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||||
|
gst::subclass::ElementMetadata::new(
|
||||||
|
"ST2038 Anc Mux",
|
||||||
|
"Muxer",
|
||||||
|
"Combines multiple ST2038 Anc streams",
|
||||||
|
"Sebastian Dröge <sebastian@centricular.com>",
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(&*ELEMENT_METADATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||||
|
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||||
|
let caps = gst::Caps::new_empty_simple("meta/x-st-2038");
|
||||||
|
let src_pad_template = gst::PadTemplate::builder(
|
||||||
|
"src",
|
||||||
|
gst::PadDirection::Src,
|
||||||
|
gst::PadPresence::Always,
|
||||||
|
&caps,
|
||||||
|
)
|
||||||
|
.gtype(gst_base::AggregatorPad::static_type())
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let sink_pad_template = gst::PadTemplate::builder(
|
||||||
|
"sink_%u",
|
||||||
|
gst::PadDirection::Sink,
|
||||||
|
gst::PadPresence::Request,
|
||||||
|
&caps,
|
||||||
|
)
|
||||||
|
.gtype(super::St2038AncMuxSinkPad::static_type())
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
vec![src_pad_template, sink_pad_template]
|
||||||
|
});
|
||||||
|
|
||||||
|
PAD_TEMPLATES.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GstObjectImpl for St2038AncMux {}
|
||||||
|
|
||||||
|
impl ObjectImpl for St2038AncMux {}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for St2038AncMux {
|
||||||
|
const NAME: &'static str = "GstSt2038AncMux";
|
||||||
|
type Type = super::St2038AncMux;
|
||||||
|
type ParentType = gst_base::Aggregator;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct PadState {
|
||||||
|
queued_buffers: Vec<gst::Buffer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct St2038AncMuxSinkPad {
|
||||||
|
pad_state: Mutex<PadState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl St2038AncMuxSinkPad {}
|
||||||
|
|
||||||
|
impl AggregatorPadImpl for St2038AncMuxSinkPad {
|
||||||
|
fn flush(
|
||||||
|
&self,
|
||||||
|
_aggregator: &gst_base::Aggregator,
|
||||||
|
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||||
|
let mut state = self.pad_state.lock().unwrap();
|
||||||
|
state.queued_buffers.clear();
|
||||||
|
Ok(gst::FlowSuccess::Ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PadImpl for St2038AncMuxSinkPad {}
|
||||||
|
|
||||||
|
impl GstObjectImpl for St2038AncMuxSinkPad {}
|
||||||
|
|
||||||
|
impl ObjectImpl for St2038AncMuxSinkPad {}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for St2038AncMuxSinkPad {
|
||||||
|
const NAME: &'static str = "GstSt2038AncMuxSinkPad";
|
||||||
|
type Type = super::St2038AncMuxSinkPad;
|
||||||
|
type ParentType = gst_base::AggregatorPad;
|
||||||
|
}
|
32
video/closedcaption/src/st2038ancmux/mod.rs
Normal file
32
video/closedcaption/src/st2038ancmux/mod.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright (C) 2024 Sebastian Dröge <sebastian@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 gst::glib;
|
||||||
|
use gst::prelude::*;
|
||||||
|
|
||||||
|
mod imp;
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct St2038AncMux(ObjectSubclass<imp::St2038AncMux>) @extends gst_base::Aggregator, gst::Element, gst::Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct St2038AncMuxSinkPad(ObjectSubclass<imp::St2038AncMuxSinkPad>) @extends gst_base::AggregatorPad, gst::Pad, gst::Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||||
|
#[cfg(feature = "doc")]
|
||||||
|
St2038AncMuxSinkPad::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
|
||||||
|
|
||||||
|
gst::Element::register(
|
||||||
|
Some(plugin),
|
||||||
|
"st2038ancmux",
|
||||||
|
gst::Rank::NONE,
|
||||||
|
St2038AncMux::static_type(),
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in a new issue