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 };
48
49 static GstStaticPadTemplate sinktemplate = GST_STATIC_PAD_TEMPLATE ("sink",
50 GST_PAD_SINK,
51 GST_PAD_ALWAYS,
52 GST_STATIC_CAPS_ANY);
53
54 static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src",
55 GST_PAD_SRC,
56 GST_PAD_ALWAYS,
57 GST_STATIC_CAPS_ANY);
58
59 static GstStaticPadTemplate captiontemplate =
60 GST_STATIC_PAD_TEMPLATE ("caption",
61 GST_PAD_SRC,
62 GST_PAD_SOMETIMES,
63 GST_STATIC_CAPS
64 ("closedcaption/x-cea-608,format={ (string) raw, (string) s334-1a}; "
65 "closedcaption/x-cea-708,format={ (string) cc_data, (string) cdp }"));
66
67 G_DEFINE_TYPE (GstCCExtractor, gst_cc_extractor, GST_TYPE_ELEMENT);
68 #define parent_class gst_cc_extractor_parent_class
69
70 static gboolean gst_cc_extractor_sink_event (GstPad * pad, GstObject * parent,
71 GstEvent * event);
72 static gboolean gst_cc_extractor_sink_query (GstPad * pad, GstObject * parent,
73 GstQuery * query);
74 static GstFlowReturn gst_cc_extractor_chain (GstPad * pad, GstObject * parent,
75 GstBuffer * buf);
76 static GstStateChangeReturn gst_cc_extractor_change_state (GstElement *
77 element, GstStateChange transition);
78 static void gst_cc_extractor_finalize (GObject * self);
79
80
81 static void
gst_cc_extractor_class_init(GstCCExtractorClass * klass)82 gst_cc_extractor_class_init (GstCCExtractorClass * klass)
83 {
84 GObjectClass *gobject_class;
85 GstElementClass *gstelement_class;
86
87 gobject_class = (GObjectClass *) klass;
88 gstelement_class = (GstElementClass *) klass;
89
90 gobject_class->finalize = gst_cc_extractor_finalize;
91
92 gstelement_class->change_state =
93 GST_DEBUG_FUNCPTR (gst_cc_extractor_change_state);
94
95 gst_element_class_set_static_metadata (gstelement_class,
96 "Closed Caption Extractor",
97 "Filter",
98 "Extract GstVideoCaptionMeta from input stream",
99 "Edward Hervey <edward@centricular.com>");
100
101 gst_element_class_add_static_pad_template (gstelement_class, &sinktemplate);
102 gst_element_class_add_static_pad_template (gstelement_class, &srctemplate);
103 gst_element_class_add_static_pad_template (gstelement_class,
104 &captiontemplate);
105
106 GST_DEBUG_CATEGORY_INIT (gst_cc_extractor_debug, "ccextractor",
107 0, "Closed Caption extractor");
108 }
109
110 static GstIterator *
gst_cc_extractor_iterate_internal_links(GstPad * pad,GstObject * parent)111 gst_cc_extractor_iterate_internal_links (GstPad * pad, GstObject * parent)
112 {
113 GstCCExtractor *filter = (GstCCExtractor *) parent;
114 GstIterator *it = NULL;
115 GstPad *opad = NULL;
116
117 if (pad == filter->sinkpad)
118 opad = filter->srcpad;
119 else if (pad == filter->srcpad || pad == filter->captionpad)
120 opad = filter->sinkpad;
121
122 if (opad) {
123 GValue value = { 0, };
124
125 g_value_init (&value, GST_TYPE_PAD);
126 g_value_set_object (&value, opad);
127 it = gst_iterator_new_single (GST_TYPE_PAD, &value);
128 g_value_unset (&value);
129 }
130
131 return it;
132 }
133
134 static void
gst_cc_extractor_reset(GstCCExtractor * filter)135 gst_cc_extractor_reset (GstCCExtractor * filter)
136 {
137 filter->caption_type = GST_VIDEO_CAPTION_TYPE_UNKNOWN;
138 gst_flow_combiner_reset (filter->combiner);
139 gst_flow_combiner_add_pad (filter->combiner, filter->srcpad);
140
141 if (filter->captionpad) {
142 gst_flow_combiner_remove_pad (filter->combiner, filter->captionpad);
143 gst_pad_set_active (filter->captionpad, FALSE);
144 gst_element_remove_pad ((GstElement *) filter, filter->captionpad);
145 filter->captionpad = NULL;
146 }
147
148 memset (&filter->video_info, 0, sizeof (filter->video_info));
149 }
150
151 static void
gst_cc_extractor_init(GstCCExtractor * filter)152 gst_cc_extractor_init (GstCCExtractor * filter)
153 {
154 filter->sinkpad = gst_pad_new_from_static_template (&sinktemplate, "sink");
155 gst_pad_set_event_function (filter->sinkpad,
156 GST_DEBUG_FUNCPTR (gst_cc_extractor_sink_event));
157 gst_pad_set_query_function (filter->sinkpad,
158 GST_DEBUG_FUNCPTR (gst_cc_extractor_sink_query));
159 gst_pad_set_chain_function (filter->sinkpad,
160 GST_DEBUG_FUNCPTR (gst_cc_extractor_chain));
161 gst_pad_set_iterate_internal_links_function (filter->sinkpad,
162 GST_DEBUG_FUNCPTR (gst_cc_extractor_iterate_internal_links));
163 GST_PAD_SET_PROXY_CAPS (filter->sinkpad);
164 GST_PAD_SET_PROXY_ALLOCATION (filter->sinkpad);
165 GST_PAD_SET_PROXY_SCHEDULING (filter->sinkpad);
166
167 filter->srcpad = gst_pad_new_from_static_template (&srctemplate, "src");
168 gst_pad_set_iterate_internal_links_function (filter->srcpad,
169 GST_DEBUG_FUNCPTR (gst_cc_extractor_iterate_internal_links));
170 GST_PAD_SET_PROXY_CAPS (filter->srcpad);
171 GST_PAD_SET_PROXY_ALLOCATION (filter->srcpad);
172 GST_PAD_SET_PROXY_SCHEDULING (filter->srcpad);
173
174 gst_element_add_pad (GST_ELEMENT (filter), filter->sinkpad);
175 gst_element_add_pad (GST_ELEMENT (filter), filter->srcpad);
176
177 filter->combiner = gst_flow_combiner_new ();
178
179 gst_cc_extractor_reset (filter);
180 }
181
182 static GstEvent *
create_stream_start_event_from_stream_start_event(GstEvent * event)183 create_stream_start_event_from_stream_start_event (GstEvent * event)
184 {
185 GstEvent *new_event;
186 const gchar *stream_id;
187 gchar *new_stream_id;
188 guint group_id;
189
190 gst_event_parse_stream_start (event, &stream_id);
191 new_stream_id = g_strdup_printf ("%s/caption", stream_id);
192
193 new_event = gst_event_new_stream_start (new_stream_id);
194 g_free (new_stream_id);
195 if (gst_event_parse_group_id (event, &group_id))
196 gst_event_set_group_id (new_event, group_id);
197
198 return new_event;
199 }
200
201 static gboolean
gst_cc_extractor_sink_event(GstPad * pad,GstObject * parent,GstEvent * event)202 gst_cc_extractor_sink_event (GstPad * pad, GstObject * parent, GstEvent * event)
203 {
204 GstCCExtractor *filter = GST_CCEXTRACTOR (parent);
205
206 GST_LOG_OBJECT (pad, "received %s event: %" GST_PTR_FORMAT,
207 GST_EVENT_TYPE_NAME (event), event);
208 switch (GST_EVENT_TYPE (event)) {
209 case GST_EVENT_CAPS:{
210 GstCaps *caps;
211
212 gst_event_parse_caps (event, &caps);
213 if (!gst_video_info_from_caps (&filter->video_info, caps)) {
214 /* We require any kind of video caps here */
215 gst_event_unref (event);
216 return FALSE;
217 }
218 break;
219 }
220 case GST_EVENT_STREAM_START:
221 if (filter->captionpad) {
222 GstEvent *new_event =
223 create_stream_start_event_from_stream_start_event (event);
224 gst_pad_push_event (filter->captionpad, new_event);
225 }
226 break;
227 default:
228 /* Also forward all other events to the caption pad if present */
229 if (filter->captionpad)
230 gst_pad_push_event (filter->captionpad, gst_event_ref (event));
231 break;
232 }
233
234 /* This only forwards to the non-caption source pad */
235 return gst_pad_event_default (pad, parent, event);
236 }
237
238 static gboolean
gst_cc_extractor_sink_query(GstPad * pad,GstObject * parent,GstQuery * query)239 gst_cc_extractor_sink_query (GstPad * pad, GstObject * parent, GstQuery * query)
240 {
241 GST_LOG_OBJECT (pad, "received %s query: %" GST_PTR_FORMAT,
242 GST_QUERY_TYPE_NAME (query), query);
243 switch (GST_QUERY_TYPE (query)) {
244 case GST_QUERY_ACCEPT_CAPS:{
245 GstCaps *caps;
246 const GstStructure *s;
247
248 gst_query_parse_accept_caps (query, &caps);
249
250 /* FIXME: Ideally we would declare this in our caps but there's no way
251 * to declare caps of type "video/" and "image/" that would match all
252 * such caps
253 */
254 s = gst_caps_get_structure (caps, 0);
255 if (s && (g_str_has_prefix (gst_structure_get_name (s), "video/")
256 || g_str_has_prefix (gst_structure_get_name (s), "image/")))
257 gst_query_set_accept_caps_result (query, TRUE);
258 else
259 gst_query_set_accept_caps_result (query, FALSE);
260
261 return TRUE;
262 }
263 default:
264 break;
265 }
266
267 return gst_pad_query_default (pad, parent, query);
268 }
269
270 static GstCaps *
create_caps_from_caption_type(GstVideoCaptionType caption_type,const GstVideoInfo * video_info)271 create_caps_from_caption_type (GstVideoCaptionType caption_type,
272 const GstVideoInfo * video_info)
273 {
274 GstCaps *caption_caps = gst_video_caption_type_to_caps (caption_type);
275
276 gst_caps_set_simple (caption_caps, "framerate", GST_TYPE_FRACTION,
277 video_info->fps_n, video_info->fps_d, NULL);
278
279 return caption_caps;
280 }
281
282 static gboolean
forward_sticky_events(GstPad * pad,GstEvent ** event,gpointer user_data)283 forward_sticky_events (GstPad * pad, GstEvent ** event, gpointer user_data)
284 {
285 GstCCExtractor *filter = user_data;
286
287 switch (GST_EVENT_TYPE (*event)) {
288 case GST_EVENT_CAPS:{
289 GstCaps *caption_caps =
290 create_caps_from_caption_type (filter->caption_type,
291 &filter->video_info);
292
293 if (caption_caps) {
294 GstEvent *new_event = gst_event_new_caps (caption_caps);
295 gst_event_set_seqnum (new_event, gst_event_get_seqnum (*event));
296 gst_pad_store_sticky_event (filter->captionpad, new_event);
297 gst_event_unref (new_event);
298 gst_caps_unref (caption_caps);
299 }
300
301 break;
302 }
303 case GST_EVENT_STREAM_START:{
304 GstEvent *new_event =
305 create_stream_start_event_from_stream_start_event (*event);
306 gst_pad_store_sticky_event (filter->captionpad, new_event);
307 gst_event_unref (new_event);
308
309 break;
310 }
311 default:
312 gst_pad_store_sticky_event (filter->captionpad, *event);
313 break;
314 }
315
316 return TRUE;
317 }
318
319 static GstFlowReturn
gst_cc_extractor_handle_meta(GstCCExtractor * filter,GstBuffer * buf,GstVideoCaptionMeta * meta,GstVideoTimeCodeMeta * tc_meta)320 gst_cc_extractor_handle_meta (GstCCExtractor * filter, GstBuffer * buf,
321 GstVideoCaptionMeta * meta, GstVideoTimeCodeMeta * tc_meta)
322 {
323 GstBuffer *outbuf = NULL;
324 GstFlowReturn flow;
325
326 GST_DEBUG_OBJECT (filter, "Handling meta");
327
328 /* Check if the meta type matches the configured one */
329 if (filter->captionpad == NULL) {
330 GST_DEBUG_OBJECT (filter, "Creating new caption pad");
331
332 /* Create the caption pad and set the caps */
333 filter->captionpad =
334 gst_pad_new_from_static_template (&captiontemplate, "caption");
335 gst_pad_set_iterate_internal_links_function (filter->sinkpad,
336 GST_DEBUG_FUNCPTR (gst_cc_extractor_iterate_internal_links));
337 gst_pad_set_active (filter->captionpad, TRUE);
338
339 filter->caption_type = meta->caption_type;
340
341 gst_pad_sticky_events_foreach (filter->sinkpad, forward_sticky_events,
342 filter);
343
344 if (!gst_pad_has_current_caps (filter->captionpad)) {
345 GST_ERROR_OBJECT (filter, "Unknown/invalid caption type");
346 return GST_FLOW_NOT_NEGOTIATED;
347 }
348
349 gst_element_add_pad (GST_ELEMENT (filter), filter->captionpad);
350 gst_flow_combiner_add_pad (filter->combiner, filter->captionpad);
351 } else if (meta->caption_type != filter->caption_type) {
352 GstCaps *caption_caps =
353 create_caps_from_caption_type (meta->caption_type, &filter->video_info);
354
355 GST_DEBUG_OBJECT (filter, "Caption type changed from %d to %d",
356 filter->caption_type, meta->caption_type);
357 if (caption_caps == NULL) {
358 GST_ERROR_OBJECT (filter, "Unknown/invalid caption type");
359 return GST_FLOW_NOT_NEGOTIATED;
360 }
361
362 gst_pad_push_event (filter->captionpad, gst_event_new_caps (caption_caps));
363 gst_caps_unref (caption_caps);
364
365 filter->caption_type = meta->caption_type;
366 }
367
368 GST_DEBUG_OBJECT (filter,
369 "Creating new buffer of size %" G_GSIZE_FORMAT " bytes", meta->size);
370 /* Extract caption data into new buffer with identical buffer timestamps */
371 outbuf = gst_buffer_new_allocate (NULL, meta->size, NULL);
372 gst_buffer_fill (outbuf, 0, meta->data, meta->size);
373 GST_BUFFER_PTS (outbuf) = GST_BUFFER_PTS (buf);
374 GST_BUFFER_DTS (outbuf) = GST_BUFFER_DTS (buf);
375 GST_BUFFER_DURATION (outbuf) = GST_BUFFER_DURATION (buf);
376
377 if (tc_meta)
378 gst_buffer_add_video_time_code_meta (outbuf, &tc_meta->tc);
379
380 /* We don't really care about the flow return */
381 flow = gst_pad_push (filter->captionpad, outbuf);
382
383 /* Set flow return on pad and return combined value */
384 return gst_flow_combiner_update_pad_flow (filter->combiner,
385 filter->captionpad, flow);
386 }
387
388 static GstFlowReturn
gst_cc_extractor_chain(GstPad * pad,GstObject * parent,GstBuffer * buf)389 gst_cc_extractor_chain (GstPad * pad, GstObject * parent, GstBuffer * buf)
390 {
391 GstCCExtractor *filter = (GstCCExtractor *) parent;
392 GstFlowReturn flow = GST_FLOW_OK;
393 GstVideoCaptionMeta *cc_meta;
394 GstVideoTimeCodeMeta *tc_meta;
395 gpointer iter = NULL;
396
397 tc_meta = gst_buffer_get_video_time_code_meta (buf);
398
399 while ((cc_meta =
400 (GstVideoCaptionMeta *) gst_buffer_iterate_meta_filtered (buf, &iter,
401 GST_VIDEO_CAPTION_META_API_TYPE)) && flow == GST_FLOW_OK) {
402 flow = gst_cc_extractor_handle_meta (filter, buf, cc_meta, tc_meta);
403 }
404
405 /* If there's an issue handling the CC, return immediately */
406 if (flow != GST_FLOW_OK) {
407 gst_buffer_unref (buf);
408 return flow;
409 }
410
411 /* Push the buffer downstream and return the combined flow return */
412 return gst_flow_combiner_update_pad_flow (filter->combiner, filter->srcpad,
413 gst_pad_push (filter->srcpad, buf));
414 }
415
416 static GstStateChangeReturn
gst_cc_extractor_change_state(GstElement * element,GstStateChange transition)417 gst_cc_extractor_change_state (GstElement * element, GstStateChange transition)
418 {
419 GstStateChangeReturn ret;
420 GstCCExtractor *filter = GST_CCEXTRACTOR (element);
421
422 switch (transition) {
423 case GST_STATE_CHANGE_NULL_TO_READY:
424 break;
425 case GST_STATE_CHANGE_READY_TO_PAUSED:
426 break;
427 case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
428 break;
429 default:
430 break;
431 }
432
433 ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
434 if (ret != GST_STATE_CHANGE_SUCCESS)
435 return ret;
436
437 switch (transition) {
438 case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
439 break;
440 case GST_STATE_CHANGE_PAUSED_TO_READY:
441 gst_cc_extractor_reset (filter);
442 break;
443 case GST_STATE_CHANGE_READY_TO_NULL:
444 default:
445 break;
446 }
447
448 return ret;
449 }
450
451 static void
gst_cc_extractor_finalize(GObject * object)452 gst_cc_extractor_finalize (GObject * object)
453 {
454 GstCCExtractor *filter = GST_CCEXTRACTOR (object);
455
456 gst_flow_combiner_free (filter->combiner);
457
458 G_OBJECT_CLASS (parent_class)->finalize (object);
459 }
460