• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /* GStreamer
2  * Copyright (C) <1999> Erik Walthinsen <omega@cse.ogi.edu>
3  * Copyright (C) 2004 Ronald S. Bultje <rbultje@ronald.bitfreak.net>
4  * Copyright (C) 2006 Tim-Philipp Müller <tim centricular net>
5  * Copyright (C) <2015> British Broadcasting Corporation <dash@rd.bbc.co.uk>
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Library General Public
9  * License as published by the Free Software Foundation; either
10  * version 2 of the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Library General Public License for more details.
16  *
17  * You should have received a copy of the GNU Library General Public
18  * License along with this library; if not, write to the
19  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
20  * Boston, MA 02110-1301, USA.
21  */
22 
23 /**
24  * SECTION:element-ttmlparse
25  * @title: ttmlparse
26  *
27  * Parses timed text subtitle files described using Timed Text Markup Language
28  * (TTML). Currently, only the EBU-TT-D profile of TTML, designed for
29  * distribution of subtitles over IP, is supported.
30  *
31  * The parser outputs a #GstBuffer for each scene in the input TTML file, a
32  * scene being a period of time during which a static set of subtitles should
33  * be visible. The parser places each text element within a scene into its own
34  * #GstMemory within the scene's buffer, and attaches metadata to the buffer
35  * describing the styling and layout associated with all the contained text
36  * elements. A downstream renderer element uses this information to correctly
37  * render the text on top of video frames.
38  *
39  * ## Example launch lines
40  * |[
41  * gst-launch-1.0 filesrc location=<media file location> ! video/quicktime ! qtdemux name=q ttmlrender name=r q. ! queue ! h264parse ! avdec_h264 ! autovideoconvert ! r.video_sink filesrc location=<subtitle file location> blocksize=16777216 ! queue ! ttmlparse ! r.text_sink r. ! ximagesink q. ! queue ! aacparse ! avdec_aac ! audioconvert ! alsasink
42  * ]| Parse and render TTML subtitles contained in a single XML file over an
43  * MP4 stream containing H.264 video and AAC audio.
44  *
45  */
46 
47 #include <stdio.h>
48 #include <stdlib.h>
49 #include <string.h>
50 #include <sys/types.h>
51 #include <glib.h>
52 
53 #include "gstttmlelements.h"
54 #include "gstttmlparse.h"
55 #include "ttmlparse.h"
56 
57 GST_DEBUG_CATEGORY (ttmlparse_debug);
58 #define GST_CAT_DEFAULT ttmlparse_debug
59 
60 
61 #define DEFAULT_ENCODING   NULL
62 
63 static GstStaticPadTemplate sink_templ = GST_STATIC_PAD_TEMPLATE ("sink",
64     GST_PAD_SINK,
65     GST_PAD_ALWAYS,
66     GST_STATIC_CAPS ("application/ttml+xml")
67     );
68 
69 static GstStaticPadTemplate src_templ = GST_STATIC_PAD_TEMPLATE ("src",
70     GST_PAD_SRC,
71     GST_PAD_ALWAYS,
72     GST_STATIC_CAPS ("text/x-raw(meta:GstSubtitleMeta)")
73     );
74 
75 static gboolean gst_ttml_parse_src_event (GstPad * pad, GstObject * parent,
76     GstEvent * event);
77 static gboolean gst_ttml_parse_src_query (GstPad * pad, GstObject * parent,
78     GstQuery * query);
79 static gboolean gst_ttml_parse_sink_event (GstPad * pad, GstObject * parent,
80     GstEvent * event);
81 
82 static GstStateChangeReturn gst_ttml_parse_change_state (GstElement * element,
83     GstStateChange transition);
84 
85 static GstFlowReturn gst_ttml_parse_chain (GstPad * sinkpad, GstObject * parent,
86     GstBuffer * buf);
87 static gboolean gst_element_ttmlparse_init (GstPlugin * plugin);
88 
89 #define gst_ttml_parse_parent_class parent_class
90 G_DEFINE_TYPE (GstTtmlParse, gst_ttml_parse, GST_TYPE_ELEMENT);
91 GST_ELEMENT_REGISTER_DEFINE_CUSTOM (ttmlparse, gst_element_ttmlparse_init);
92 
93 static void
gst_ttml_parse_dispose(GObject * object)94 gst_ttml_parse_dispose (GObject * object)
95 {
96   GstTtmlParse *ttmlparse = GST_TTML_PARSE (object);
97 
98   GST_DEBUG_OBJECT (ttmlparse, "cleaning up subtitle parser");
99 
100   g_free (ttmlparse->encoding);
101   ttmlparse->encoding = NULL;
102 
103   g_free (ttmlparse->detected_encoding);
104   ttmlparse->detected_encoding = NULL;
105 
106   if (ttmlparse->adapter) {
107     g_object_unref (ttmlparse->adapter);
108     ttmlparse->adapter = NULL;
109   }
110 
111   if (ttmlparse->textbuf) {
112     g_string_free (ttmlparse->textbuf, TRUE);
113     ttmlparse->textbuf = NULL;
114   }
115 
116   GST_CALL_PARENT (G_OBJECT_CLASS, dispose, (object));
117 }
118 
119 static void
gst_ttml_parse_class_init(GstTtmlParseClass * klass)120 gst_ttml_parse_class_init (GstTtmlParseClass * klass)
121 {
122   GObjectClass *object_class = G_OBJECT_CLASS (klass);
123   GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
124 
125   object_class->dispose = gst_ttml_parse_dispose;
126 
127   gst_element_class_add_pad_template (element_class,
128       gst_static_pad_template_get (&sink_templ));
129   gst_element_class_add_pad_template (element_class,
130       gst_static_pad_template_get (&src_templ));
131   gst_element_class_set_static_metadata (element_class,
132       "TTML subtitle parser", "Codec/Parser/Subtitle",
133       "Parses TTML subtitle files",
134       "GStreamer maintainers <gstreamer-devel@lists.sourceforge.net>, "
135       "Chris Bass <dash@rd.bbc.co.uk>");
136 
137   element_class->change_state = gst_ttml_parse_change_state;
138 }
139 
140 static void
gst_ttml_parse_init(GstTtmlParse * ttmlparse)141 gst_ttml_parse_init (GstTtmlParse * ttmlparse)
142 {
143   ttmlparse->sinkpad = gst_pad_new_from_static_template (&sink_templ, "sink");
144   gst_pad_set_chain_function (ttmlparse->sinkpad,
145       GST_DEBUG_FUNCPTR (gst_ttml_parse_chain));
146   gst_pad_set_event_function (ttmlparse->sinkpad,
147       GST_DEBUG_FUNCPTR (gst_ttml_parse_sink_event));
148   gst_element_add_pad (GST_ELEMENT (ttmlparse), ttmlparse->sinkpad);
149 
150   ttmlparse->srcpad = gst_pad_new_from_static_template (&src_templ, "src");
151   gst_pad_set_event_function (ttmlparse->srcpad,
152       GST_DEBUG_FUNCPTR (gst_ttml_parse_src_event));
153   gst_pad_set_query_function (ttmlparse->srcpad,
154       GST_DEBUG_FUNCPTR (gst_ttml_parse_src_query));
155   gst_element_add_pad (GST_ELEMENT (ttmlparse), ttmlparse->srcpad);
156 
157   ttmlparse->textbuf = g_string_new (NULL);
158   gst_segment_init (&ttmlparse->segment, GST_FORMAT_TIME);
159   ttmlparse->need_segment = TRUE;
160   ttmlparse->encoding = g_strdup (DEFAULT_ENCODING);
161   ttmlparse->detected_encoding = NULL;
162   ttmlparse->adapter = gst_adapter_new ();
163 }
164 
165 /*
166  * Source pad functions.
167  */
168 static gboolean
gst_ttml_parse_src_query(GstPad * pad,GstObject * parent,GstQuery * query)169 gst_ttml_parse_src_query (GstPad * pad, GstObject * parent, GstQuery * query)
170 {
171   GstTtmlParse *self = GST_TTML_PARSE (parent);
172   gboolean ret = FALSE;
173 
174   GST_DEBUG ("Handling %s query", GST_QUERY_TYPE_NAME (query));
175 
176   switch (GST_QUERY_TYPE (query)) {
177     case GST_QUERY_POSITION:{
178       GstFormat fmt;
179 
180       gst_query_parse_position (query, &fmt, NULL);
181       if (fmt != GST_FORMAT_TIME) {
182         ret = gst_pad_peer_query (self->sinkpad, query);
183       } else {
184         ret = TRUE;
185         gst_query_set_position (query, GST_FORMAT_TIME, self->segment.position);
186       }
187       break;
188     }
189     case GST_QUERY_SEEKING:
190     {
191       GstFormat fmt;
192       gboolean seekable = FALSE;
193 
194       ret = TRUE;
195 
196       gst_query_parse_seeking (query, &fmt, NULL, NULL, NULL);
197       if (fmt == GST_FORMAT_TIME) {
198         GstQuery *peerquery = gst_query_new_seeking (GST_FORMAT_BYTES);
199 
200         seekable = gst_pad_peer_query (self->sinkpad, peerquery);
201         if (seekable)
202           gst_query_parse_seeking (peerquery, NULL, &seekable, NULL, NULL);
203         gst_query_unref (peerquery);
204       }
205 
206       gst_query_set_seeking (query, fmt, seekable, seekable ? 0 : -1, -1);
207       break;
208     }
209     default:
210       ret = gst_pad_query_default (pad, parent, query);
211       break;
212   }
213 
214   return ret;
215 }
216 
217 static gboolean
gst_ttml_parse_src_event(GstPad * pad,GstObject * parent,GstEvent * event)218 gst_ttml_parse_src_event (GstPad * pad, GstObject * parent, GstEvent * event)
219 {
220   GstTtmlParse *self = GST_TTML_PARSE (parent);
221   gboolean ret = FALSE;
222 
223   GST_DEBUG ("Handling %s event", GST_EVENT_TYPE_NAME (event));
224 
225   switch (GST_EVENT_TYPE (event)) {
226     case GST_EVENT_SEEK:
227     {
228       GstFormat format;
229       GstSeekFlags flags;
230       GstSeekType start_type, stop_type;
231       gint64 start, stop;
232       gdouble rate;
233       gboolean update;
234 
235       gst_event_parse_seek (event, &rate, &format, &flags,
236           &start_type, &start, &stop_type, &stop);
237 
238       if (format != GST_FORMAT_TIME) {
239         GST_WARNING_OBJECT (self, "we only support seeking in TIME format");
240         gst_event_unref (event);
241         goto beach;
242       }
243 
244       /* Convert that seek to a seeking in bytes at position 0,
245          FIXME: could use an index */
246       ret = gst_pad_push_event (self->sinkpad,
247           gst_event_new_seek (rate, GST_FORMAT_BYTES, flags,
248               GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_NONE, 0));
249 
250       if (ret) {
251         /* Apply the seek to our segment */
252         gst_segment_do_seek (&self->segment, rate, format, flags,
253             start_type, start, stop_type, stop, &update);
254 
255         GST_DEBUG_OBJECT (self, "segment after seek: %" GST_SEGMENT_FORMAT,
256             &self->segment);
257 
258         self->need_segment = TRUE;
259       } else {
260         GST_WARNING_OBJECT (self, "seek to 0 bytes failed");
261       }
262 
263       gst_event_unref (event);
264       break;
265     }
266     default:
267       ret = gst_pad_event_default (pad, parent, event);
268       break;
269   }
270 
271 beach:
272   return ret;
273 }
274 
275 static gchar *
gst_convert_to_utf8(const gchar * str,gsize len,const gchar * encoding,gsize * consumed,GError ** err)276 gst_convert_to_utf8 (const gchar * str, gsize len, const gchar * encoding,
277     gsize * consumed, GError ** err)
278 {
279   gchar *ret = NULL;
280 
281   *consumed = 0;
282   /* The char cast is necessary in glib < 2.24 */
283   ret =
284       g_convert_with_fallback (str, len, "UTF-8", encoding, (char *) "*",
285       consumed, NULL, err);
286   if (ret == NULL)
287     return ret;
288 
289   /* + 3 to skip UTF-8 BOM if it was added */
290   len = strlen (ret);
291   if (len >= 3 && (guint8) ret[0] == 0xEF && (guint8) ret[1] == 0xBB
292       && (guint8) ret[2] == 0xBF)
293     memmove (ret, ret + 3, len + 1 - 3);
294 
295   return ret;
296 }
297 
298 static gchar *
detect_encoding(const gchar * str,gsize len)299 detect_encoding (const gchar * str, gsize len)
300 {
301   if (len >= 3 && (guint8) str[0] == 0xEF && (guint8) str[1] == 0xBB
302       && (guint8) str[2] == 0xBF)
303     return g_strdup ("UTF-8");
304 
305   if (len >= 2 && (guint8) str[0] == 0xFE && (guint8) str[1] == 0xFF)
306     return g_strdup ("UTF-16BE");
307 
308   if (len >= 2 && (guint8) str[0] == 0xFF && (guint8) str[1] == 0xFE)
309     return g_strdup ("UTF-16LE");
310 
311   if (len >= 4 && (guint8) str[0] == 0x00 && (guint8) str[1] == 0x00
312       && (guint8) str[2] == 0xFE && (guint8) str[3] == 0xFF)
313     return g_strdup ("UTF-32BE");
314 
315   if (len >= 4 && (guint8) str[0] == 0xFF && (guint8) str[1] == 0xFE
316       && (guint8) str[2] == 0x00 && (guint8) str[3] == 0x00)
317     return g_strdup ("UTF-32LE");
318 
319   return NULL;
320 }
321 
322 static gchar *
convert_encoding(GstTtmlParse * self,const gchar * str,gsize len,gsize * consumed)323 convert_encoding (GstTtmlParse * self, const gchar * str, gsize len,
324     gsize * consumed)
325 {
326   const gchar *encoding;
327   GError *err = NULL;
328   gchar *ret = NULL;
329 
330   *consumed = 0;
331 
332   /* First try any detected encoding */
333   if (self->detected_encoding) {
334     ret =
335         gst_convert_to_utf8 (str, len, self->detected_encoding, consumed, &err);
336 
337     if (!err)
338       return ret;
339 
340     GST_WARNING_OBJECT (self, "could not convert string from '%s' to UTF-8: %s",
341         self->detected_encoding, err->message);
342     g_free (self->detected_encoding);
343     self->detected_encoding = NULL;
344     g_error_free (err);
345   }
346 
347   /* Otherwise check if it's UTF8 */
348   if (self->valid_utf8) {
349     if (g_utf8_validate (str, len, NULL)) {
350       GST_LOG_OBJECT (self, "valid UTF-8, no conversion needed");
351       *consumed = len;
352       return g_strndup (str, len);
353     }
354     GST_INFO_OBJECT (self, "invalid UTF-8!");
355     self->valid_utf8 = FALSE;
356   }
357 
358   /* Else try fallback */
359   encoding = self->encoding;
360   if (encoding == NULL || *encoding == '\0') {
361     /* if local encoding is UTF-8 and no encoding specified
362      * via the environment variable, assume ISO-8859-15 */
363     if (g_get_charset (&encoding)) {
364       encoding = "ISO-8859-15";
365     }
366   }
367 
368   ret = gst_convert_to_utf8 (str, len, encoding, consumed, &err);
369 
370   if (err) {
371     GST_WARNING_OBJECT (self, "could not convert string from '%s' to UTF-8: %s",
372         encoding, err->message);
373     g_error_free (err);
374 
375     /* invalid input encoding, fall back to ISO-8859-15 (always succeeds) */
376     ret = gst_convert_to_utf8 (str, len, "ISO-8859-15", consumed, NULL);
377   }
378 
379   GST_LOG_OBJECT (self,
380       "successfully converted %" G_GSIZE_FORMAT " characters from %s to UTF-8"
381       "%s", len, encoding, (err) ? " , using ISO-8859-15 as fallback" : "");
382 
383   return ret;
384 }
385 
386 static GstCaps *
gst_ttml_parse_get_src_caps(GstTtmlParse * self)387 gst_ttml_parse_get_src_caps (GstTtmlParse * self)
388 {
389   GstCaps *caps;
390   GstCapsFeatures *features = gst_caps_features_new ("meta:GstSubtitleMeta",
391       NULL);
392 
393   caps = gst_caps_new_empty_simple ("text/x-raw");
394   gst_caps_set_features (caps, 0, features);
395   return caps;
396 }
397 
398 static void
feed_textbuf(GstTtmlParse * self,GstBuffer * buf)399 feed_textbuf (GstTtmlParse * self, GstBuffer * buf)
400 {
401   gboolean discont;
402   gsize consumed;
403   gchar *input = NULL;
404   const guint8 *data;
405   gsize avail;
406 
407   discont = GST_BUFFER_IS_DISCONT (buf);
408 
409   if (GST_BUFFER_OFFSET_IS_VALID (buf) &&
410       GST_BUFFER_OFFSET (buf) != self->offset) {
411     self->offset = GST_BUFFER_OFFSET (buf);
412     discont = TRUE;
413   }
414 
415   if (discont) {
416     GST_INFO ("discontinuity");
417     /* flush the parser state */
418     g_string_truncate (self->textbuf, 0);
419     gst_adapter_clear (self->adapter);
420     /* we could set a flag to make sure that the next buffer we push out also
421      * has the DISCONT flag set, but there's no point really given that it's
422      * subtitles which are discontinuous by nature. */
423   }
424 
425   self->offset += gst_buffer_get_size (buf);
426 
427   gst_adapter_push (self->adapter, buf);
428 
429   avail = gst_adapter_available (self->adapter);
430   data = gst_adapter_map (self->adapter, avail);
431   input = convert_encoding (self, (const gchar *) data, avail, &consumed);
432 
433   if (input && consumed > 0) {
434     if (!self->textbuf)
435       self->textbuf = g_string_new (input);
436     else
437       self->textbuf = g_string_append (self->textbuf, input);
438 
439     gst_adapter_unmap (self->adapter);
440     gst_adapter_flush (self->adapter, consumed);
441   } else {
442     gst_adapter_unmap (self->adapter);
443   }
444 
445   g_free (input);
446 }
447 
448 static GstFlowReturn
handle_buffer(GstTtmlParse * self,GstBuffer * buf)449 handle_buffer (GstTtmlParse * self, GstBuffer * buf)
450 {
451   GstFlowReturn ret = GST_FLOW_OK;
452   GstCaps *caps = NULL;
453   GList *subtitle_list = NULL;
454   GList *iter;
455   GstClockTime begin = GST_BUFFER_PTS (buf);
456   GstClockTime duration = GST_BUFFER_DURATION (buf);
457   guint consumed;
458 
459   if (self->first_buffer) {
460     GstMapInfo map;
461 
462     gst_buffer_map (buf, &map, GST_MAP_READ);
463     self->detected_encoding = detect_encoding ((gchar *) map.data, map.size);
464     gst_buffer_unmap (buf, &map);
465     self->first_buffer = FALSE;
466   }
467 
468   feed_textbuf (self, buf);
469 
470   if (!(caps = gst_ttml_parse_get_src_caps (self)))
471     return GST_FLOW_EOS;
472   gst_caps_unref (caps);
473 
474   /* Push newsegment if needed */
475   if (self->need_segment) {
476     GST_LOG_OBJECT (self, "pushing newsegment event with %" GST_SEGMENT_FORMAT,
477         &self->segment);
478 
479     gst_pad_push_event (self->srcpad, gst_event_new_segment (&self->segment));
480     self->need_segment = FALSE;
481   }
482 
483   do {
484     consumed = ttml_parse (self->textbuf->str, begin, duration, &subtitle_list);
485 
486     if (!consumed) {
487       GST_DEBUG_OBJECT (self, "need more data");
488       return ret;
489     }
490 
491     self->textbuf = g_string_erase (self->textbuf, 0, consumed);
492 
493     for (iter = subtitle_list; iter; iter = g_list_next (iter)) {
494       GstBuffer *op_buffer = GST_BUFFER (iter->data);
495       self->segment.position = GST_BUFFER_PTS (op_buffer);
496 
497       ret = gst_pad_push (self->srcpad, op_buffer);
498 
499       if (ret != GST_FLOW_OK) {
500         GST_DEBUG_OBJECT (self, "flow: %s", gst_flow_get_name (ret));
501         break;
502       }
503     }
504 
505     g_list_free (subtitle_list);
506   } while (TRUE);
507 
508   return ret;
509 }
510 
511 static GstFlowReturn
gst_ttml_parse_chain(GstPad * sinkpad,GstObject * parent,GstBuffer * buf)512 gst_ttml_parse_chain (GstPad * sinkpad, GstObject * parent, GstBuffer * buf)
513 {
514   GstTtmlParse *self = GST_TTML_PARSE (parent);
515   return handle_buffer (self, buf);
516 }
517 
518 static gboolean
gst_ttml_parse_sink_event(GstPad * pad,GstObject * parent,GstEvent * event)519 gst_ttml_parse_sink_event (GstPad * pad, GstObject * parent, GstEvent * event)
520 {
521   GstTtmlParse *self = GST_TTML_PARSE (parent);
522   gboolean ret = FALSE;
523 
524   GST_DEBUG ("Handling %s event", GST_EVENT_TYPE_NAME (event));
525 
526   switch (GST_EVENT_TYPE (event)) {
527     case GST_EVENT_SEGMENT:
528     {
529       const GstSegment *s;
530       gst_event_parse_segment (event, &s);
531       if (s->format == GST_FORMAT_TIME)
532         gst_event_copy_segment (event, &self->segment);
533       GST_DEBUG_OBJECT (self, "newsegment (%s)",
534           gst_format_get_name (self->segment.format));
535 
536       /* if not time format, we'll either start with a 0 timestamp anyway or
537        * it's following a seek in which case we'll have saved the requested
538        * seek segment and don't want to overwrite it (remember that on a seek
539        * we always just seek back to the start in BYTES format and just throw
540        * away all text that's before the requested position; if the subtitles
541        * come from an upstream demuxer, it won't be able to handle our BYTES
542        * seek request and instead send us a newsegment from the seek request
543        * it received via its video pads instead, so all is fine then too) */
544       ret = TRUE;
545       self->need_segment = TRUE;
546       gst_event_unref (event);
547       break;
548     }
549     case GST_EVENT_CAPS:
550     {
551       GstCaps *caps;
552       gst_event_unref (event);
553 
554       caps = gst_ttml_parse_get_src_caps (self);
555       event = gst_event_new_caps (caps);
556       gst_caps_unref (caps);
557 
558       ret = gst_pad_push_event (self->srcpad, event);
559       break;
560     }
561     default:
562       ret = gst_pad_event_default (pad, parent, event);
563       break;
564   }
565 
566   return ret;
567 }
568 
569 static GstStateChangeReturn
gst_ttml_parse_change_state(GstElement * element,GstStateChange transition)570 gst_ttml_parse_change_state (GstElement * element, GstStateChange transition)
571 {
572   GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;
573   GstTtmlParse *self = GST_TTML_PARSE (element);
574 
575   switch (transition) {
576     case GST_STATE_CHANGE_READY_TO_PAUSED:
577       /* format detection will init the parser state */
578       self->offset = 0;
579       self->valid_utf8 = TRUE;
580       self->first_buffer = TRUE;
581       g_free (self->detected_encoding);
582       self->detected_encoding = NULL;
583       g_string_truncate (self->textbuf, 0);
584       gst_adapter_clear (self->adapter);
585       break;
586     default:
587       break;
588   }
589 
590   ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
591   if (ret == GST_STATE_CHANGE_FAILURE)
592     return ret;
593 
594   switch (transition) {
595     case GST_STATE_CHANGE_PAUSED_TO_READY:
596       break;
597     default:
598       break;
599   }
600 
601   return ret;
602 }
603 
604 static gboolean
gst_element_ttmlparse_init(GstPlugin * plugin)605 gst_element_ttmlparse_init (GstPlugin * plugin)
606 {
607   guint rank = GST_RANK_NONE;
608 
609   ttml_element_init (plugin);
610 
611   GST_DEBUG_CATEGORY_INIT (ttmlparse_debug, "ttmlparse", 0, "TTML parser");
612 
613   /* We don't want this autoplugged by default yet for now */
614   if (g_getenv ("GST_TTML_AUTOPLUG")) {
615     GST_INFO_OBJECT (plugin, "Registering ttml elements with primary rank.");
616     rank = GST_RANK_PRIMARY;
617   }
618 
619   return gst_element_register (plugin, "ttmlparse", rank, GST_TYPE_TTML_PARSE);
620 }
621