• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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