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