1 /*
2 * GStreamer
3 * Copyright (C) 2018 Edward Hervey <edward@centricular.com>
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Library General Public
7 * License as published by the Free Software Foundation; either
8 * version 2 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Library General Public License for more details.
14 *
15 * You should have received a copy of the GNU Library General Public
16 * License along with this library; if not, write to the
17 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
18 * Boston, MA 02110-1301, USA.
19 */
20
21 /**
22 * SECTION:element-ccextractor
23 * @title: ccextractor
24 * @short_description: Extract GstVideoCaptionMeta from input stream
25 *
26 * Note: This element must be added after a pipeline's decoder, otherwise closed captions may
27 * be extracted out of order.
28 *
29 */
30
31 #ifdef HAVE_CONFIG_H
32 # include <config.h>
33 #endif
34
35 #include <gst/gst.h>
36 #include <gst/video/video.h>
37 #include <string.h>
38
39 #include "gstccextractor.h"
40
41 GST_DEBUG_CATEGORY_STATIC (gst_cc_extractor_debug);
42 #define GST_CAT_DEFAULT gst_cc_extractor_debug
43
44 enum
45 {
46 PROP_0,
47 PROP_REMOVE_CAPTION_META,
48 };
49
50 static GstStaticPadTemplate sinktemplate = GST_STATIC_PAD_TEMPLATE ("sink",
51 GST_PAD_SINK,
52 GST_PAD_ALWAYS,
53 GST_STATIC_CAPS_ANY);
54
55 static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src",
56 GST_PAD_SRC,
57 GST_PAD_ALWAYS,
58 GST_STATIC_CAPS_ANY);
59
60 static GstStaticPadTemplate captiontemplate =
61 GST_STATIC_PAD_TEMPLATE ("caption",
62 GST_PAD_SRC,
63 GST_PAD_SOMETIMES,
64 GST_STATIC_CAPS
65 ("closedcaption/x-cea-608,format={ (string) raw, (string) s334-1a}; "
66 "closedcaption/x-cea-708,format={ (string) cc_data, (string) cdp }"));
67
68 #define parent_class gst_cc_extractor_parent_class
69 G_DEFINE_TYPE (GstCCExtractor, gst_cc_extractor, GST_TYPE_ELEMENT);
70 GST_ELEMENT_REGISTER_DEFINE (ccextractor, "ccextractor",
71 GST_RANK_NONE, GST_TYPE_CCEXTRACTOR);
72
73 static gboolean gst_cc_extractor_sink_event (GstPad * pad, GstObject * parent,
74 GstEvent * event);
75 static gboolean gst_cc_extractor_sink_query (GstPad * pad, GstObject * parent,
76 GstQuery * query);
77 static GstFlowReturn gst_cc_extractor_chain (GstPad * pad, GstObject * parent,
78 GstBuffer * buf);
79 static GstStateChangeReturn gst_cc_extractor_change_state (GstElement *
80 element, GstStateChange transition);
81
82 static void gst_cc_extractor_finalize (GObject * self);
83 static void gst_cc_extractor_set_property (GObject * self, guint prop_id,
84 const GValue * value, GParamSpec * pspec);
85 static void gst_cc_extractor_get_property (GObject * self, guint prop_id,
86 GValue * value, GParamSpec * pspec);
87
88 static void
gst_cc_extractor_class_init(GstCCExtractorClass * klass)89 gst_cc_extractor_class_init (GstCCExtractorClass * klass)
90 {
91 GObjectClass *gobject_class;
92 GstElementClass *gstelement_class;
93
94 gobject_class = (GObjectClass *) klass;
95 gstelement_class = (GstElementClass *) klass;
96
97 gobject_class->finalize = gst_cc_extractor_finalize;
98 gobject_class->set_property = gst_cc_extractor_set_property;
99 gobject_class->get_property = gst_cc_extractor_get_property;
100
101 /**
102 * GstCCExtractor:remove-caption-meta
103 *
104 * Selects whether the #GstVideoCaptionMeta should be removed from the
105 * outgoing video buffers or whether it should be kept.
106 *
107 * Since: 1.18
108 */
109 g_object_class_install_property (G_OBJECT_CLASS (klass),
110 PROP_REMOVE_CAPTION_META, g_param_spec_boolean ("remove-caption-meta",
111 "Remove Caption Meta",
112 "Remove caption meta from outgoing video buffers", FALSE,
113 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
114
115 gstelement_class->change_state =
116 GST_DEBUG_FUNCPTR (gst_cc_extractor_change_state);
117
118 gst_element_class_set_static_metadata (gstelement_class,
119 "Closed Caption Extractor",
120 "Filter",
121 "Extract GstVideoCaptionMeta from input stream",
122 "Edward Hervey <edward@centricular.com>");
123
124 gst_element_class_add_static_pad_template (gstelement_class, &sinktemplate);
125 gst_element_class_add_static_pad_template (gstelement_class, &srctemplate);
126 gst_element_class_add_static_pad_template (gstelement_class,
127 &captiontemplate);
128
129 GST_DEBUG_CATEGORY_INIT (gst_cc_extractor_debug, "ccextractor",
130 0, "Closed Caption extractor");
131 }
132
133 static GstIterator *
gst_cc_extractor_iterate_internal_links(GstPad * pad,GstObject * parent)134 gst_cc_extractor_iterate_internal_links (GstPad * pad, GstObject * parent)
135 {
136 GstCCExtractor *filter = (GstCCExtractor *) parent;
137 GstIterator *it = NULL;
138 GstPad *opad = NULL;
139
140 if (pad == filter->sinkpad)
141 opad = filter->srcpad;
142 else if (pad == filter->srcpad || pad == filter->captionpad)
143 opad = filter->sinkpad;
144
145 if (opad) {
146 GValue value = { 0, };
147
148 g_value_init (&value, GST_TYPE_PAD);
149 g_value_set_object (&value, opad);
150 it = gst_iterator_new_single (GST_TYPE_PAD, &value);
151 g_value_unset (&value);
152 }
153
154 return it;
155 }
156
157 static void
gst_cc_extractor_reset(GstCCExtractor * filter)158 gst_cc_extractor_reset (GstCCExtractor * filter)
159 {
160 filter->caption_type = GST_VIDEO_CAPTION_TYPE_UNKNOWN;
161 gst_flow_combiner_reset (filter->combiner);
162 gst_flow_combiner_add_pad (filter->combiner, filter->srcpad);
163
164 if (filter->captionpad) {
165 gst_flow_combiner_remove_pad (filter->combiner, filter->captionpad);
166 gst_pad_set_active (filter->captionpad, FALSE);
167 gst_element_remove_pad ((GstElement *) filter, filter->captionpad);
168 filter->captionpad = NULL;
169 }
170
171 memset (&filter->video_info, 0, sizeof (filter->video_info));
172 }
173
174 static void
gst_cc_extractor_init(GstCCExtractor * filter)175 gst_cc_extractor_init (GstCCExtractor * filter)
176 {
177 filter->sinkpad = gst_pad_new_from_static_template (&sinktemplate, "sink");
178 gst_pad_set_event_function (filter->sinkpad,
179 GST_DEBUG_FUNCPTR (gst_cc_extractor_sink_event));
180 gst_pad_set_query_function (filter->sinkpad,
181 GST_DEBUG_FUNCPTR (gst_cc_extractor_sink_query));
182 gst_pad_set_chain_function (filter->sinkpad,
183 GST_DEBUG_FUNCPTR (gst_cc_extractor_chain));
184 gst_pad_set_iterate_internal_links_function (filter->sinkpad,
185 GST_DEBUG_FUNCPTR (gst_cc_extractor_iterate_internal_links));
186 GST_PAD_SET_PROXY_CAPS (filter->sinkpad);
187 GST_PAD_SET_PROXY_ALLOCATION (filter->sinkpad);
188 GST_PAD_SET_PROXY_SCHEDULING (filter->sinkpad);
189
190 filter->srcpad = gst_pad_new_from_static_template (&srctemplate, "src");
191 gst_pad_set_iterate_internal_links_function (filter->srcpad,
192 GST_DEBUG_FUNCPTR (gst_cc_extractor_iterate_internal_links));
193 GST_PAD_SET_PROXY_CAPS (filter->srcpad);
194 GST_PAD_SET_PROXY_ALLOCATION (filter->srcpad);
195 GST_PAD_SET_PROXY_SCHEDULING (filter->srcpad);
196
197 gst_element_add_pad (GST_ELEMENT (filter), filter->sinkpad);
198 gst_element_add_pad (GST_ELEMENT (filter), filter->srcpad);
199
200 filter->combiner = gst_flow_combiner_new ();
201
202 gst_cc_extractor_reset (filter);
203 }
204
205 static GstEvent *
create_stream_start_event_from_stream_start_event(GstEvent * event)206 create_stream_start_event_from_stream_start_event (GstEvent * event)
207 {
208 GstEvent *new_event;
209 const gchar *stream_id;
210 gchar *new_stream_id;
211 guint group_id;
212
213 gst_event_parse_stream_start (event, &stream_id);
214 new_stream_id = g_strdup_printf ("%s/caption", stream_id);
215
216 new_event = gst_event_new_stream_start (new_stream_id);
217 g_free (new_stream_id);
218 if (gst_event_parse_group_id (event, &group_id))
219 gst_event_set_group_id (new_event, group_id);
220
221 return new_event;
222 }
223
224 static gboolean
gst_cc_extractor_sink_event(GstPad * pad,GstObject * parent,GstEvent * event)225 gst_cc_extractor_sink_event (GstPad * pad, GstObject * parent, GstEvent * event)
226 {
227 GstCCExtractor *filter = GST_CCEXTRACTOR (parent);
228
229 GST_LOG_OBJECT (pad, "received %s event: %" GST_PTR_FORMAT,
230 GST_EVENT_TYPE_NAME (event), event);
231 switch (GST_EVENT_TYPE (event)) {
232 case GST_EVENT_CAPS:{
233 GstCaps *caps;
234
235 gst_event_parse_caps (event, &caps);
236 if (!gst_video_info_from_caps (&filter->video_info, caps)) {
237 /* We require any kind of video caps here */
238 gst_event_unref (event);
239 return FALSE;
240 }
241 break;
242 }
243 case GST_EVENT_STREAM_START:
244 if (filter->captionpad) {
245 GstEvent *new_event =
246 create_stream_start_event_from_stream_start_event (event);
247 gst_pad_push_event (filter->captionpad, new_event);
248 }
249 break;
250 default:
251 /* Also forward all other events to the caption pad if present */
252 if (filter->captionpad)
253 gst_pad_push_event (filter->captionpad, gst_event_ref (event));
254 break;
255 }
256
257 /* This only forwards to the non-caption source pad */
258 return gst_pad_event_default (pad, parent, event);
259 }
260
261 static gboolean
gst_cc_extractor_sink_query(GstPad * pad,GstObject * parent,GstQuery * query)262 gst_cc_extractor_sink_query (GstPad * pad, GstObject * parent, GstQuery * query)
263 {
264 GST_LOG_OBJECT (pad, "received %s query: %" GST_PTR_FORMAT,
265 GST_QUERY_TYPE_NAME (query), query);
266 switch (GST_QUERY_TYPE (query)) {
267 case GST_QUERY_ACCEPT_CAPS:{
268 GstCaps *caps;
269 const GstStructure *s;
270
271 gst_query_parse_accept_caps (query, &caps);
272
273 /* FIXME: Ideally we would declare this in our caps but there's no way
274 * to declare caps of type "video/" and "image/" that would match all
275 * such caps
276 */
277 s = gst_caps_get_structure (caps, 0);
278 if (s && (g_str_has_prefix (gst_structure_get_name (s), "video/")
279 || g_str_has_prefix (gst_structure_get_name (s), "image/")))
280 gst_query_set_accept_caps_result (query, TRUE);
281 else
282 gst_query_set_accept_caps_result (query, FALSE);
283
284 return TRUE;
285 }
286 default:
287 break;
288 }
289
290 return gst_pad_query_default (pad, parent, query);
291 }
292
293 static GstCaps *
create_caps_from_caption_type(GstVideoCaptionType caption_type,const GstVideoInfo * video_info)294 create_caps_from_caption_type (GstVideoCaptionType caption_type,
295 const GstVideoInfo * video_info)
296 {
297 GstCaps *caption_caps = gst_video_caption_type_to_caps (caption_type);
298
299 gst_caps_set_simple (caption_caps, "framerate", GST_TYPE_FRACTION,
300 video_info->fps_n, video_info->fps_d, NULL);
301
302 return caption_caps;
303 }
304
305 static gboolean
forward_sticky_events(GstPad * pad,GstEvent ** event,gpointer user_data)306 forward_sticky_events (GstPad * pad, GstEvent ** event, gpointer user_data)
307 {
308 GstCCExtractor *filter = user_data;
309
310 switch (GST_EVENT_TYPE (*event)) {
311 case GST_EVENT_CAPS:{
312 GstCaps *caption_caps =
313 create_caps_from_caption_type (filter->caption_type,
314 &filter->video_info);
315
316 if (caption_caps) {
317 GstEvent *new_event = gst_event_new_caps (caption_caps);
318 gst_event_set_seqnum (new_event, gst_event_get_seqnum (*event));
319 gst_pad_store_sticky_event (filter->captionpad, new_event);
320 gst_event_unref (new_event);
321 gst_caps_unref (caption_caps);
322 }
323
324 break;
325 }
326 case GST_EVENT_STREAM_START:{
327 GstEvent *new_event =
328 create_stream_start_event_from_stream_start_event (*event);
329 gst_pad_store_sticky_event (filter->captionpad, new_event);
330 gst_event_unref (new_event);
331
332 break;
333 }
334 default:
335 gst_pad_store_sticky_event (filter->captionpad, *event);
336 break;
337 }
338
339 return TRUE;
340 }
341
342 static GstFlowReturn
gst_cc_extractor_handle_meta(GstCCExtractor * filter,GstBuffer * buf,GstVideoCaptionMeta * meta,GstVideoTimeCodeMeta * tc_meta)343 gst_cc_extractor_handle_meta (GstCCExtractor * filter, GstBuffer * buf,
344 GstVideoCaptionMeta * meta, GstVideoTimeCodeMeta * tc_meta)
345 {
346 GstBuffer *outbuf = NULL;
347 GstFlowReturn flow;
348
349 GST_DEBUG_OBJECT (filter, "Handling meta");
350
351 /* Check if the meta type matches the configured one */
352 if (filter->captionpad == NULL) {
353 GST_DEBUG_OBJECT (filter, "Creating new caption pad");
354
355 /* Create the caption pad and set the caps */
356 filter->captionpad =
357 gst_pad_new_from_static_template (&captiontemplate, "caption");
358 gst_pad_set_iterate_internal_links_function (filter->sinkpad,
359 GST_DEBUG_FUNCPTR (gst_cc_extractor_iterate_internal_links));
360 gst_pad_set_active (filter->captionpad, TRUE);
361
362 filter->caption_type = meta->caption_type;
363
364 gst_pad_sticky_events_foreach (filter->sinkpad, forward_sticky_events,
365 filter);
366
367 if (!gst_pad_has_current_caps (filter->captionpad)) {
368 GST_ERROR_OBJECT (filter, "Unknown/invalid caption type");
369 return GST_FLOW_NOT_NEGOTIATED;
370 }
371
372 gst_element_add_pad (GST_ELEMENT (filter), filter->captionpad);
373 gst_flow_combiner_add_pad (filter->combiner, filter->captionpad);
374 } else if (meta->caption_type != filter->caption_type) {
375 GstCaps *caption_caps =
376 create_caps_from_caption_type (meta->caption_type, &filter->video_info);
377
378 GST_DEBUG_OBJECT (filter, "Caption type changed from %d to %d",
379 filter->caption_type, meta->caption_type);
380 if (caption_caps == NULL) {
381 GST_ERROR_OBJECT (filter, "Unknown/invalid caption type");
382 return GST_FLOW_NOT_NEGOTIATED;
383 }
384
385 gst_pad_push_event (filter->captionpad, gst_event_new_caps (caption_caps));
386 gst_caps_unref (caption_caps);
387
388 filter->caption_type = meta->caption_type;
389 }
390
391 GST_DEBUG_OBJECT (filter,
392 "Creating new buffer of size %" G_GSIZE_FORMAT " bytes", meta->size);
393 /* Extract caption data into new buffer with identical buffer timestamps */
394 outbuf = gst_buffer_new_allocate (NULL, meta->size, NULL);
395 gst_buffer_fill (outbuf, 0, meta->data, meta->size);
396 GST_BUFFER_PTS (outbuf) = GST_BUFFER_PTS (buf);
397 GST_BUFFER_DTS (outbuf) = GST_BUFFER_DTS (buf);
398 GST_BUFFER_DURATION (outbuf) = GST_BUFFER_DURATION (buf);
399
400 if (tc_meta)
401 gst_buffer_add_video_time_code_meta (outbuf, &tc_meta->tc);
402
403 gst_buffer_set_flags (outbuf, gst_buffer_get_flags (buf));
404 /* We don't really care about the flow return */
405 flow = gst_pad_push (filter->captionpad, outbuf);
406
407 /* Set flow return on pad and return combined value */
408 return gst_flow_combiner_update_pad_flow (filter->combiner,
409 filter->captionpad, flow);
410 }
411
412 static gboolean
remove_caption_meta(GstBuffer * buffer,GstMeta ** meta,gpointer user_data)413 remove_caption_meta (GstBuffer * buffer, GstMeta ** meta, gpointer user_data)
414 {
415 if ((*meta)->info->api == GST_VIDEO_CAPTION_META_API_TYPE)
416 *meta = NULL;
417
418 return TRUE;
419 }
420
421 static GstFlowReturn
gst_cc_extractor_chain(GstPad * pad,GstObject * parent,GstBuffer * buf)422 gst_cc_extractor_chain (GstPad * pad, GstObject * parent, GstBuffer * buf)
423 {
424 GstCCExtractor *filter = (GstCCExtractor *) parent;
425 GstFlowReturn flow = GST_FLOW_OK;
426 GstVideoCaptionMeta *cc_meta;
427 GstVideoTimeCodeMeta *tc_meta;
428 gboolean had_cc_meta = FALSE;
429 gpointer iter = NULL;
430
431 tc_meta = gst_buffer_get_video_time_code_meta (buf);
432
433 while ((cc_meta =
434 (GstVideoCaptionMeta *) gst_buffer_iterate_meta_filtered (buf, &iter,
435 GST_VIDEO_CAPTION_META_API_TYPE)) && flow == GST_FLOW_OK) {
436 had_cc_meta = TRUE;
437 flow = gst_cc_extractor_handle_meta (filter, buf, cc_meta, tc_meta);
438 }
439
440 /* If there's an issue handling the CC, return immediately */
441 if (flow != GST_FLOW_OK) {
442 gst_buffer_unref (buf);
443 return flow;
444 }
445
446 if (filter->remove_caption_meta) {
447 buf = gst_buffer_make_writable (buf);
448 gst_buffer_foreach_meta (buf, remove_caption_meta, NULL);
449 }
450
451 if (!had_cc_meta && filter->captionpad && GST_BUFFER_PTS_IS_VALID (buf)) {
452 gst_pad_push_event (filter->captionpad,
453 gst_event_new_gap (GST_BUFFER_PTS (buf), GST_BUFFER_DURATION (buf)));
454 }
455
456 /* Push the buffer downstream and return the combined flow return */
457 return gst_flow_combiner_update_pad_flow (filter->combiner, filter->srcpad,
458 gst_pad_push (filter->srcpad, buf));
459 }
460
461 static GstStateChangeReturn
gst_cc_extractor_change_state(GstElement * element,GstStateChange transition)462 gst_cc_extractor_change_state (GstElement * element, GstStateChange transition)
463 {
464 GstStateChangeReturn ret;
465 GstCCExtractor *filter = GST_CCEXTRACTOR (element);
466
467 switch (transition) {
468 case GST_STATE_CHANGE_NULL_TO_READY:
469 break;
470 case GST_STATE_CHANGE_READY_TO_PAUSED:
471 break;
472 case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
473 break;
474 default:
475 break;
476 }
477
478 ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
479 if (ret != GST_STATE_CHANGE_SUCCESS)
480 return ret;
481
482 switch (transition) {
483 case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
484 break;
485 case GST_STATE_CHANGE_PAUSED_TO_READY:
486 gst_cc_extractor_reset (filter);
487 break;
488 case GST_STATE_CHANGE_READY_TO_NULL:
489 default:
490 break;
491 }
492
493 return ret;
494 }
495
496 static void
gst_cc_extractor_finalize(GObject * object)497 gst_cc_extractor_finalize (GObject * object)
498 {
499 GstCCExtractor *filter = GST_CCEXTRACTOR (object);
500
501 gst_flow_combiner_free (filter->combiner);
502
503 G_OBJECT_CLASS (parent_class)->finalize (object);
504 }
505
506 static void
gst_cc_extractor_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)507 gst_cc_extractor_set_property (GObject * object, guint prop_id,
508 const GValue * value, GParamSpec * pspec)
509 {
510 GstCCExtractor *filter = GST_CCEXTRACTOR (object);
511
512 switch (prop_id) {
513 case PROP_REMOVE_CAPTION_META:
514 filter->remove_caption_meta = g_value_get_boolean (value);
515 break;
516 default:
517 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
518 break;
519 }
520 }
521
522 static void
gst_cc_extractor_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)523 gst_cc_extractor_get_property (GObject * object, guint prop_id, GValue * value,
524 GParamSpec * pspec)
525 {
526 GstCCExtractor *filter = GST_CCEXTRACTOR (object);
527
528 switch (prop_id) {
529 case PROP_REMOVE_CAPTION_META:
530 g_value_set_boolean (value, filter->remove_caption_meta);
531 break;
532 default:
533 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
534 break;
535 }
536 }
537