gstreamer/subprojects/gst-plugins-bad/ext/closedcaption/gstcea608mux.c
Matthew Waters a26b363d3e closedcaption: produce valid cea608 padding by default
Cea608 (valid) padding removal is available on the input side of ccconverter
or configurable on cccombiner.  cccombiner can now configure whether
valid or invalid cea608 padding is used and for valid padding, how long
after valid non-padding to keep sending valid padding.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/6300>
2024-03-12 02:49:01 +00:00

446 lines
14 KiB
C

/*
* GStreamer
* Copyright (C) 2023 Mathieu Duponchelle <mathieu@centricular.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
/**
* SECTION:element-cea608mux
* @title: cea608mux
* @short_description: Combine CC1 and CC3 raw 608 streams
*
* ```
* gst-launch-1.0 cea608mux name=mux ! fakesink dump=true \
* filesrc location=one.scc ! sccparse ! closedcaption/x-cea-608 ! ccconverter ! mux. \
* filesrc location=two.scc ! sccparse ! ccconverter ! closedcaption/x-cea-608, format=raw, field=0 ! \
* capssetter caps="closedcaption/x-cea-608, format=raw, field=1" ! mux.
* ```
*
* Since: 1.24
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <gst/gst.h>
#include <gst/base/base.h>
#include <gst/video/video.h>
#include <string.h>
#include "ccutils.h"
#include "gstcea608mux.h"
GST_DEBUG_CATEGORY_STATIC (gst_cea608_mux_debug);
#define GST_CAT_DEFAULT gst_cea608_mux_debug
static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src",
GST_PAD_SRC,
GST_PAD_ALWAYS,
GST_STATIC_CAPS ("closedcaption/x-cea-608, format=s334-1a, "
"framerate=(fraction){60/1, 60000/1001, 50/1, 30/1, 30000/1001, 25/1, 24/1, 24000/1001}"));
static GstStaticPadTemplate cc1_template = GST_STATIC_PAD_TEMPLATE ("cc1",
GST_PAD_SINK,
GST_PAD_REQUEST,
GST_STATIC_CAPS ("closedcaption/x-cea-608,format=raw,field=0"));
static GstStaticPadTemplate cc3_template = GST_STATIC_PAD_TEMPLATE ("cc3",
GST_PAD_SINK,
GST_PAD_REQUEST,
GST_STATIC_CAPS ("closedcaption/x-cea-608,format=raw,field=1"));
#define parent_class gst_cea608_mux_parent_class
G_DEFINE_TYPE (GstCea608Mux, gst_cea608_mux, GST_TYPE_AGGREGATOR);
GST_ELEMENT_REGISTER_DEFINE (cea608mux, "cea608mux",
GST_RANK_NONE, GST_TYPE_CEA608MUX);
enum
{
PROP_0,
};
static void
gst_cea608_mux_finalize (GObject * object)
{
GstCea608Mux *self = GST_CEA608MUX (object);
gst_clear_object (&self->cc_buffer);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
#define GST_FLOW_NEED_DATA GST_FLOW_CUSTOM_SUCCESS
static GstAggregatorPad *
find_best_pad (GstAggregator * aggregator, GstClockTime * ts, gboolean timeout)
{
GstAggregatorPad *best = NULL;
GstClockTime best_ts = GST_CLOCK_TIME_NONE;
GstIterator *pads;
GValue padptr = { 0, };
gboolean done = FALSE;
pads = gst_element_iterate_sink_pads (GST_ELEMENT (aggregator));
while (!done) {
switch (gst_iterator_next (pads, &padptr)) {
case GST_ITERATOR_OK:{
GstAggregatorPad *apad = g_value_get_object (&padptr);
GstClockTime t = GST_CLOCK_TIME_NONE;
GstBuffer *buffer;
buffer = gst_aggregator_pad_peek_buffer (apad);
if (!buffer) {
if (!timeout && !GST_PAD_IS_EOS (apad)) {
gst_object_replace ((GstObject **) & best, NULL);
best_ts = GST_CLOCK_TIME_NONE;
done = TRUE;
}
break;
}
if (GST_CLOCK_TIME_IS_VALID (GST_BUFFER_DTS_OR_PTS (buffer))) {
t = gst_segment_to_running_time (&apad->segment, GST_FORMAT_TIME,
GST_BUFFER_PTS (buffer));
}
if (!GST_CLOCK_TIME_IS_VALID (best_ts) ||
(GST_CLOCK_TIME_IS_VALID (t) && t < best_ts)) {
gst_object_replace ((GstObject **) & best, GST_OBJECT (apad));
best_ts = t;
}
gst_buffer_unref (buffer);
break;
}
case GST_ITERATOR_DONE:
done = TRUE;
break;
case GST_ITERATOR_RESYNC:
gst_iterator_resync (pads);
/* Clear the best pad and start again. It might have disappeared */
gst_object_replace ((GstObject **) & best, NULL);
best_ts = GST_CLOCK_TIME_NONE;
break;
case GST_ITERATOR_ERROR:
/* This can't happen if the parameters to gst_iterator_next() are valid */
g_assert_not_reached ();
break;
}
g_value_reset (&padptr);
}
g_value_unset (&padptr);
gst_iterator_free (pads);
if (best) {
GST_LOG_OBJECT (aggregator,
"Best pad found with TS %" GST_TIME_FORMAT ": %" GST_PTR_FORMAT,
GST_TIME_ARGS (best_ts), best);
} else {
GST_LOG_OBJECT (aggregator, "Best pad not found");
}
if (ts && GST_CLOCK_TIME_IS_VALID (best_ts))
*ts = best_ts;
return best;
}
static gboolean
all_pads_eos (GstAggregator * agg)
{
GList *l;
gboolean ret = TRUE;
GST_OBJECT_LOCK (agg);
for (l = GST_ELEMENT_CAST (agg)->sinkpads; l; l = l->next) {
GstAggregatorPad *pad = GST_AGGREGATOR_PAD (l->data);
if (!gst_aggregator_pad_is_eos (pad)) {
ret = FALSE;
break;
}
}
GST_OBJECT_UNLOCK (agg);
return ret;
}
static void
take_s334_both_fields (GstCea608Mux * self, GstBuffer * buffer)
{
GstMapInfo out = GST_MAP_INFO_INIT;
guint s334_len, cc_data_len, i;
gst_buffer_map (buffer, &out, GST_MAP_READWRITE);
cc_data_len = out.size;
cc_buffer_take_cc_data (self->cc_buffer, self->cdp_fps_entry, out.data,
&cc_data_len);
s334_len = drop_ccp_from_cc_data (out.data, cc_data_len);
if (s334_len < 0) {
s334_len = 0;
goto out;
}
for (i = 0; i < s334_len / 3; i++) {
guint byte = out.data[i * 3];
/* We have to assume a line offset of 0 */
out.data[i * 3] = (byte == 0xfc || byte == 0xf8) ? 0x80 : 0x00;
}
out:
gst_buffer_unmap (buffer, &out);
gst_buffer_set_size (buffer, s334_len);
}
static GstFlowReturn
finish_s334_both_fields (GstCea608Mux * self)
{
GstClockTime output_pts = gst_util_uint64_scale_int (GST_SECOND,
self->cdp_fps_entry->fps_d * self->n_output_buffers,
self->cdp_fps_entry->fps_n);
GstClockTime output_duration =
gst_util_uint64_scale_int (GST_SECOND, self->cdp_fps_entry->fps_d,
self->cdp_fps_entry->fps_n);
GstBuffer *output = gst_buffer_new_allocate (NULL, MAX_CDP_PACKET_LEN, NULL);
GstSegment *agg_segment =
&GST_AGGREGATOR_PAD (GST_AGGREGATOR (self)->srcpad)->segment;
output_pts += self->start_time;
take_s334_both_fields (self, output);
GST_BUFFER_PTS (output) = output_pts;
GST_BUFFER_DURATION (output) = output_duration;
GST_DEBUG_OBJECT (self, "Finishing %" GST_PTR_FORMAT, output);
self->n_output_buffers += 1;
agg_segment->position = output_pts + output_duration;
return gst_aggregator_finish_buffer (GST_AGGREGATOR (self), output);
}
static GstFlowReturn
gst_cea608_mux_aggregate (GstAggregator * aggregator, gboolean timeout)
{
GstCea608Mux *self = GST_CEA608MUX (aggregator);
GstFlowReturn flow_ret = GST_FLOW_OK;
GstAggregatorPad *best_pad = NULL;
GstClockTime output_duration =
gst_util_uint64_scale_int (GST_SECOND, self->cdp_fps_entry->fps_d,
self->cdp_fps_entry->fps_n);
GstSegment *agg_segment = &GST_AGGREGATOR_PAD (aggregator->srcpad)->segment;
GstClockTime output_start_time = agg_segment->position;
GstClockTime output_end_running_time;
if (agg_segment->position == -1 || agg_segment->position < agg_segment->start)
output_start_time = agg_segment->start;
if (!GST_CLOCK_TIME_IS_VALID (self->start_time)) {
self->start_time = output_start_time;
GST_DEBUG_OBJECT (self, "Start time %" GST_TIME_FORMAT,
GST_TIME_ARGS (self->start_time));
}
best_pad =
find_best_pad (aggregator, &self->earliest_input_running_time, timeout);
output_end_running_time =
gst_segment_to_running_time (agg_segment, GST_FORMAT_TIME,
output_start_time + output_duration);
GST_LOG_OBJECT (self, "best-pad: %s, timeout: %d, "
"earliest input running time: %"
GST_TIME_FORMAT ", output running time: %" GST_TIME_FORMAT,
best_pad ? GST_OBJECT_NAME (best_pad) : "NULL", timeout,
GST_TIME_ARGS (self->earliest_input_running_time),
GST_TIME_ARGS (output_end_running_time));
if (GST_CLOCK_TIME_IS_VALID (self->earliest_input_running_time)
&& self->earliest_input_running_time > output_end_running_time) {
/* Nothing to consume, earliest pad is not ready yet */
GST_LOG_OBJECT (self, "Nothing to consume");
} else if (best_pad) {
GstBuffer *buffer;
buffer = gst_aggregator_pad_pop_buffer (GST_AGGREGATOR_PAD (best_pad));
if (buffer) {
GstMapInfo map;
gst_buffer_map (buffer, &map, GST_MAP_READ);
if (g_strcmp0 (GST_PAD_NAME (best_pad), "cc1") == 0) {
GST_DEBUG_OBJECT (self, "Consuming CC1 %" GST_PTR_FORMAT, buffer);
cc_buffer_push_separated (self->cc_buffer, map.data, map.size, NULL, 0,
NULL, 0);
} else {
GST_DEBUG_OBJECT (self, "Consuming CC3 %" GST_PTR_FORMAT, buffer);
cc_buffer_push_separated (self->cc_buffer, NULL, 0, map.data, map.size,
NULL, 0);
}
gst_buffer_unmap (buffer, &map);
gst_buffer_unref (buffer);
} else {
/* We got flushed */
flow_ret = GST_AGGREGATOR_FLOW_NEED_DATA;
}
} else if (all_pads_eos (aggregator)) {
GST_INFO_OBJECT (self, "EOS!");
flow_ret = GST_FLOW_EOS;
} else {
GST_LOG_OBJECT (self, "Need more data");
flow_ret = GST_AGGREGATOR_FLOW_NEED_DATA;
}
if (flow_ret == GST_FLOW_OK) {
if (timeout || output_end_running_time < self->earliest_input_running_time) {
flow_ret = finish_s334_both_fields (self);
}
} else if (flow_ret == GST_FLOW_EOS && !cc_buffer_is_empty (self->cc_buffer)) {
flow_ret = finish_s334_both_fields (self);
}
g_clear_pointer (&best_pad, gst_object_unref);
return flow_ret;
}
static gboolean
gst_cea608_mux_stop (GstAggregator * aggregator)
{
GstCea608Mux *self = GST_CEA608MUX (aggregator);
cc_buffer_discard (self->cc_buffer);
self->n_output_buffers = 0;
self->earliest_input_running_time = 0;
self->start_time = GST_CLOCK_TIME_NONE;
return TRUE;
}
static GstFlowReturn
gst_cea608_mux_flush (GstAggregator * aggregator)
{
GstCea608Mux *self = GST_CEA608MUX (aggregator);
GstSegment *agg_segment = &GST_AGGREGATOR_PAD (aggregator->srcpad)->segment;
GST_DEBUG_OBJECT (self, "Flush");
cc_buffer_discard (self->cc_buffer);
self->n_output_buffers = 0;
self->earliest_input_running_time = 0;
self->start_time = GST_CLOCK_TIME_NONE;
agg_segment->position = -1;
return GST_FLOW_OK;
}
static gboolean
gst_cea608_mux_negotiated_src_caps (GstAggregator * agg, GstCaps * caps)
{
GstStructure *s = gst_caps_get_structure (caps, 0);
gint fps_n, fps_d;
GstCea608Mux *self = GST_CEA608MUX (agg);
GstClockTime latency;
GST_INFO_OBJECT (agg->srcpad, "set src caps: %" GST_PTR_FORMAT, caps);
g_assert (gst_structure_get_fraction (s, "framerate", &fps_n,
&fps_d) == TRUE);
self->cdp_fps_entry = cdp_fps_entry_from_fps (fps_n, fps_d);
g_assert (self->cdp_fps_entry != NULL && self->cdp_fps_entry->fps_n != 0);
latency =
gst_util_uint64_scale (GST_SECOND, self->cdp_fps_entry->fps_d,
self->cdp_fps_entry->fps_n);
gst_aggregator_set_latency (agg, latency, latency);
return TRUE;
}
static GstBuffer *
gst_cea608_mux_clip (GstAggregator * aggregator, GstAggregatorPad * pad,
GstBuffer * buffer)
{
GstClockTime time;
if (!GST_BUFFER_PTS_IS_VALID (buffer))
return buffer;
time = gst_segment_to_running_time (&pad->segment, GST_FORMAT_TIME,
GST_BUFFER_PTS (buffer));
if (!GST_CLOCK_TIME_IS_VALID (time)) {
GST_DEBUG_OBJECT (pad, "Dropping buffer on pad outside segment %"
GST_TIME_FORMAT, GST_TIME_ARGS (GST_BUFFER_PTS (buffer)));
gst_buffer_unref (buffer);
return NULL;
}
return buffer;
}
static void
gst_cea608_mux_class_init (GstCea608MuxClass * klass)
{
GObjectClass *gobject_class;
GstElementClass *gstelement_class;
GstAggregatorClass *aggregator_class;
gobject_class = (GObjectClass *) klass;
gstelement_class = (GstElementClass *) klass;
aggregator_class = (GstAggregatorClass *) klass;
gobject_class->finalize = gst_cea608_mux_finalize;
gst_element_class_set_static_metadata (gstelement_class,
"Closed Caption Muxer",
"Aggregator",
"Combines raw 608 streams",
"Mathieu Duponchelle <mathieu@centricular.com>");
gst_element_class_add_static_pad_template_with_gtype (gstelement_class,
&srctemplate, GST_TYPE_AGGREGATOR_PAD);
gst_element_class_add_static_pad_template_with_gtype (gstelement_class,
&cc1_template, GST_TYPE_AGGREGATOR_PAD);
gst_element_class_add_static_pad_template_with_gtype (gstelement_class,
&cc3_template, GST_TYPE_AGGREGATOR_PAD);
aggregator_class->aggregate = gst_cea608_mux_aggregate;
aggregator_class->stop = gst_cea608_mux_stop;
aggregator_class->flush = gst_cea608_mux_flush;
aggregator_class->negotiated_src_caps = gst_cea608_mux_negotiated_src_caps;
aggregator_class->get_next_time = gst_aggregator_simple_get_next_time;
aggregator_class->clip = gst_cea608_mux_clip;
GST_DEBUG_CATEGORY_INIT (gst_cea608_mux_debug, "cea608mux",
0, "Closed Caption muxer");
}
static void
gst_cea608_mux_init (GstCea608Mux * self)
{
self->cc_buffer = cc_buffer_new ();
cc_buffer_set_max_buffer_time (self->cc_buffer, GST_CLOCK_TIME_NONE);
cc_buffer_set_output_padding (self->cc_buffer, TRUE, FALSE);
cc_buffer_set_cea608_padding_strategy (self->cc_buffer,
CC_BUFFER_CEA608_PADDING_STRATEGY_VALID |
CC_BUFFER_CEA608_PADDING_STRATEGY_INPUT_REMOVE);
self->cdp_fps_entry = &null_fps_entry;
self->start_time = GST_CLOCK_TIME_NONE;
}