From cdcc28c833e0a493bceac05428e0c46191872069 Mon Sep 17 00:00:00 2001 From: Thiago Santos Date: Tue, 1 Dec 2009 13:13:24 -0300 Subject: [PATCH] subparse: add qttext support Adds basic support for qttext subtitles, still lacks markup tags to make it prettier, but the plain text already works. Implemented according to: http://www.apple.com/quicktime/tutorials/texttracks.html http://www.apple.com/quicktime/tutorials/textdescriptors.html Fixes #603357 --- gst/subparse/Makefile.am | 7 +- gst/subparse/gstsubparse.c | 53 +++++++++--- gst/subparse/gstsubparse.h | 3 +- gst/subparse/qttextparse.c | 195 +++++++++++++++++++++++++++++++++++++++++++++ gst/subparse/qttextparse.h | 36 +++++++++ 5 files changed, 282 insertions(+), 12 deletions(-) create mode 100644 gst/subparse/qttextparse.c create mode 100644 gst/subparse/qttextparse.h diff --git a/gst/subparse/Makefile.am b/gst/subparse/Makefile.am index 1227c26..4a33bd4 100644 --- a/gst/subparse/Makefile.am +++ b/gst/subparse/Makefile.am @@ -15,7 +15,9 @@ libgstsubparse_la_SOURCES = \ tmplayerparse.c \ tmplayerparse.h \ mpl2parse.c \ - mpl2parse.h + mpl2parse.h \ + qttextparse.c \ + qttextparse.h libgstsubparse_la_CFLAGS = $(GST_CFLAGS) $(GST_BASE_CFLAGS) libgstsubparse_la_LDFLAGS = $(GST_PLUGIN_LDFLAGS) @@ -27,4 +29,5 @@ noinst_HEADERS = \ gstsubparse.h \ samiparse.h \ tmplayerparse.h \ - mpl2parse.h + mpl2parse.h \ + qttextparse.h diff --git a/gst/subparse/gstsubparse.c b/gst/subparse/gstsubparse.c index 41a1c7e..dc26f65 100644 --- a/gst/subparse/gstsubparse.c +++ b/gst/subparse/gstsubparse.c @@ -34,6 +34,7 @@ #include "samiparse.h" #include "tmplayerparse.h" #include "mpl2parse.h" +#include "qttextparse.h" GST_DEBUG_CATEGORY (sub_parse_debug); @@ -67,14 +68,15 @@ static GstStaticPadTemplate sink_templ = GST_STATIC_PAD_TEMPLATE ("sink", GST_PAD_ALWAYS, GST_STATIC_CAPS ("application/x-subtitle; application/x-subtitle-sami; " "application/x-subtitle-tmplayer; application/x-subtitle-mpl2; " - "application/x-subtitle-dks") + "application/x-subtitle-dks; application/x-subtitle-qttext") ); #else static GstStaticPadTemplate sink_templ = GST_STATIC_PAD_TEMPLATE ("sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS ("application/x-subtitle; application/x-subtitle-dks; " - "application/x-subtitle-tmplayer; application/x-subtitle-mpl2") + "application/x-subtitle-tmplayer; application/x-subtitle-mpl2; " + "application/x-subtitle-qttext") ); #endif @@ -143,6 +145,19 @@ gst_sub_parse_dispose (GObject * object) GST_DEBUG_OBJECT (subparse, "cleaning up subtitle parser"); + switch (subparse->parser_type) { + case GST_SUB_PARSE_FORMAT_QTTEXT: + qttext_context_deinit (&subparse->state); + break; +#ifndef GST_DISABLE_XML + case GST_SUB_PARSE_FORMAT_SAMI: + sami_context_deinit (&subparse->state); + break; +#endif + default: + break; + } + if (subparse->encoding) { g_free (subparse->encoding); subparse->encoding = NULL; @@ -162,10 +177,6 @@ gst_sub_parse_dispose (GObject * object) g_string_free (subparse->textbuf, TRUE); subparse->textbuf = NULL; } -#ifndef GST_DISABLE_XML - if (subparse->parser_type == GST_SUB_PARSE_FORMAT_SAMI) - sami_context_deinit (&subparse->state); -#endif GST_CALL_PARENT (G_OBJECT_CLASS, dispose, (object)); } @@ -422,6 +433,8 @@ gst_sub_parse_get_format_description (GstSubParseFormat format) return "SubViewer"; case GST_SUB_PARSE_FORMAT_DKS: return "DKS"; + case GST_SUB_PARSE_FORMAT_QTTEXT: + return "QTtext"; default: case GST_SUB_PARSE_FORMAT_UNKNOWN: break; @@ -1196,11 +1209,17 @@ parser_state_dispose (GstSubParse * self, ParserState * state) g_string_free (state->buf, TRUE); state->buf = NULL; } + if (state->user_data) { + switch (self->parser_type) { #ifndef GST_DISABLE_XML - if (state->user_data && self->parser_type == GST_SUB_PARSE_FORMAT_SAMI) { - sami_context_reset (state); - } + case GST_SUB_PARSE_FORMAT_SAMI: + sami_context_reset (state); + break; #endif + default: + break; + } + } } /* regex type enum */ @@ -1324,6 +1343,10 @@ gst_sub_parse_data_format_autodetect (gchar * match_str) GST_LOG ("SubViewer (time based) format detected"); return GST_SUB_PARSE_FORMAT_SUBVIEWER; } + if (strstr (match_str, "{QTtext}") != NULL) { + GST_LOG ("QTtext (time based) format detected"); + return GST_SUB_PARSE_FORMAT_QTTEXT; + } GST_DEBUG ("no subtitle format detected"); return GST_SUB_PARSE_FORMAT_UNKNOWN; @@ -1377,6 +1400,10 @@ gst_sub_parse_format_autodetect (GstSubParse * self) case GST_SUB_PARSE_FORMAT_SUBVIEWER: self->parse_line = parse_subviewer; return gst_caps_new_simple ("text/plain", NULL); + case GST_SUB_PARSE_FORMAT_QTTEXT: + self->parse_line = parse_qttext; + qttext_context_init (&self->state); + return gst_caps_new_simple ("text/x-pango-markup", NULL); case GST_SUB_PARSE_FORMAT_UNKNOWN: default: GST_DEBUG ("no subtitle format detected"); @@ -1706,6 +1733,10 @@ static GstStaticCaps smi_caps = GST_STATIC_CAPS ("application/x-subtitle-sami"); static GstStaticCaps dks_caps = GST_STATIC_CAPS ("application/x-subtitle-dks"); #define DKS_CAPS (gst_static_caps_get (&dks_caps)) +static GstStaticCaps qttext_caps = +GST_STATIC_CAPS ("application/x-subtitle-qttext"); +#define QTTEXT_CAPS (gst_static_caps_get (&qttext_caps)) + static void gst_subparse_type_find (GstTypeFind * tf, gpointer private) { @@ -1811,6 +1842,10 @@ gst_subparse_type_find (GstTypeFind * tf, gpointer private) GST_DEBUG ("DKS format detected"); caps = DKS_CAPS; break; + case GST_SUB_PARSE_FORMAT_QTTEXT: + GST_DEBUG ("QTtext format detected"); + caps = QTTEXT_CAPS; + break; default: case GST_SUB_PARSE_FORMAT_UNKNOWN: GST_DEBUG ("no subtitle format detected"); diff --git a/gst/subparse/gstsubparse.h b/gst/subparse/gstsubparse.h index 13924d3..5731d91 100644 --- a/gst/subparse/gstsubparse.h +++ b/gst/subparse/gstsubparse.h @@ -54,7 +54,8 @@ typedef enum GST_SUB_PARSE_FORMAT_TMPLAYER = 5, GST_SUB_PARSE_FORMAT_MPL2 = 6, GST_SUB_PARSE_FORMAT_SUBVIEWER = 7, - GST_SUB_PARSE_FORMAT_DKS = 8 + GST_SUB_PARSE_FORMAT_DKS = 8, + GST_SUB_PARSE_FORMAT_QTTEXT = 9 } GstSubParseFormat; typedef struct { diff --git a/gst/subparse/qttextparse.c b/gst/subparse/qttextparse.c new file mode 100644 index 0000000..c056940 --- /dev/null +++ b/gst/subparse/qttextparse.c @@ -0,0 +1,195 @@ +/* GStreamer QTtext subtitle parser + * Copyright (c) 2009 Thiago Santos > + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "qttextparse.h" + +#include + +#define MIN_TO_NSEC (60 * GST_SECOND) +#define HOUR_TO_NSEC (60 * MIN_TO_NSEC) + +#define GST_QTTEXT_CONTEXT(state) ((GstQTTextContext *) (state)->user_data) + +typedef struct _GstQTTextContext GstQTTextContext; + +struct _GstQTTextContext +{ + /* timing variables */ + gint timescale; + gboolean absolute; + guint64 start_time; + +}; + +void +qttext_context_init (ParserState * state) +{ + GstQTTextContext *context; + + state->user_data = g_new0 (GstQTTextContext, 1); + + context = GST_QTTEXT_CONTEXT (state); + + /* we use 1000 as a default */ + context->timescale = 1000; + context->absolute = TRUE; +} + +void +qttext_context_deinit (ParserState * state) +{ + g_free (state->user_data); + state->user_data = NULL; +} + +static gboolean +qttext_parse_tag (ParserState * state, const gchar * line, gint * index) +{ + gchar *next; + gint next_index; + + g_assert (line[*index] == '{'); + + next = strchr (line + *index, '}'); + if (next == NULL) { + goto error_out; + } else { + next_index = 1 + (next - line); + } + g_assert (line[next_index - 1] == '}'); + + *index = *index + 1; /* skip the { */ + + /* now identify our tag */ + if (strncmp (line + *index, "QTtext", 6) == 0) { + /* NOP */ + } else { + GST_WARNING ("Unused qttext tag starting at: %s", line + *index); + } + + *index = next_index; + return TRUE; + +error_out: + { + GST_WARNING ("Failed to parse qttext tag at line %s", line); + return FALSE; + } +} + +static guint64 +qttext_parse_timestamp (ParserState * state, const gchar * line, gint index) +{ + int ret; + gint hour, min, sec, dec; + GstQTTextContext *context = GST_QTTEXT_CONTEXT (state); + + ret = sscanf (line + index, "[%d:%d:%d.%d]", &hour, &min, &sec, &dec); + if (ret != 3 && ret != 4) { + /* bad timestamp */ + GST_WARNING ("Bad qttext timestamp found: %s", line); + return 0; + } + + if (ret == 3) { + /* be forgiving for missing decimal part */ + dec = 0; + } + + /* parse the decimal part according to the timescale */ + g_assert (context->timescale != 0); + dec = (GST_SECOND * dec) / context->timescale; + + /* return the result */ + return hour * HOUR_TO_NSEC + min * MIN_TO_NSEC + sec * GST_SECOND + dec; +} + +static void +qttext_prepare_text (ParserState * state) +{ + if (state->buf == NULL) { + state->buf = g_string_sized_new (256); /* this should be enough */ + } else { + g_string_append (state->buf, "\n"); + } + + /* TODO add the pango markup */ +} + +static void +qttext_parse_text (ParserState * state, const gchar * line, gint index) +{ + qttext_prepare_text (state); + g_string_append (state->buf, line + index); +} + +gchar * +parse_qttext (ParserState * state, const gchar * line) +{ + gint i; + guint64 ts; + gchar *ret = NULL; + GstQTTextContext *context = GST_QTTEXT_CONTEXT (state); + + i = 0; + while (line[i] != '\0') { + /* find first interesting character from 'i' onwards */ + + if (line[i] == '{') { + /* this is a tag, parse it */ + if (!qttext_parse_tag (state, line, &i)) { + break; + } + } else if (line[i] == '[') { + /* this is a time, convert it to a timestamp */ + ts = qttext_parse_timestamp (state, line, i); + + /* check if we have pending text to send, in case we prepare it */ + if (state->buf) { + ret = g_string_free (state->buf, FALSE); + if (context->absolute) + state->duration = ts - context->start_time; + else + state->duration = ts; + state->start_time = context->start_time; + } + state->buf = NULL; + + if (ts == 0) { + /* this is an error */ + } else { + if (context->absolute) + context->start_time = ts; + else + context->start_time += ts; + } + + /* we assume there is nothing else on this line */ + break; + + } else if (line[i] == ' ' || line[i] == '\t') { + i++; /* NOP */ + } else { + /* this is the actual text, output the rest of the line as it */ + qttext_parse_text (state, line, i); + break; + } + } + return ret; +} diff --git a/gst/subparse/qttextparse.h b/gst/subparse/qttextparse.h new file mode 100644 index 0000000..6d4f393 --- /dev/null +++ b/gst/subparse/qttextparse.h @@ -0,0 +1,36 @@ +/* GStreamer QTtext subtitle parser + * Copyright (c) 2009 Thiago Santos + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef _QTTEXT_PARSE_H_ +#define _QTTEXT_PARSE_H_ + +#include "gstsubparse.h" + +G_BEGIN_DECLS + +gchar * parse_qttext (ParserState * state, const gchar * line); + +void qttext_context_init (ParserState * state); + +void qttext_context_deinit (ParserState * state); + +G_END_DECLS + +#endif /* _QTTEXT_PARSE_H_ */ + -- 2.7.4