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) 2016 Philippe Normand <pnormand@igalia.com>
6 * Copyright (C) 2016 Jan Schmidt <jan@centricular.com>
8 * This library is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Library General Public
10 * License as published by the Free Software Foundation; either
11 * version 2 of the License, or (at your option) any later version.
13 * This library is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * Library General Public License for more details.
18 * You should have received a copy of the GNU Library General Public
19 * License along with this library; if not, write to the
20 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
21 * Boston, MA 02110-1301, USA.
31 #include <sys/types.h>
34 #include "gstsubparse.h"
35 #include "gstssaparse.h"
36 #include "samiparse.h"
37 #include "tmplayerparse.h"
38 #include "mpl2parse.h"
39 #include "qttextparse.h"
41 GST_DEBUG_CATEGORY (sub_parse_debug);
43 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
44 #define SUBPARSE_SEEK_GET_LOCK(elem) (&elem->seek_lock)
45 #define SUBPARSE_SEEK_LOCK(elem) g_mutex_lock(SUBPARSE_SEEK_GET_LOCK(elem))
46 #define SUBPARSE_SEEK_TRYLOCK(elem) g_mutex_trylock(SUBPARSE_SEEK_GET_LOCK(elem))
47 #define SUBPARSE_SEEK_UNLOCK(elem) g_mutex_unlock(SUBPARSE_SEEK_GET_LOCK(elem))
50 #define DEFAULT_ENCODING NULL
51 #define ATTRIBUTE_REGEX "\\s?[a-zA-Z0-9\\. \t\\(\\)]*"
52 static const gchar *allowed_srt_tags[] = { "i", "b", "u", NULL };
53 static const gchar *allowed_vtt_tags[] =
54 { "i", "b", "c", "u", "v", "ruby", "rt", NULL };
56 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
57 #define DEFAULT_CURRENT_LANGUAGE NULL
64 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
65 PROP_EXTSUB_CURRENT_LANGUAGE
70 gst_sub_parse_set_property (GObject * object, guint prop_id,
71 const GValue * value, GParamSpec * pspec);
73 gst_sub_parse_get_property (GObject * object, guint prop_id,
74 GValue * value, GParamSpec * pspec);
77 static GstStaticPadTemplate sink_templ = GST_STATIC_PAD_TEMPLATE ("sink",
80 GST_STATIC_CAPS ("application/x-subtitle; application/x-subtitle-sami; "
81 "application/x-subtitle-tmplayer; application/x-subtitle-mpl2; "
82 "application/x-subtitle-dks; application/x-subtitle-qttext;"
83 "application/x-subtitle-lrc; application/x-subtitle-vtt")
86 static GstStaticPadTemplate src_templ = GST_STATIC_PAD_TEMPLATE ("src",
89 GST_STATIC_CAPS ("text/x-raw, format= { pango-markup, utf8 }")
93 static gboolean gst_sub_parse_src_event (GstPad * pad, GstObject * parent,
95 static gboolean gst_sub_parse_src_query (GstPad * pad, GstObject * parent,
97 static gboolean gst_sub_parse_sink_event (GstPad * pad, GstObject * parent,
100 static GstStateChangeReturn gst_sub_parse_change_state (GstElement * element,
101 GstStateChange transition);
103 static GstFlowReturn gst_sub_parse_chain (GstPad * sinkpad, GstObject * parent,
106 #define gst_sub_parse_parent_class parent_class
107 G_DEFINE_TYPE (GstSubParse, gst_sub_parse, GST_TYPE_ELEMENT);
110 gst_sub_parse_dispose (GObject * object)
112 GstSubParse *subparse = GST_SUBPARSE (object);
114 GST_DEBUG_OBJECT (subparse, "cleaning up subtitle parser");
116 if (subparse->encoding) {
117 g_free (subparse->encoding);
118 subparse->encoding = NULL;
121 if (subparse->detected_encoding) {
122 g_free (subparse->detected_encoding);
123 subparse->detected_encoding = NULL;
126 if (subparse->adapter) {
127 g_object_unref (subparse->adapter);
128 subparse->adapter = NULL;
131 if (subparse->textbuf) {
132 g_string_free (subparse->textbuf, TRUE);
133 subparse->textbuf = NULL;
136 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
137 g_free (subparse->state.current_language);
138 subparse->state.current_language = NULL;
140 g_mutex_clear (&subparse->seek_lock);
142 GST_CALL_PARENT (G_OBJECT_CLASS, dispose, (object));
146 gst_sub_parse_class_init (GstSubParseClass * klass)
148 GObjectClass *object_class = G_OBJECT_CLASS (klass);
149 GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
151 object_class->dispose = gst_sub_parse_dispose;
152 object_class->set_property = gst_sub_parse_set_property;
153 object_class->get_property = gst_sub_parse_get_property;
155 gst_element_class_add_static_pad_template (element_class, &sink_templ);
156 gst_element_class_add_static_pad_template (element_class, &src_templ);
157 gst_element_class_set_static_metadata (element_class,
158 "Subtitle parser", "Codec/Parser/Subtitle",
159 "Parses subtitle (.sub) files into text streams",
160 "Gustavo J. A. M. Carneiro <gjc@inescporto.pt>, "
161 "GStreamer maintainers <gstreamer-devel@lists.freedesktop.org>");
163 element_class->change_state = gst_sub_parse_change_state;
165 g_object_class_install_property (object_class, PROP_ENCODING,
166 g_param_spec_string ("subtitle-encoding", "subtitle charset encoding",
167 "Encoding to assume if input subtitles are not in UTF-8 or any other "
168 "Unicode encoding. If not set, the GST_SUBTITLE_ENCODING environment "
169 "variable will be checked for an encoding to use. If that is not set "
170 "either, ISO-8859-15 will be assumed.", DEFAULT_ENCODING,
171 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
173 g_object_class_install_property (object_class, PROP_VIDEOFPS,
174 gst_param_spec_fraction ("video-fps", "Video framerate",
175 "Framerate of the video stream. This is needed by some subtitle "
176 "formats to synchronize subtitles and video properly. If not set "
177 "and the subtitle format requires it subtitles may be out of sync.",
178 0, 1, G_MAXINT, 1, 24000, 1001,
179 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
180 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
181 g_object_class_install_property (object_class, PROP_EXTSUB_CURRENT_LANGUAGE,
182 g_param_spec_string ("current-language", "Current language",
183 "Current language of the subtitle in external subtitle case.",
184 DEFAULT_CURRENT_LANGUAGE,
185 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
190 gst_sub_parse_init (GstSubParse * subparse)
192 subparse->sinkpad = gst_pad_new_from_static_template (&sink_templ, "sink");
193 gst_pad_set_chain_function (subparse->sinkpad,
194 GST_DEBUG_FUNCPTR (gst_sub_parse_chain));
195 gst_pad_set_event_function (subparse->sinkpad,
196 GST_DEBUG_FUNCPTR (gst_sub_parse_sink_event));
197 gst_element_add_pad (GST_ELEMENT (subparse), subparse->sinkpad);
199 subparse->srcpad = gst_pad_new_from_static_template (&src_templ, "src");
200 gst_pad_set_event_function (subparse->srcpad,
201 GST_DEBUG_FUNCPTR (gst_sub_parse_src_event));
202 gst_pad_set_query_function (subparse->srcpad,
203 GST_DEBUG_FUNCPTR (gst_sub_parse_src_query));
204 gst_element_add_pad (GST_ELEMENT (subparse), subparse->srcpad);
206 subparse->textbuf = g_string_new (NULL);
207 subparse->parser_type = GST_SUB_PARSE_FORMAT_UNKNOWN;
208 subparse->flushing = FALSE;
209 gst_segment_init (&subparse->segment, GST_FORMAT_TIME);
210 subparse->need_segment = TRUE;
211 subparse->encoding = g_strdup (DEFAULT_ENCODING);
212 subparse->detected_encoding = NULL;
213 subparse->adapter = gst_adapter_new ();
215 subparse->fps_n = 24000;
216 subparse->fps_d = 1001;
217 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
218 subparse->state.language_list = NULL;
219 subparse->state.current_language = NULL;
220 subparse->state.langlist_msg_posted = FALSE;
222 g_mutex_init (&subparse->seek_lock);
227 * Source pad functions.
231 gst_sub_parse_src_query (GstPad * pad, GstObject * parent, GstQuery * query)
233 GstSubParse *self = GST_SUBPARSE (parent);
234 gboolean ret = FALSE;
236 GST_DEBUG ("Handling %s query", GST_QUERY_TYPE_NAME (query));
238 switch (GST_QUERY_TYPE (query)) {
239 case GST_QUERY_POSITION:{
242 gst_query_parse_position (query, &fmt, NULL);
243 if (fmt != GST_FORMAT_TIME) {
244 ret = gst_pad_peer_query (self->sinkpad, query);
247 gst_query_set_position (query, GST_FORMAT_TIME, self->segment.position);
251 case GST_QUERY_SEEKING:
254 gboolean seekable = FALSE;
258 gst_query_parse_seeking (query, &fmt, NULL, NULL, NULL);
259 if (fmt == GST_FORMAT_TIME) {
260 GstQuery *peerquery = gst_query_new_seeking (GST_FORMAT_BYTES);
262 seekable = gst_pad_peer_query (self->sinkpad, peerquery);
264 gst_query_parse_seeking (peerquery, NULL, &seekable, NULL, NULL);
265 gst_query_unref (peerquery);
268 gst_query_set_seeking (query, fmt, seekable, seekable ? 0 : -1, -1);
272 ret = gst_pad_query_default (pad, parent, query);
280 gst_sub_parse_src_event (GstPad * pad, GstObject * parent, GstEvent * event)
282 GstSubParse *self = GST_SUBPARSE (parent);
283 gboolean ret = FALSE;
285 GST_DEBUG ("Handling %s event", GST_EVENT_TYPE_NAME (event));
287 switch (GST_EVENT_TYPE (event)) {
292 GstSeekType start_type, stop_type;
297 gst_event_parse_seek (event, &rate, &format, &flags,
298 &start_type, &start, &stop_type, &stop);
300 if (format != GST_FORMAT_TIME) {
301 GST_WARNING_OBJECT (self, "we only support seeking in TIME format");
302 gst_event_unref (event);
306 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
307 SUBPARSE_SEEK_LOCK (self);
309 /* Convert that seek to a seeking in bytes at position 0,
310 FIXME: could use an index */
311 ret = gst_pad_push_event (self->sinkpad,
312 gst_event_new_seek (rate, GST_FORMAT_BYTES, flags,
313 GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_NONE, 0));
316 /* Apply the seek to our segment */
317 gst_segment_do_seek (&self->segment, rate, format, flags,
318 start_type, start, stop_type, stop, &update);
320 GST_DEBUG_OBJECT (self, "segment after seek: %" GST_SEGMENT_FORMAT,
323 /* will mark need_segment when receiving segment from upstream,
324 * after FLUSH and all that has happened,
325 * rather than racing with chain */
327 GST_WARNING_OBJECT (self, "seek to 0 bytes failed");
330 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
331 SUBPARSE_SEEK_UNLOCK (self);
334 gst_event_unref (event);
338 ret = gst_pad_event_default (pad, parent, event);
347 gst_sub_parse_set_property (GObject * object, guint prop_id,
348 const GValue * value, GParamSpec * pspec)
350 GstSubParse *subparse = GST_SUBPARSE (object);
352 GST_OBJECT_LOCK (subparse);
355 g_free (subparse->encoding);
356 subparse->encoding = g_value_dup_string (value);
357 GST_LOG_OBJECT (object, "subtitle encoding set to %s",
358 GST_STR_NULL (subparse->encoding));
362 subparse->fps_n = gst_value_get_fraction_numerator (value);
363 subparse->fps_d = gst_value_get_fraction_denominator (value);
364 GST_DEBUG_OBJECT (object, "video framerate set to %d/%d", subparse->fps_n,
367 if (!subparse->state.have_internal_fps) {
368 subparse->state.fps_n = subparse->fps_n;
369 subparse->state.fps_d = subparse->fps_d;
373 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
374 case PROP_EXTSUB_CURRENT_LANGUAGE:
375 g_free(subparse->state.current_language);
376 subparse->state.current_language = g_value_dup_string (value);
377 GST_LOG_OBJECT (subparse, "subtitle current language set to %s",
378 GST_STR_NULL (subparse->state.current_language));
379 sami_context_change_language(&subparse->state);
383 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
386 GST_OBJECT_UNLOCK (subparse);
390 gst_sub_parse_get_property (GObject * object, guint prop_id,
391 GValue * value, GParamSpec * pspec)
393 GstSubParse *subparse = GST_SUBPARSE (object);
395 GST_OBJECT_LOCK (subparse);
398 g_value_set_string (value, subparse->encoding);
401 gst_value_set_fraction (value, subparse->fps_n, subparse->fps_d);
403 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
404 case PROP_EXTSUB_CURRENT_LANGUAGE:
405 g_value_set_string (value, subparse->state.current_language);
409 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
412 GST_OBJECT_UNLOCK (subparse);
416 gst_sub_parse_get_format_description (GstSubParseFormat format)
419 case GST_SUB_PARSE_FORMAT_MDVDSUB:
421 case GST_SUB_PARSE_FORMAT_SUBRIP:
423 case GST_SUB_PARSE_FORMAT_MPSUB:
425 case GST_SUB_PARSE_FORMAT_SAMI:
427 case GST_SUB_PARSE_FORMAT_TMPLAYER:
429 case GST_SUB_PARSE_FORMAT_MPL2:
431 case GST_SUB_PARSE_FORMAT_SUBVIEWER:
433 case GST_SUB_PARSE_FORMAT_DKS:
435 case GST_SUB_PARSE_FORMAT_VTT:
437 case GST_SUB_PARSE_FORMAT_QTTEXT:
439 case GST_SUB_PARSE_FORMAT_LRC:
442 case GST_SUB_PARSE_FORMAT_UNKNOWN:
449 gst_convert_to_utf8 (const gchar * str, gsize len, const gchar * encoding,
450 gsize * consumed, GError ** err)
455 /* The char cast is necessary in glib < 2.24 */
457 g_convert_with_fallback (str, len, "UTF-8", encoding, (char *) "*",
458 consumed, NULL, err);
462 /* + 3 to skip UTF-8 BOM if it was added */
464 if (len >= 3 && (guint8) ret[0] == 0xEF && (guint8) ret[1] == 0xBB
465 && (guint8) ret[2] == 0xBF)
466 memmove (ret, ret + 3, len + 1 - 3);
472 detect_encoding (const gchar * str, gsize len)
474 if (len >= 3 && (guint8) str[0] == 0xEF && (guint8) str[1] == 0xBB
475 && (guint8) str[2] == 0xBF)
476 return g_strdup ("UTF-8");
478 if (len >= 2 && (guint8) str[0] == 0xFE && (guint8) str[1] == 0xFF)
479 return g_strdup ("UTF-16BE");
481 if (len >= 2 && (guint8) str[0] == 0xFF && (guint8) str[1] == 0xFE)
482 return g_strdup ("UTF-16LE");
484 if (len >= 4 && (guint8) str[0] == 0x00 && (guint8) str[1] == 0x00
485 && (guint8) str[2] == 0xFE && (guint8) str[3] == 0xFF)
486 return g_strdup ("UTF-32BE");
488 if (len >= 4 && (guint8) str[0] == 0xFF && (guint8) str[1] == 0xFE
489 && (guint8) str[2] == 0x00 && (guint8) str[3] == 0x00)
490 return g_strdup ("UTF-32LE");
496 convert_encoding (GstSubParse * self, const gchar * str, gsize len,
499 const gchar *encoding;
505 /* First try any detected encoding */
506 if (self->detected_encoding) {
508 gst_convert_to_utf8 (str, len, self->detected_encoding, consumed, &err);
513 GST_WARNING_OBJECT (self, "could not convert string from '%s' to UTF-8: %s",
514 self->detected_encoding, err->message);
515 g_free (self->detected_encoding);
516 self->detected_encoding = NULL;
517 g_clear_error (&err);
520 /* Otherwise check if it's UTF8 */
521 if (self->valid_utf8) {
522 if (g_utf8_validate (str, len, NULL)) {
523 GST_LOG_OBJECT (self, "valid UTF-8, no conversion needed");
525 return g_strndup (str, len);
527 GST_INFO_OBJECT (self, "invalid UTF-8!");
528 self->valid_utf8 = FALSE;
531 /* Else try fallback */
532 encoding = self->encoding;
533 if (encoding == NULL || *encoding == '\0') {
534 encoding = g_getenv ("GST_SUBTITLE_ENCODING");
536 if (encoding == NULL || *encoding == '\0') {
537 /* if local encoding is UTF-8 and no encoding specified
538 * via the environment variable, assume ISO-8859-15 */
539 if (g_get_charset (&encoding)) {
540 encoding = "ISO-8859-15";
544 ret = gst_convert_to_utf8 (str, len, encoding, consumed, &err);
547 GST_WARNING_OBJECT (self, "could not convert string from '%s' to UTF-8: %s",
548 encoding, err->message);
549 g_clear_error (&err);
551 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
552 if (!strcmp (self->encoding, "EUC-KR")) {
553 GST_LOG_OBJECT (self, "use CP949 as fallback");
554 g_free (self->encoding);
555 self->encoding = g_strdup ("CP949");
556 encoding = self->encoding;
557 ret = gst_convert_to_utf8 (str, len, encoding, consumed, &err);
560 /* invalid input encoding, fall back to ISO-8859-15 (always succeeds) */
561 GST_LOG_OBJECT (self, "use ISO-8859-15 as fallback");
562 ret = gst_convert_to_utf8 (str, len, "ISO-8859-15", consumed, NULL);
563 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
568 GST_LOG_OBJECT (self,
569 "successfully converted %" G_GSIZE_FORMAT " characters from %s to UTF-8",
576 get_next_line (GstSubParse * self)
579 const char *line_end;
581 gboolean have_r = FALSE;
583 line_end = strchr (self->textbuf->str, '\n');
586 /* end-of-line not found; return for more data */
590 /* get rid of '\r' */
591 if (line_end != self->textbuf->str && *(line_end - 1) == '\r') {
596 line_len = line_end - self->textbuf->str;
597 line = g_strndup (self->textbuf->str, line_len);
598 self->textbuf = g_string_erase (self->textbuf, 0,
599 line_len + (have_r ? 2 : 1));
604 parse_mdvdsub (ParserState * state, const gchar * line)
606 const gchar *line_split;
608 guint start_frame, end_frame;
609 guint64 clip_start = 0, clip_stop = 0;
610 gboolean in_seg = FALSE;
614 /* style variables */
620 if (sscanf (line, "{%u}{%u}", &start_frame, &end_frame) != 2) {
621 g_warning ("Parse of the following line, assumed to be in microdvd .sub"
622 " format, failed:\n%s", line);
626 /* skip the {%u}{%u} part */
627 line = strchr (line, '}') + 1;
628 line = strchr (line, '}') + 1;
630 /* see if there's a first line with a framerate */
631 if (start_frame == 1 && end_frame == 1) {
632 gchar *rest, *end = NULL;
634 rest = g_strdup (line);
635 g_strdelimit (rest, ",", '.');
636 fps = g_ascii_strtod (rest, &end);
638 gst_util_double_to_fraction (fps, &state->fps_n, &state->fps_d);
639 GST_INFO ("framerate from file: %d/%d ('%s')", state->fps_n,
647 gst_util_uint64_scale (start_frame, GST_SECOND * state->fps_d,
650 gst_util_uint64_scale (end_frame - start_frame, GST_SECOND * state->fps_d,
653 /* Check our segment start/stop */
654 in_seg = gst_segment_clip (state->segment, GST_FORMAT_TIME,
655 state->start_time, state->start_time + state->duration, &clip_start,
658 /* No need to parse that text if it's out of segment */
660 state->start_time = clip_start;
661 state->duration = clip_stop - clip_start;
666 markup = g_string_new (NULL);
671 /* parse style markup */
672 if (strncmp (line, "{y:i}", 5) == 0) {
674 line = strchr (line, '}') + 1;
676 if (strncmp (line, "{y:b}", 5) == 0) {
678 line = strchr (line, '}') + 1;
680 if (sscanf (line, "{s:%u}", &fontsize) == 1) {
681 line = strchr (line, '}') + 1;
683 /* forward slashes at beginning/end signify italics too */
684 if (g_str_has_prefix (line, "/")) {
688 if ((line_split = strchr (line, '|')))
689 line_chunk = g_markup_escape_text (line, line_split - line);
691 line_chunk = g_markup_escape_text (line, strlen (line));
693 /* Remove italics markers at end of line/stanza (CHECKME: are end slashes
694 * always at the end of a line or can they span multiple lines?) */
695 if (g_str_has_suffix (line_chunk, "/")) {
696 line_chunk[strlen (line_chunk) - 1] = '\0';
699 markup = g_string_append (markup, "<span");
701 g_string_append (markup, " style=\"italic\"");
703 g_string_append (markup, " weight=\"bold\"");
705 g_string_append_printf (markup, " size=\"%u\"", fontsize * 1000);
706 g_string_append_printf (markup, ">%s</span>", line_chunk);
709 g_string_append (markup, "\n");
710 line = line_split + 1;
716 g_string_free (markup, FALSE);
717 GST_DEBUG ("parse_mdvdsub returning (%f+%f): %s",
718 state->start_time / (double) GST_SECOND,
719 state->duration / (double) GST_SECOND, ret);
724 strip_trailing_newlines (gchar * txt)
730 while (len > 1 && txt[len - 1] == '\n') {
737 /* we want to escape text in general, but retain basic markup like
738 * <i></i>, <u></u>, and <b></b>. The easiest and safest way is to
739 * just unescape a white list of allowed markups again after
740 * escaping everything (the text between these simple markers isn't
741 * necessarily escaped, so it seems best to do it like this) */
743 subrip_unescape_formatting (gchar * txt, gconstpointer allowed_tags_ptr,
744 gboolean allows_tag_attributes)
748 gchar *allowed_tags_pattern, *search_pattern;
749 const gchar *replace_pattern;
751 /* No processing needed if no escaped tag marker found in the string. */
752 if (strstr (txt, "<") == NULL)
755 /* Build a list of alternates for our regexp.
756 * FIXME: Could be built once and stored */
757 allowed_tags_pattern = g_strjoinv ("|", (gchar **) allowed_tags_ptr);
758 /* Look for starting/ending escaped tags with optional attributes. */
759 search_pattern = g_strdup_printf ("<(/)?\\ *(%s)(%s)>",
760 allowed_tags_pattern, ATTRIBUTE_REGEX);
761 /* And unescape appropriately */
762 if (allows_tag_attributes) {
763 replace_pattern = "<\\1\\2\\3>";
765 replace_pattern = "<\\1\\2>";
768 tag_regex = g_regex_new (search_pattern, 0, 0, NULL);
769 res = g_regex_replace (tag_regex, txt, strlen (txt), 0,
770 replace_pattern, 0, NULL);
772 /* res will always be shorter than the input or identical, so this
777 g_free (search_pattern);
778 g_free (allowed_tags_pattern);
780 g_regex_unref (tag_regex);
785 subrip_remove_unhandled_tag (gchar * start, gchar * stop)
789 tag = start + strlen ("<");
793 if (g_ascii_tolower (*tag) < 'a' || g_ascii_tolower (*tag) > 'z')
798 GST_LOG ("removing unhandled tag '%s'", start);
800 memmove (start, stop, strlen (stop) + 1);
804 /* remove tags we haven't explicitly allowed earlier on, like font tags
807 subrip_remove_unhandled_tags (gchar * txt)
811 for (pos = txt; pos != NULL && *pos != '\0'; ++pos) {
812 if (strncmp (pos, "<", 4) == 0 && (gt = strstr (pos + 4, ">"))) {
813 if (subrip_remove_unhandled_tag (pos, gt + strlen (">")))
819 /* we only allow a fixed set of tags like <i>, <u> and <b>, so let's
820 * take a simple approach. This code assumes the input has been
821 * escaped and subrip_unescape_formatting() has then been run over the
822 * input! This function adds missing closing markup tags and removes
823 * broken closing tags for tags that have never been opened. */
825 subrip_fix_up_markup (gchar ** p_txt, gconstpointer allowed_tags_ptr)
827 gchar *cur, *next_tag;
828 GPtrArray *open_tags = NULL;
829 guint num_open_tags = 0;
830 const gchar *iter_tag;
836 GMatchInfo *match_info;
837 gchar **allowed_tags = (gchar **) allowed_tags_ptr;
839 g_assert (*p_txt != NULL);
841 open_tags = g_ptr_array_new_with_free_func (g_free);
843 while (*cur != '\0') {
844 next_tag = strchr (cur, '<');
845 if (next_tag == NULL)
849 while (index < g_strv_length (allowed_tags)) {
850 iter_tag = allowed_tags[index];
851 /* Look for a white listed tag */
852 cur_tag = g_strconcat ("<", iter_tag, ATTRIBUTE_REGEX, ">", NULL);
853 tag_regex = g_regex_new (cur_tag, 0, 0, NULL);
854 (void) g_regex_match (tag_regex, next_tag, 0, &match_info);
856 if (g_match_info_matches (match_info)) {
857 gint start_pos, end_pos;
858 gchar *word = g_match_info_fetch (match_info, 0);
859 g_match_info_fetch_pos (match_info, 0, &start_pos, &end_pos);
860 if (start_pos == 0) {
861 offset = strlen (word);
865 g_match_info_free (match_info);
866 g_regex_unref (tag_regex);
870 /* OK we found a tag, let's keep track of it */
871 g_ptr_array_add (open_tags, g_ascii_strdown (iter_tag, -1));
883 if (*next_tag == '<' && *(next_tag + 1) == '/') {
884 end_tag = strchr (cur, '>');
886 const gchar *last = NULL;
887 if (num_open_tags > 0)
888 last = g_ptr_array_index (open_tags, num_open_tags - 1);
889 if (num_open_tags == 0
890 || g_ascii_strncasecmp (end_tag - 1, last, strlen (last))) {
891 GST_LOG ("broken input, closing tag '%s' is not open", end_tag - 1);
892 memmove (next_tag, end_tag + 1, strlen (end_tag) + 1);
893 next_tag -= strlen (end_tag);
896 g_ptr_array_remove_index (open_tags, num_open_tags);
904 if (num_open_tags > 0) {
907 s = g_string_new (*p_txt);
908 while (num_open_tags > 0) {
909 GST_LOG ("adding missing closing tag '%s'",
910 (char *) g_ptr_array_index (open_tags, num_open_tags - 1));
911 g_string_append_c (s, '<');
912 g_string_append_c (s, '/');
913 g_string_append (s, g_ptr_array_index (open_tags, num_open_tags - 1));
914 g_string_append_c (s, '>');
918 *p_txt = g_string_free (s, FALSE);
920 g_ptr_array_free (open_tags, TRUE);
924 parse_subrip_time (const gchar * ts_string, GstClockTime * t)
926 gchar s[128] = { '\0', };
928 guint hour, min, sec, msec, len;
930 while (*ts_string == ' ')
933 g_strlcpy (s, ts_string, sizeof (s));
934 if ((end = strstr (s, "-->")))
938 /* ms may be in these formats:
939 * hh:mm:ss,500 = 500ms
942 * hh:mm:ss, 50 = 50ms
944 * and the same with . instead of ,.
945 * sscanf() doesn't differentiate between ' 5' and '5' so munge
946 * the white spaces within the timestamp to '0' (I'm sure there's a
947 * way to make sscanf() do this for us, but how?)
949 g_strdelimit (s, " ", '0');
950 g_strdelimit (s, ".", ',');
952 /* make sure we have exactly three digits after he comma */
955 /* If there isn't a ',' the timestamp is broken */
956 /* https://gitlab.freedesktop.org/gstreamer/gst-plugins-base/issues/532#note_100179 */
957 GST_WARNING ("failed to parse subrip timestamp string '%s'", s);
967 g_strlcat (&p[len], "0", 2);
971 GST_LOG ("parsing timestamp '%s'", s);
972 if (sscanf (s, "%u:%u:%u,%u", &hour, &min, &sec, &msec) != 4) {
973 GST_WARNING ("failed to parse subrip timestamp string '%s'", s);
977 *t = ((hour * 3600) + (min * 60) + sec) * GST_SECOND + msec * GST_MSECOND;
981 /* cue settings are part of the WebVTT specification. They are
982 * declared after the time interval in the first line of the
983 * cue. Example: 00:00:01,000 --> 00:00:02,000 D:vertical-lr A:start
984 * See also http://www.whatwg.org/specs/web-apps/current-work/webvtt.html
987 parse_webvtt_cue_settings (ParserState * state, const gchar * settings)
989 gchar **splitted_settings = g_strsplit_set (settings, " \t", -1);
991 gint16 text_position, text_size;
992 gint16 line_position;
993 gboolean vertical_found = FALSE;
994 gboolean alignment_found = FALSE;
996 while (i < g_strv_length (splitted_settings)) {
997 gboolean valid_tag = FALSE;
998 switch (splitted_settings[i][0]) {
1000 if (sscanf (splitted_settings[i], "T:%" G_GINT16_FORMAT "%%",
1001 &text_position) > 0) {
1002 state->text_position = (guint8) text_position;
1007 if (strlen (splitted_settings[i]) > 2) {
1008 vertical_found = TRUE;
1009 g_free (state->vertical);
1010 state->vertical = g_strdup (splitted_settings[i] + 2);
1015 if (g_str_has_suffix (splitted_settings[i], "%")) {
1016 if (sscanf (splitted_settings[i], "L:%" G_GINT16_FORMAT "%%",
1017 &line_position) > 0) {
1018 state->line_position = line_position;
1022 if (sscanf (splitted_settings[i], "L:%" G_GINT16_FORMAT,
1023 &line_position) > 0) {
1024 state->line_number = line_position;
1030 if (sscanf (splitted_settings[i], "S:%" G_GINT16_FORMAT "%%",
1032 state->text_size = (guint8) text_size;
1037 if (strlen (splitted_settings[i]) > 2) {
1038 g_free (state->alignment);
1039 state->alignment = g_strdup (splitted_settings[i] + 2);
1040 alignment_found = TRUE;
1048 GST_LOG ("Invalid or unrecognised setting found: %s",
1049 splitted_settings[i]);
1053 g_strfreev (splitted_settings);
1054 if (!vertical_found) {
1055 g_free (state->vertical);
1056 state->vertical = g_strdup ("");
1058 if (!alignment_found) {
1059 g_free (state->alignment);
1060 state->alignment = g_strdup ("");
1065 parse_subrip (ParserState * state, const gchar * line)
1069 switch (state->state) {
1074 /* looking for a single integer as a Cue ID, but we
1075 * don't actually use it */
1077 id = g_ascii_strtoull (line, &endptr, 10);
1078 if (id == G_MAXUINT64 && errno == ERANGE)
1080 else if (id == 0 && errno == EINVAL)
1082 else if (endptr != line && *endptr == '\0')
1088 GstClockTime ts_start, ts_end;
1091 /* looking for start_time --> end_time */
1092 if ((end_time = strstr (line, " --> ")) &&
1093 parse_subrip_time (line, &ts_start) &&
1094 parse_subrip_time (end_time + strlen (" --> "), &ts_end) &&
1095 state->start_time <= ts_end) {
1097 state->start_time = ts_start;
1098 state->duration = ts_end - ts_start;
1100 GST_DEBUG ("error parsing subrip time line '%s'", line);
1107 /* No need to parse that text if it's out of segment */
1108 guint64 clip_start = 0, clip_stop = 0;
1109 gboolean in_seg = FALSE;
1111 /* Check our segment start/stop */
1112 in_seg = gst_segment_clip (state->segment, GST_FORMAT_TIME,
1113 state->start_time, state->start_time + state->duration,
1114 &clip_start, &clip_stop);
1117 state->start_time = clip_start;
1118 state->duration = clip_stop - clip_start;
1124 /* looking for subtitle text; empty line ends this subtitle entry */
1125 if (state->buf->len)
1126 g_string_append_c (state->buf, '\n');
1127 g_string_append (state->buf, line);
1128 if (strlen (line) == 0) {
1129 ret = g_markup_escape_text (state->buf->str, state->buf->len);
1130 g_string_truncate (state->buf, 0);
1132 subrip_unescape_formatting (ret, state->allowed_tags,
1133 state->allows_tag_attributes);
1134 subrip_remove_unhandled_tags (ret);
1135 strip_trailing_newlines (ret);
1136 subrip_fix_up_markup (&ret, state->allowed_tags);
1141 g_return_val_if_reached (NULL);
1146 parse_lrc (ParserState * state, const gchar * line)
1155 if (sscanf (line, "[%u:%02u.%03u]", &m, &s, &c) != 3 &&
1156 sscanf (line, "[%u:%02u.%02u]", &m, &s, &c) != 3)
1159 start = strchr (line, ']');
1160 if (start - line == 9)
1165 state->start_time = gst_util_uint64_scale (m, 60 * GST_SECOND, 1)
1166 + gst_util_uint64_scale (s, GST_SECOND, 1)
1167 + gst_util_uint64_scale (c, milli * GST_MSECOND, 1);
1168 state->duration = GST_CLOCK_TIME_NONE;
1170 return g_strdup (start + 1);
1173 /* WebVTT is a new subtitle format for the upcoming HTML5 video track
1174 * element. This format is similar to Subrip, the biggest differences
1175 * are that there can be cue settings detailing how to display the cue
1176 * text and more markup tags are allowed.
1177 * See also http://www.whatwg.org/specs/web-apps/current-work/webvtt.html
1180 parse_webvtt (ParserState * state, const gchar * line)
1182 /* Cue IDs are optional in WebVTT, but not in subrip,
1183 * so when in state 0 (cue ID), also check if we're
1184 * already at the start --> end time marker */
1185 if (state->state == 0 || state->state == 1) {
1186 GstClockTime ts_start, ts_end;
1188 gchar *cue_settings = NULL;
1190 /* looking for start_time --> end_time */
1191 if ((end_time = strstr (line, " --> ")) &&
1192 parse_subrip_time (line, &ts_start) &&
1193 parse_subrip_time (end_time + strlen (" --> "), &ts_end) &&
1194 state->start_time <= ts_end) {
1196 state->start_time = ts_start;
1197 state->duration = ts_end - ts_start;
1198 cue_settings = strstr (end_time + strlen (" --> "), " ");
1200 GST_DEBUG ("error parsing subrip time line '%s'", line);
1204 state->text_position = 0;
1205 state->text_size = 0;
1206 state->line_position = 0;
1207 state->line_number = 0;
1210 parse_webvtt_cue_settings (state, cue_settings + 1);
1212 g_free (state->vertical);
1213 state->vertical = g_strdup ("");
1214 g_free (state->alignment);
1215 state->alignment = g_strdup ("");
1220 return parse_subrip (state, line);
1224 unescape_newlines_br (gchar * read)
1226 gchar *write = read;
1228 /* Replace all occurences of '[br]' with a newline as version 2
1229 * of the subviewer format uses this for newlines */
1231 if (read[0] == '\0' || read[1] == '\0' || read[2] == '\0' || read[3] == '\0')
1235 if (strncmp (read, "[br]", 4) == 0) {
1249 parse_subviewer (ParserState * state, const gchar * line)
1251 guint h1, m1, s1, ms1;
1252 guint h2, m2, s2, ms2;
1255 /* TODO: Maybe also parse the fields in the header, especially DELAY.
1256 * For examples see the unit test or
1257 * http://www.doom9.org/index.html?/sub.htm */
1259 switch (state->state) {
1261 /* looking for start_time,end_time */
1262 if (sscanf (line, "%u:%u:%u.%u,%u:%u:%u.%u",
1263 &h1, &m1, &s1, &ms1, &h2, &m2, &s2, &ms2) == 8) {
1266 (((guint64) h1) * 3600 + m1 * 60 + s1) * GST_SECOND +
1269 (((guint64) h2) * 3600 + m2 * 60 + s2) * GST_SECOND +
1270 ms2 * GST_MSECOND - state->start_time;
1275 /* No need to parse that text if it's out of segment */
1276 guint64 clip_start = 0, clip_stop = 0;
1277 gboolean in_seg = FALSE;
1279 /* Check our segment start/stop */
1280 in_seg = gst_segment_clip (state->segment, GST_FORMAT_TIME,
1281 state->start_time, state->start_time + state->duration,
1282 &clip_start, &clip_stop);
1285 state->start_time = clip_start;
1286 state->duration = clip_stop - clip_start;
1292 /* looking for subtitle text; empty line ends this subtitle entry */
1293 if (state->buf->len)
1294 g_string_append_c (state->buf, '\n');
1295 g_string_append (state->buf, line);
1296 if (strlen (line) == 0) {
1297 ret = g_strdup (state->buf->str);
1298 unescape_newlines_br (ret);
1299 strip_trailing_newlines (ret);
1300 g_string_truncate (state->buf, 0);
1306 g_assert_not_reached ();
1312 parse_mpsub (ParserState * state, const gchar * line)
1317 switch (state->state) {
1319 /* looking for two floats (offset, duration) */
1320 if (sscanf (line, "%f %f", &t1, &t2) == 2) {
1322 state->start_time += state->duration + GST_SECOND * t1;
1323 state->duration = GST_SECOND * t2;
1327 { /* No need to parse that text if it's out of segment */
1328 guint64 clip_start = 0, clip_stop = 0;
1329 gboolean in_seg = FALSE;
1331 /* Check our segment start/stop */
1332 in_seg = gst_segment_clip (state->segment, GST_FORMAT_TIME,
1333 state->start_time, state->start_time + state->duration,
1334 &clip_start, &clip_stop);
1337 state->start_time = clip_start;
1338 state->duration = clip_stop - clip_start;
1344 /* looking for subtitle text; empty line ends this
1346 if (state->buf->len)
1347 g_string_append_c (state->buf, '\n');
1348 g_string_append (state->buf, line);
1349 if (strlen (line) == 0) {
1350 ret = g_strdup (state->buf->str);
1351 g_string_truncate (state->buf, 0);
1357 g_assert_not_reached ();
1362 static const gchar *
1363 dks_skip_timestamp (const gchar * line)
1365 while (*line && *line != ']')
1373 parse_dks (ParserState * state, const gchar * line)
1377 switch (state->state) {
1379 /* Looking for the start time and text */
1380 if (sscanf (line, "[%u:%u:%u]", &h, &m, &s) == 3) {
1382 state->start_time = (((guint64) h) * 3600 + m * 60 + s) * GST_SECOND;
1383 text = dks_skip_timestamp (line);
1386 g_string_append (state->buf, text);
1392 guint64 clip_start = 0, clip_stop = 0;
1396 /* Looking for the end time */
1397 if (sscanf (line, "[%u:%u:%u]", &h, &m, &s) == 3) {
1399 state->duration = (((guint64) h) * 3600 + m * 60 + s) * GST_SECOND -
1402 GST_WARNING ("Failed to parse subtitle end time");
1406 /* Check if this subtitle is out of the current segment */
1407 in_seg = gst_segment_clip (state->segment, GST_FORMAT_TIME,
1408 state->start_time, state->start_time + state->duration,
1409 &clip_start, &clip_stop);
1415 state->start_time = clip_start;
1416 state->duration = clip_stop - clip_start;
1418 ret = g_strdup (state->buf->str);
1419 g_string_truncate (state->buf, 0);
1420 unescape_newlines_br (ret);
1424 g_assert_not_reached ();
1430 parser_state_init (ParserState * state)
1432 GST_DEBUG ("initialising parser");
1435 g_string_truncate (state->buf, 0);
1437 state->buf = g_string_new (NULL);
1440 state->start_time = 0;
1441 state->duration = 0;
1442 state->max_duration = 0; /* no limit */
1444 state->segment = NULL;
1446 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
1447 state->language_list = NULL;
1449 g_free(state->current_language);
1450 state->current_language = NULL;
1452 state->langlist_msg_posted = FALSE;
1457 parser_state_dispose (GstSubParse * self, ParserState * state)
1460 g_string_free (state->buf, TRUE);
1464 g_free (state->vertical);
1465 state->vertical = NULL;
1466 g_free (state->alignment);
1467 state->alignment = NULL;
1469 if (state->user_data) {
1470 switch (self->parser_type) {
1471 case GST_SUB_PARSE_FORMAT_QTTEXT:
1472 qttext_context_deinit (state);
1474 case GST_SUB_PARSE_FORMAT_SAMI:
1475 sami_context_deinit (state);
1481 state->allowed_tags = NULL;
1484 /* regex type enum */
1487 GST_SUB_PARSE_REGEX_UNKNOWN = 0,
1488 GST_SUB_PARSE_REGEX_MDVDSUB = 1,
1489 GST_SUB_PARSE_REGEX_SUBRIP = 2,
1490 GST_SUB_PARSE_REGEX_DKS = 3,
1491 GST_SUB_PARSE_REGEX_VTT = 4,
1495 gst_sub_parse_data_format_autodetect_regex_once (GstSubParseRegex regtype)
1497 gpointer result = NULL;
1498 GError *gerr = NULL;
1500 case GST_SUB_PARSE_REGEX_MDVDSUB:
1502 (gpointer) g_regex_new ("^\\{[0-9]+\\}\\{[0-9]+\\}",
1503 G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, &gerr);
1504 if (result == NULL) {
1505 g_warning ("Compilation of mdvd regex failed: %s", gerr->message);
1506 g_clear_error (&gerr);
1509 case GST_SUB_PARSE_REGEX_SUBRIP:
1511 g_regex_new ("^[\\s\\n]*[\\n]? {0,3}[ 0-9]{1,4}\\s*(\x0d)?\x0a"
1512 " ?[0-9]{1,2}: ?[0-9]{1,2}: ?[0-9]{1,2}[,.] {0,2}[0-9]{1,3}"
1513 " +--> +[0-9]{1,2}: ?[0-9]{1,2}: ?[0-9]{1,2}[,.] {0,2}[0-9]{1,2}",
1514 G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, &gerr);
1515 if (result == NULL) {
1516 g_warning ("Compilation of subrip regex failed: %s", gerr->message);
1517 g_clear_error (&gerr);
1520 case GST_SUB_PARSE_REGEX_DKS:
1521 result = (gpointer) g_regex_new ("^\\[[0-9]+:[0-9]+:[0-9]+\\].*",
1522 G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, &gerr);
1523 if (result == NULL) {
1524 g_warning ("Compilation of dks regex failed: %s", gerr->message);
1525 g_clear_error (&gerr);
1528 case GST_SUB_PARSE_REGEX_VTT:
1530 g_regex_new ("^(\\xef\\xbb\\xbf)?WEBVTT[\\xa\\xd\\x20\\x9]", 0, 0,
1532 if (result == NULL) {
1533 g_warning ("Compilation of vtt regex failed: %s", gerr->message);
1534 g_error_free (gerr);
1539 GST_WARNING ("Trying to allocate regex of unknown type %u", regtype);
1545 * FIXME: maybe we should pass along a second argument, the preceding
1546 * text buffer, because that is how this originally worked, even though
1547 * I don't really see the use of that.
1550 static GstSubParseFormat
1551 gst_sub_parse_data_format_autodetect (gchar * match_str)
1555 static GOnce mdvd_rx_once = G_ONCE_INIT;
1556 static GOnce subrip_rx_once = G_ONCE_INIT;
1557 static GOnce dks_rx_once = G_ONCE_INIT;
1558 static GOnce vtt_rx_once = G_ONCE_INIT;
1565 g_once (&mdvd_rx_once,
1566 (GThreadFunc) gst_sub_parse_data_format_autodetect_regex_once,
1567 (gpointer) GST_SUB_PARSE_REGEX_MDVDSUB);
1568 g_once (&subrip_rx_once,
1569 (GThreadFunc) gst_sub_parse_data_format_autodetect_regex_once,
1570 (gpointer) GST_SUB_PARSE_REGEX_SUBRIP);
1571 g_once (&dks_rx_once,
1572 (GThreadFunc) gst_sub_parse_data_format_autodetect_regex_once,
1573 (gpointer) GST_SUB_PARSE_REGEX_DKS);
1574 g_once (&vtt_rx_once,
1575 (GThreadFunc) gst_sub_parse_data_format_autodetect_regex_once,
1576 (gpointer) GST_SUB_PARSE_REGEX_VTT);
1578 mdvd_grx = (GRegex *) mdvd_rx_once.retval;
1579 subrip_grx = (GRegex *) subrip_rx_once.retval;
1580 dks_grx = (GRegex *) dks_rx_once.retval;
1581 vtt_grx = (GRegex *) vtt_rx_once.retval;
1583 if (g_regex_match (mdvd_grx, match_str, 0, NULL)) {
1584 GST_LOG ("MicroDVD (frame based) format detected");
1585 return GST_SUB_PARSE_FORMAT_MDVDSUB;
1587 if (g_regex_match (subrip_grx, match_str, 0, NULL)) {
1588 GST_LOG ("SubRip (time based) format detected");
1589 return GST_SUB_PARSE_FORMAT_SUBRIP;
1591 if (g_regex_match (dks_grx, match_str, 0, NULL)) {
1592 GST_LOG ("DKS (time based) format detected");
1593 return GST_SUB_PARSE_FORMAT_DKS;
1595 if (g_regex_match (vtt_grx, match_str, 0, NULL) == TRUE) {
1596 GST_LOG ("WebVTT (time based) format detected");
1597 return GST_SUB_PARSE_FORMAT_VTT;
1600 if (!strncmp (match_str, "FORMAT=TIME", 11)) {
1601 GST_LOG ("MPSub (time based) format detected");
1602 return GST_SUB_PARSE_FORMAT_MPSUB;
1604 if (strstr (match_str, "<SAMI>") != NULL ||
1605 strstr (match_str, "<sami>") != NULL) {
1606 GST_LOG ("SAMI (time based) format detected");
1607 return GST_SUB_PARSE_FORMAT_SAMI;
1609 /* we're boldly assuming the first subtitle appears within the first hour */
1610 if (sscanf (match_str, "0:%02u:%02u:", &n1, &n2) == 2 ||
1611 sscanf (match_str, "0:%02u:%02u=", &n1, &n2) == 2 ||
1612 sscanf (match_str, "00:%02u:%02u:", &n1, &n2) == 2 ||
1613 sscanf (match_str, "00:%02u:%02u=", &n1, &n2) == 2 ||
1614 sscanf (match_str, "00:%02u:%02u,%u=", &n1, &n2, &n3) == 3) {
1615 GST_LOG ("TMPlayer (time based) format detected");
1616 return GST_SUB_PARSE_FORMAT_TMPLAYER;
1618 if (sscanf (match_str, "[%u][%u]", &n1, &n2) == 2) {
1619 GST_LOG ("MPL2 (time based) format detected");
1620 return GST_SUB_PARSE_FORMAT_MPL2;
1622 if (strstr (match_str, "[INFORMATION]") != NULL) {
1623 GST_LOG ("SubViewer (time based) format detected");
1624 return GST_SUB_PARSE_FORMAT_SUBVIEWER;
1626 if (strstr (match_str, "{QTtext}") != NULL) {
1627 GST_LOG ("QTtext (time based) format detected");
1628 return GST_SUB_PARSE_FORMAT_QTTEXT;
1630 /* We assume the LRC file starts immediately */
1631 if (match_str[0] == '[') {
1632 gboolean all_lines_good = TRUE;
1636 ptr = split = g_strsplit (match_str, "\n", -1);
1637 while (*ptr && *(ptr + 1)) {
1639 gint len = strlen (str);
1641 if (sscanf (str, "[%u:%02u.%02u]", &n1, &n2, &n3) == 3 ||
1642 sscanf (str, "[%u:%02u.%03u]", &n1, &n2, &n3) == 3) {
1643 all_lines_good = TRUE;
1644 } else if (str[len - 1] == ']' && strchr (str, ':') != NULL) {
1645 all_lines_good = TRUE;
1647 all_lines_good = FALSE;
1656 return GST_SUB_PARSE_FORMAT_LRC;
1659 GST_DEBUG ("no subtitle format detected");
1660 return GST_SUB_PARSE_FORMAT_UNKNOWN;
1664 gst_sub_parse_format_autodetect (GstSubParse * self)
1667 GstSubParseFormat format;
1669 if (strlen (self->textbuf->str) < 30) {
1670 GST_DEBUG ("File too small to be a subtitles file");
1674 data = g_strndup (self->textbuf->str, 35);
1675 format = gst_sub_parse_data_format_autodetect (data);
1678 self->parser_type = format;
1679 self->subtitle_codec = gst_sub_parse_get_format_description (format);
1680 parser_state_init (&self->state);
1681 self->state.allowed_tags = NULL;
1684 case GST_SUB_PARSE_FORMAT_MDVDSUB:
1685 self->parse_line = parse_mdvdsub;
1686 return gst_caps_new_simple ("text/x-raw",
1687 "format", G_TYPE_STRING, "pango-markup", NULL);
1688 case GST_SUB_PARSE_FORMAT_SUBRIP:
1689 self->state.allowed_tags = (gpointer) allowed_srt_tags;
1690 self->state.allows_tag_attributes = FALSE;
1691 self->parse_line = parse_subrip;
1692 return gst_caps_new_simple ("text/x-raw",
1693 "format", G_TYPE_STRING, "pango-markup", NULL);
1694 case GST_SUB_PARSE_FORMAT_MPSUB:
1695 self->parse_line = parse_mpsub;
1696 return gst_caps_new_simple ("text/x-raw",
1697 "format", G_TYPE_STRING, "utf8", NULL);
1698 case GST_SUB_PARSE_FORMAT_SAMI:
1699 self->parse_line = parse_sami;
1700 sami_context_init (&self->state);
1701 return gst_caps_new_simple ("text/x-raw",
1702 "format", G_TYPE_STRING, "pango-markup", NULL);
1703 case GST_SUB_PARSE_FORMAT_TMPLAYER:
1704 self->parse_line = parse_tmplayer;
1705 self->state.max_duration = 5 * GST_SECOND;
1706 return gst_caps_new_simple ("text/x-raw",
1707 "format", G_TYPE_STRING, "utf8", NULL);
1708 case GST_SUB_PARSE_FORMAT_MPL2:
1709 self->parse_line = parse_mpl2;
1710 return gst_caps_new_simple ("text/x-raw",
1711 "format", G_TYPE_STRING, "pango-markup", NULL);
1712 case GST_SUB_PARSE_FORMAT_DKS:
1713 self->parse_line = parse_dks;
1714 return gst_caps_new_simple ("text/x-raw",
1715 "format", G_TYPE_STRING, "utf8", NULL);
1716 case GST_SUB_PARSE_FORMAT_VTT:
1717 self->state.allowed_tags = (gpointer) allowed_vtt_tags;
1718 self->state.allows_tag_attributes = TRUE;
1719 self->parse_line = parse_webvtt;
1720 return gst_caps_new_simple ("text/x-raw",
1721 "format", G_TYPE_STRING, "pango-markup", NULL);
1722 case GST_SUB_PARSE_FORMAT_SUBVIEWER:
1723 self->parse_line = parse_subviewer;
1724 return gst_caps_new_simple ("text/x-raw",
1725 "format", G_TYPE_STRING, "utf8", NULL);
1726 case GST_SUB_PARSE_FORMAT_QTTEXT:
1727 self->parse_line = parse_qttext;
1728 qttext_context_init (&self->state);
1729 return gst_caps_new_simple ("text/x-raw",
1730 "format", G_TYPE_STRING, "pango-markup", NULL);
1731 case GST_SUB_PARSE_FORMAT_LRC:
1732 self->parse_line = parse_lrc;
1733 return gst_caps_new_simple ("text/x-raw",
1734 "format", G_TYPE_STRING, "utf8", NULL);
1735 case GST_SUB_PARSE_FORMAT_UNKNOWN:
1737 GST_DEBUG ("no subtitle format detected");
1738 GST_ELEMENT_ERROR (self, STREAM, WRONG_TYPE,
1739 ("The input is not a valid/supported subtitle file"), (NULL));
1745 feed_textbuf (GstSubParse * self, GstBuffer * buf)
1749 gchar *input = NULL;
1753 discont = GST_BUFFER_IS_DISCONT (buf);
1755 if (GST_BUFFER_OFFSET_IS_VALID (buf) &&
1756 GST_BUFFER_OFFSET (buf) != self->offset) {
1757 self->offset = GST_BUFFER_OFFSET (buf);
1762 GST_INFO ("discontinuity");
1763 /* flush the parser state */
1764 parser_state_init (&self->state);
1765 g_string_truncate (self->textbuf, 0);
1766 gst_adapter_clear (self->adapter);
1767 if (self->parser_type == GST_SUB_PARSE_FORMAT_SAMI)
1768 sami_context_reset (&self->state);
1769 /* we could set a flag to make sure that the next buffer we push out also
1770 * has the DISCONT flag set, but there's no point really given that it's
1771 * subtitles which are discontinuous by nature. */
1774 self->offset += gst_buffer_get_size (buf);
1776 gst_adapter_push (self->adapter, buf);
1778 avail = gst_adapter_available (self->adapter);
1779 data = gst_adapter_map (self->adapter, avail);
1780 input = convert_encoding (self, (const gchar *) data, avail, &consumed);
1782 if (input && consumed > 0) {
1783 self->textbuf = g_string_append (self->textbuf, input);
1784 gst_adapter_unmap (self->adapter);
1785 gst_adapter_flush (self->adapter, consumed);
1787 gst_adapter_unmap (self->adapter);
1793 static GstFlowReturn
1794 handle_buffer (GstSubParse * self, GstBuffer * buf)
1796 GstFlowReturn ret = GST_FLOW_OK;
1797 GstCaps *caps = NULL;
1798 gchar *line, *subtitle;
1799 gboolean need_tags = FALSE;
1800 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
1801 GstMessage *m = NULL;
1804 if (self->first_buffer) {
1807 gst_buffer_map (buf, &map, GST_MAP_READ);
1808 self->detected_encoding = detect_encoding ((gchar *) map.data, map.size);
1809 gst_buffer_unmap (buf, &map);
1810 self->first_buffer = FALSE;
1811 self->state.fps_n = self->fps_n;
1812 self->state.fps_d = self->fps_d;
1815 feed_textbuf (self, buf);
1817 /* make sure we know the format */
1818 if (G_UNLIKELY (self->parser_type == GST_SUB_PARSE_FORMAT_UNKNOWN)) {
1819 if (!(caps = gst_sub_parse_format_autodetect (self))) {
1820 return GST_FLOW_EOS;
1822 if (!gst_pad_set_caps (self->srcpad, caps)) {
1823 gst_caps_unref (caps);
1824 return GST_FLOW_EOS;
1826 gst_caps_unref (caps);
1830 /* Push newsegment if needed */
1831 if (self->need_segment) {
1832 GST_LOG_OBJECT (self, "pushing newsegment event with %" GST_SEGMENT_FORMAT,
1835 gst_pad_push_event (self->srcpad, gst_event_new_segment (&self->segment));
1836 self->need_segment = FALSE;
1841 if (self->subtitle_codec != NULL) {
1844 tags = gst_tag_list_new (GST_TAG_SUBTITLE_CODEC, self->subtitle_codec,
1846 gst_pad_push_event (self->srcpad, gst_event_new_tag (tags));
1850 while (!self->flushing && (line = get_next_line (self))) {
1853 /* Set segment on our parser state machine */
1854 self->state.segment = &self->segment;
1855 /* Now parse the line, out of segment lines will just return NULL */
1856 GST_LOG_OBJECT (self, "State %d. Parsing line '%s'", self->state.state,
1858 subtitle = self->parse_line (&self->state, line + offset);
1860 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
1861 if (!self->state.langlist_msg_posted && self->state.language_list) {
1862 m = gst_message_new_element (GST_OBJECT_CAST (self), gst_structure_new("Ext_Sub_Language_List",
1863 "lang_list", G_TYPE_POINTER, self->state.language_list, NULL));
1865 gst_element_post_message (GST_ELEMENT_CAST (self), m);
1866 self->state.langlist_msg_posted = TRUE;
1867 GST_DEBUG_OBJECT (self, "curr lang as : %s ", GST_STR_NULL(self->state.current_language));
1871 guint subtitle_len = strlen (subtitle);
1873 /* +1 for terminating NUL character */
1874 buf = gst_buffer_new_and_alloc (subtitle_len + 1);
1876 /* copy terminating NUL character as well */
1877 gst_buffer_fill (buf, 0, subtitle, subtitle_len + 1);
1878 gst_buffer_set_size (buf, subtitle_len);
1880 GST_BUFFER_TIMESTAMP (buf) = self->state.start_time;
1881 GST_BUFFER_DURATION (buf) = self->state.duration;
1883 /* in some cases (e.g. tmplayer) we can only determine the duration
1884 * of a text chunk from the timestamp of the next text chunk; in those
1885 * cases, we probably want to limit the duration to something
1886 * reasonable, so we don't end up showing some text for e.g. 40 seconds
1887 * just because nothing else is being said during that time */
1888 if (self->state.max_duration > 0 && GST_BUFFER_DURATION_IS_VALID (buf)) {
1889 if (GST_BUFFER_DURATION (buf) > self->state.max_duration)
1890 GST_BUFFER_DURATION (buf) = self->state.max_duration;
1893 self->segment.position = self->state.start_time;
1895 GST_DEBUG_OBJECT (self, "Sending text '%s', %" GST_TIME_FORMAT " + %"
1896 GST_TIME_FORMAT, subtitle, GST_TIME_ARGS (self->state.start_time),
1897 GST_TIME_ARGS (self->state.duration));
1899 g_free (self->state.vertical);
1900 self->state.vertical = NULL;
1901 g_free (self->state.alignment);
1902 self->state.alignment = NULL;
1904 ret = gst_pad_push (self->srcpad, buf);
1906 /* move this forward (the tmplayer parser needs this) */
1907 if (self->state.duration != GST_CLOCK_TIME_NONE)
1908 self->state.start_time += self->state.duration;
1913 if (ret != GST_FLOW_OK) {
1914 GST_DEBUG_OBJECT (self, "flow: %s", gst_flow_get_name (ret));
1923 static GstFlowReturn
1924 gst_sub_parse_chain (GstPad * sinkpad, GstObject * parent, GstBuffer * buf)
1929 self = GST_SUBPARSE (parent);
1931 ret = handle_buffer (self, buf);
1937 gst_sub_parse_sink_event (GstPad * pad, GstObject * parent, GstEvent * event)
1939 GstSubParse *self = GST_SUBPARSE (parent);
1940 gboolean ret = FALSE;
1942 GST_LOG_OBJECT (self, "%s event", GST_EVENT_TYPE_NAME (event));
1944 switch (GST_EVENT_TYPE (event)) {
1945 case GST_EVENT_STREAM_GROUP_DONE:
1946 case GST_EVENT_EOS:{
1947 /* Make sure the last subrip chunk is pushed out even
1948 * if the file does not have an empty line at the end */
1949 if (self->parser_type == GST_SUB_PARSE_FORMAT_SUBRIP ||
1950 self->parser_type == GST_SUB_PARSE_FORMAT_TMPLAYER ||
1951 self->parser_type == GST_SUB_PARSE_FORMAT_MPL2 ||
1952 self->parser_type == GST_SUB_PARSE_FORMAT_QTTEXT ||
1953 self->parser_type == GST_SUB_PARSE_FORMAT_VTT) {
1954 gchar term_chars[] = { '\n', '\n', '\0' };
1955 GstBuffer *buf = gst_buffer_new_and_alloc (2 + 1);
1957 GST_DEBUG_OBJECT (self, "%s: force pushing of any remaining text",
1958 GST_EVENT_TYPE_NAME (event));
1960 gst_buffer_fill (buf, 0, term_chars, 3);
1961 gst_buffer_set_size (buf, 2);
1963 GST_BUFFER_OFFSET (buf) = self->offset;
1964 gst_sub_parse_chain (pad, parent, buf);
1966 ret = gst_pad_event_default (pad, parent, event);
1969 case GST_EVENT_SEGMENT:
1971 const GstSegment *s;
1973 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
1974 if (self->first_buffer) {
1975 if (!SUBPARSE_SEEK_TRYLOCK (self)) {
1976 /* new seeking request is in process */
1977 GST_WARNING_OBJECT (self, "ignore the old newsegment event");
1979 gst_event_unref (event);
1983 SUBPARSE_SEEK_LOCK (self);
1987 gst_event_parse_segment (event, &s);
1988 if (s->format == GST_FORMAT_TIME)
1989 gst_event_copy_segment (event, &self->segment);
1990 GST_DEBUG_OBJECT (self, "newsegment (%s)",
1991 gst_format_get_name (self->segment.format));
1993 /* if not time format, we'll either start with a 0 timestamp anyway or
1994 * it's following a seek in which case we'll have saved the requested
1995 * seek segment and don't want to overwrite it (remember that on a seek
1996 * we always just seek back to the start in BYTES format and just throw
1997 * away all text that's before the requested position; if the subtitles
1998 * come from an upstream demuxer, it won't be able to handle our BYTES
1999 * seek request and instead send us a newsegment from the seek request
2000 * it received via its video pads instead, so all is fine then too) */
2002 gst_event_unref (event);
2003 /* in either case, let's not simply discard this event;
2004 * trigger sending of the saved requested seek segment
2005 * or the one taken here from upstream */
2006 self->need_segment = TRUE;
2008 #ifdef TIZEN_FEATURE_SUBPARSE_MODIFICATION
2009 SUBPARSE_SEEK_UNLOCK (self);
2014 case GST_EVENT_FLUSH_START:
2016 self->flushing = TRUE;
2018 ret = gst_pad_event_default (pad, parent, event);
2021 case GST_EVENT_FLUSH_STOP:
2023 self->flushing = FALSE;
2025 ret = gst_pad_event_default (pad, parent, event);
2029 ret = gst_pad_event_default (pad, parent, event);
2037 static GstStateChangeReturn
2038 gst_sub_parse_change_state (GstElement * element, GstStateChange transition)
2040 GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;
2041 GstSubParse *self = GST_SUBPARSE (element);
2043 switch (transition) {
2044 case GST_STATE_CHANGE_READY_TO_PAUSED:
2045 /* format detection will init the parser state */
2047 self->parser_type = GST_SUB_PARSE_FORMAT_UNKNOWN;
2048 self->valid_utf8 = TRUE;
2049 self->first_buffer = TRUE;
2050 g_free (self->detected_encoding);
2051 self->detected_encoding = NULL;
2052 g_string_truncate (self->textbuf, 0);
2053 gst_adapter_clear (self->adapter);
2059 ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
2060 if (ret == GST_STATE_CHANGE_FAILURE)
2063 switch (transition) {
2064 case GST_STATE_CHANGE_PAUSED_TO_READY:
2065 parser_state_dispose (self, &self->state);
2066 self->parser_type = GST_SUB_PARSE_FORMAT_UNKNOWN;
2079 /* FIXME 0.11: these caps are ugly, use app/x-subtitle + type field or so;
2080 * also, give different subtitle formats really different types */
2081 static GstStaticCaps mpl2_caps =
2082 GST_STATIC_CAPS ("application/x-subtitle-mpl2");
2083 #define SUB_CAPS (gst_static_caps_get (&sub_caps))
2085 static GstStaticCaps tmp_caps =
2086 GST_STATIC_CAPS ("application/x-subtitle-tmplayer");
2087 #define TMP_CAPS (gst_static_caps_get (&tmp_caps))
2089 static GstStaticCaps sub_caps = GST_STATIC_CAPS ("application/x-subtitle");
2090 #define MPL2_CAPS (gst_static_caps_get (&mpl2_caps))
2092 static GstStaticCaps smi_caps = GST_STATIC_CAPS ("application/x-subtitle-sami");
2093 #define SAMI_CAPS (gst_static_caps_get (&smi_caps))
2095 static GstStaticCaps dks_caps = GST_STATIC_CAPS ("application/x-subtitle-dks");
2096 #define DKS_CAPS (gst_static_caps_get (&dks_caps))
2098 static GstStaticCaps vtt_caps = GST_STATIC_CAPS ("application/x-subtitle-vtt");
2099 #define VTT_CAPS (gst_static_caps_get (&vtt_caps))
2101 static GstStaticCaps qttext_caps =
2102 GST_STATIC_CAPS ("application/x-subtitle-qttext");
2103 #define QTTEXT_CAPS (gst_static_caps_get (&qttext_caps))
2105 static GstStaticCaps lrc_caps = GST_STATIC_CAPS ("application/x-subtitle-lrc");
2106 #define LRC_CAPS (gst_static_caps_get (&lrc_caps))
2109 gst_subparse_type_find (GstTypeFind * tf, gpointer private)
2111 GstSubParseFormat format;
2115 gchar *encoding = NULL;
2118 if (!(data = gst_type_find_peek (tf, 0, 129)))
2121 /* make sure string passed to _autodetect() is NUL-terminated */
2122 str = g_malloc0 (129);
2123 memcpy (str, data, 128);
2125 if ((encoding = detect_encoding (str, 128)) != NULL) {
2126 gchar *converted_str;
2130 converted_str = gst_convert_to_utf8 (str, 128, encoding, &tmp, &err);
2131 if (converted_str == NULL) {
2132 GST_DEBUG ("Encoding '%s' detected but conversion failed: %s", encoding,
2134 g_clear_error (&err);
2137 str = converted_str;
2142 /* Check if at least the first 120 chars are valid UTF8,
2143 * otherwise convert as always */
2144 if (!g_utf8_validate (str, 128, &end) && (end - str) < 120) {
2145 gchar *converted_str;
2149 enc = g_getenv ("GST_SUBTITLE_ENCODING");
2150 if (enc == NULL || *enc == '\0') {
2151 /* if local encoding is UTF-8 and no encoding specified
2152 * via the environment variable, assume ISO-8859-15 */
2153 if (g_get_charset (&enc)) {
2154 enc = "ISO-8859-15";
2157 converted_str = gst_convert_to_utf8 (str, 128, enc, &tmp, NULL);
2158 if (converted_str != NULL) {
2160 str = converted_str;
2164 format = gst_sub_parse_data_format_autodetect (str);
2168 case GST_SUB_PARSE_FORMAT_MDVDSUB:
2169 GST_DEBUG ("MicroDVD format detected");
2172 case GST_SUB_PARSE_FORMAT_SUBRIP:
2173 GST_DEBUG ("SubRip format detected");
2176 case GST_SUB_PARSE_FORMAT_MPSUB:
2177 GST_DEBUG ("MPSub format detected");
2180 case GST_SUB_PARSE_FORMAT_SAMI:
2181 GST_DEBUG ("SAMI (time-based) format detected");
2184 case GST_SUB_PARSE_FORMAT_TMPLAYER:
2185 GST_DEBUG ("TMPlayer (time based) format detected");
2188 /* FIXME: our MPL2 typefinding is not really good enough to warrant
2189 * returning a high probability (however, since we registered our
2190 * typefinder here with a rank of MARGINAL we should pretty much only
2191 * be called if most other typefinders have already run */
2192 case GST_SUB_PARSE_FORMAT_MPL2:
2193 GST_DEBUG ("MPL2 (time based) format detected");
2196 case GST_SUB_PARSE_FORMAT_SUBVIEWER:
2197 GST_DEBUG ("SubViewer format detected");
2200 case GST_SUB_PARSE_FORMAT_DKS:
2201 GST_DEBUG ("DKS format detected");
2204 case GST_SUB_PARSE_FORMAT_QTTEXT:
2205 GST_DEBUG ("QTtext format detected");
2208 case GST_SUB_PARSE_FORMAT_LRC:
2209 GST_DEBUG ("LRC format detected");
2212 case GST_SUB_PARSE_FORMAT_VTT:
2213 GST_DEBUG ("WebVTT format detected");
2217 case GST_SUB_PARSE_FORMAT_UNKNOWN:
2218 GST_DEBUG ("no subtitle format detected");
2222 /* if we're here, it's ok */
2223 gst_type_find_suggest (tf, GST_TYPE_FIND_MAXIMUM, caps);
2227 plugin_init (GstPlugin * plugin)
2229 GST_DEBUG_CATEGORY_INIT (sub_parse_debug, "subparse", 0, ".sub parser");
2231 if (!gst_type_find_register (plugin, "subparse_typefind", GST_RANK_MARGINAL,
2232 gst_subparse_type_find, "srt,sub,mpsub,mdvd,smi,txt,dks,vtt",
2233 SUB_CAPS, NULL, NULL))
2236 if (!gst_element_register (plugin, "subparse",
2237 GST_RANK_PRIMARY, GST_TYPE_SUBPARSE) ||
2238 !gst_element_register (plugin, "ssaparse",
2239 GST_RANK_PRIMARY, GST_TYPE_SSA_PARSE)) {
2246 GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
2250 plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)