hlsdemux: Enable support for external subtitles
[platform/upstream/gstreamer.git] / ext / closedcaption / gstccextractor.c
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
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 *
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
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
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 *
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
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
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 *
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
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
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
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
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
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 }