summaryrefslogtreecommitdiff
path: root/ext/ttml
diff options
context:
space:
mode:
authorChris Bass <floobleflam@gmail.com>2016-06-29 09:58:38 +0100
committerSebastian Dröge <sebastian@centricular.com>2016-11-01 20:46:46 +0200
commitd82ae6949feb100b74ca3781a57cbe19ce38787c (patch)
tree95943453fcebb31a768757f296defcf2a6ab243c /ext/ttml
parent280b4ac2ffc63908a74944f50589b2630be16232 (diff)
downloadgstreamer-plugins-bad-d82ae6949feb100b74ca3781a57cbe19ce38787c.tar.gz
ttml: Add plugin that supports TTML subtitles
Add a parser (ttmlparse) and renderer (ttmlrender) element that handle subtitles that use the EBU-TT-D profile of TTML1. https://bugzilla.gnome.org/show_bug.cgi?id=758232
Diffstat (limited to 'ext/ttml')
-rw-r--r--ext/ttml/Makefile.am36
-rw-r--r--ext/ttml/gstttmlparse.c570
-rw-r--r--ext/ttml/gstttmlparse.h76
-rw-r--r--ext/ttml/gstttmlplugin.c53
-rw-r--r--ext/ttml/gstttmlrender.c2456
-rw-r--r--ext/ttml/gstttmlrender.h124
-rw-r--r--ext/ttml/subtitle.c312
-rw-r--r--ext/ttml/subtitle.h592
-rw-r--r--ext/ttml/subtitlemeta.c101
-rw-r--r--ext/ttml/subtitlemeta.h66
-rw-r--r--ext/ttml/ttmlparse.c1813
-rw-r--r--ext/ttml/ttmlparse.h34
12 files changed, 6233 insertions, 0 deletions
diff --git a/ext/ttml/Makefile.am b/ext/ttml/Makefile.am
new file mode 100644
index 000000000..ce7a197ff
--- /dev/null
+++ b/ext/ttml/Makefile.am
@@ -0,0 +1,36 @@
+plugin_LTLIBRARIES = libgstttmlsubs.la
+
+# sources used to compile this plug-in
+libgstttmlsubs_la_SOURCES = \
+ subtitle.c \
+ subtitlemeta.c \
+ gstttmlparse.c \
+ gstttmlparse.h \
+ ttmlparse.c \
+ ttmlparse.h \
+ gstttmlrender.c \
+ gstttmlplugin.c
+
+# compiler and linker flags used to compile this plugin, set in configure.ac
+libgstttmlsubs_la_CFLAGS = \
+ $(GST_PLUGINS_BASE_CFLAGS) \
+ $(GST_BASE_CFLAGS) \
+ $(GST_CFLAGS) \
+ $(TTML_CFLAGS)
+
+libgstttmlsubs_la_LIBADD = \
+ $(GST_BASE_LIBS) \
+ $(GST_LIBS) \
+ -lgstvideo-$(GST_API_VERSION) \
+ $(TTML_LIBS)
+
+libgstttmlsubs_la_LDFLAGS = $(GST_PLUGIN_LDFLAGS)
+libgstttmlsubs_la_LIBTOOLFLAGS = $(GST_PLUGIN_LIBTOOLFLAGS)
+
+# headers we need but don't want installed
+noinst_HEADERS = \
+ subtitle.h \
+ subtitlemeta.h \
+ gstttmlparse.h \
+ ttmlparse.h \
+ gstttmlrender.h
diff --git a/ext/ttml/gstttmlparse.c b/ext/ttml/gstttmlparse.c
new file mode 100644
index 000000000..3daeb6efd
--- /dev/null
+++ b/ext/ttml/gstttmlparse.c
@@ -0,0 +1,570 @@
+/* GStreamer
+ * Copyright (C) <1999> Erik Walthinsen <omega@cse.ogi.edu>
+ * Copyright (C) 2004 Ronald S. Bultje <rbultje@ronald.bitfreak.net>
+ * Copyright (C) 2006 Tim-Philipp Müller <tim centricular net>
+ * Copyright (C) <2015> British Broadcasting Corporation <dash@rd.bbc.co.uk>
+ *
+ * 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., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * SECTION:element-ttmlparse
+ *
+ * Parses timed text subtitle files described using Timed Text Markup Language
+ * (TTML). Currently, only the EBU-TT-D profile of TTML, designed for
+ * distribution of subtitles over IP, is supported.
+ *
+ * The parser outputs a #GstBuffer for each scene in the input TTML file, a
+ * scene being a period of time during which a static set of subtitles should
+ * be visible. The parser places each text element within a scene into its own
+ * #GstMemory within the scene's buffer, and attaches metadata to the buffer
+ * describing the styling and layout associated with all the contained text
+ * elements. A downstream renderer element uses this information to correctly
+ * render the text on top of video frames.
+ *
+ * <refsect2>
+ * <title>Example launch lines</title>
+ * |[
+ * 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
+ * ]| Parse and render TTML subtitles contained in a single XML file over an
+ * MP4 stream containing H.264 video and AAC audio.
+ * </refsect2>
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <glib.h>
+
+#include "gstttmlparse.h"
+#include "ttmlparse.h"
+
+GST_DEBUG_CATEGORY_EXTERN (ttmlparse_debug);
+#define GST_CAT_DEFAULT ttmlparse_debug
+
+#define DEFAULT_ENCODING NULL
+
+static GstStaticPadTemplate sink_templ = GST_STATIC_PAD_TEMPLATE ("sink",
+ GST_PAD_SINK,
+ GST_PAD_ALWAYS,
+ GST_STATIC_CAPS ("application/ttml+xml")
+ );
+
+static GstStaticPadTemplate src_templ = GST_STATIC_PAD_TEMPLATE ("src",
+ GST_PAD_SRC,
+ GST_PAD_ALWAYS,
+ GST_STATIC_CAPS ("text/x-raw(meta:GstSubtitleMeta)")
+ );
+
+static gboolean gst_ttml_parse_src_event (GstPad * pad, GstObject * parent,
+ GstEvent * event);
+static gboolean gst_ttml_parse_src_query (GstPad * pad, GstObject * parent,
+ GstQuery * query);
+static gboolean gst_ttml_parse_sink_event (GstPad * pad, GstObject * parent,
+ GstEvent * event);
+
+static GstStateChangeReturn gst_ttml_parse_change_state (GstElement * element,
+ GstStateChange transition);
+
+static GstFlowReturn gst_ttml_parse_chain (GstPad * sinkpad, GstObject * parent,
+ GstBuffer * buf);
+
+#define gst_ttml_parse_parent_class parent_class
+G_DEFINE_TYPE (GstTtmlParse, gst_ttml_parse, GST_TYPE_ELEMENT);
+
+static void
+gst_ttml_parse_dispose (GObject * object)
+{
+ GstTtmlParse *ttmlparse = GST_TTML_PARSE (object);
+
+ GST_DEBUG_OBJECT (ttmlparse, "cleaning up subtitle parser");
+
+ g_free (ttmlparse->encoding);
+ ttmlparse->encoding = NULL;
+
+ g_free (ttmlparse->detected_encoding);
+ ttmlparse->detected_encoding = NULL;
+
+ if (ttmlparse->adapter) {
+ g_object_unref (ttmlparse->adapter);
+ ttmlparse->adapter = NULL;
+ }
+
+ if (ttmlparse->textbuf) {
+ g_string_free (ttmlparse->textbuf, TRUE);
+ ttmlparse->textbuf = NULL;
+ }
+
+ GST_CALL_PARENT (G_OBJECT_CLASS, dispose, (object));
+}
+
+static void
+gst_ttml_parse_class_init (GstTtmlParseClass * klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
+
+ object_class->dispose = gst_ttml_parse_dispose;
+
+ gst_element_class_add_pad_template (element_class,
+ gst_static_pad_template_get (&sink_templ));
+ gst_element_class_add_pad_template (element_class,
+ gst_static_pad_template_get (&src_templ));
+ gst_element_class_set_static_metadata (element_class,
+ "TTML subtitle parser", "Codec/Parser/Subtitle",
+ "Parses TTML subtitle files",
+ "GStreamer maintainers <gstreamer-devel@lists.sourceforge.net>, "
+ "Chris Bass <dash@rd.bbc.co.uk>");
+
+ element_class->change_state = gst_ttml_parse_change_state;
+}
+
+static void
+gst_ttml_parse_init (GstTtmlParse * ttmlparse)
+{
+ ttmlparse->sinkpad = gst_pad_new_from_static_template (&sink_templ, "sink");
+ gst_pad_set_chain_function (ttmlparse->sinkpad,
+ GST_DEBUG_FUNCPTR (gst_ttml_parse_chain));
+ gst_pad_set_event_function (ttmlparse->sinkpad,
+ GST_DEBUG_FUNCPTR (gst_ttml_parse_sink_event));
+ gst_element_add_pad (GST_ELEMENT (ttmlparse), ttmlparse->sinkpad);
+
+ ttmlparse->srcpad = gst_pad_new_from_static_template (&src_templ, "src");
+ gst_pad_set_event_function (ttmlparse->srcpad,
+ GST_DEBUG_FUNCPTR (gst_ttml_parse_src_event));
+ gst_pad_set_query_function (ttmlparse->srcpad,
+ GST_DEBUG_FUNCPTR (gst_ttml_parse_src_query));
+ gst_element_add_pad (GST_ELEMENT (ttmlparse), ttmlparse->srcpad);
+
+ ttmlparse->textbuf = g_string_new (NULL);
+ gst_segment_init (&ttmlparse->segment, GST_FORMAT_TIME);
+ ttmlparse->need_segment = TRUE;
+ ttmlparse->encoding = g_strdup (DEFAULT_ENCODING);
+ ttmlparse->detected_encoding = NULL;
+ ttmlparse->adapter = gst_adapter_new ();
+}
+
+/*
+ * Source pad functions.
+ */
+static gboolean
+gst_ttml_parse_src_query (GstPad * pad, GstObject * parent, GstQuery * query)
+{
+ GstTtmlParse *self = GST_TTML_PARSE (parent);
+ gboolean ret = FALSE;
+
+ GST_DEBUG ("Handling %s query", GST_QUERY_TYPE_NAME (query));
+
+ switch (GST_QUERY_TYPE (query)) {
+ case GST_QUERY_POSITION:{
+ GstFormat fmt;
+
+ gst_query_parse_position (query, &fmt, NULL);
+ if (fmt != GST_FORMAT_TIME) {
+ ret = gst_pad_peer_query (self->sinkpad, query);
+ } else {
+ ret = TRUE;
+ gst_query_set_position (query, GST_FORMAT_TIME, self->segment.position);
+ }
+ break;
+ }
+ case GST_QUERY_SEEKING:
+ {
+ GstFormat fmt;
+ gboolean seekable = FALSE;
+
+ ret = TRUE;
+
+ gst_query_parse_seeking (query, &fmt, NULL, NULL, NULL);
+ if (fmt == GST_FORMAT_TIME) {
+ GstQuery *peerquery = gst_query_new_seeking (GST_FORMAT_BYTES);
+
+ seekable = gst_pad_peer_query (self->sinkpad, peerquery);
+ if (seekable)
+ gst_query_parse_seeking (peerquery, NULL, &seekable, NULL, NULL);
+ gst_query_unref (peerquery);
+ }
+
+ gst_query_set_seeking (query, fmt, seekable, seekable ? 0 : -1, -1);
+ break;
+ }
+ default:
+ ret = gst_pad_query_default (pad, parent, query);
+ break;
+ }
+
+ return ret;
+}
+
+static gboolean
+gst_ttml_parse_src_event (GstPad * pad, GstObject * parent, GstEvent * event)
+{
+ GstTtmlParse *self = GST_TTML_PARSE (parent);
+ gboolean ret = FALSE;
+
+ GST_DEBUG ("Handling %s event", GST_EVENT_TYPE_NAME (event));
+
+ switch (GST_EVENT_TYPE (event)) {
+ case GST_EVENT_SEEK:
+ {
+ GstFormat format;
+ GstSeekFlags flags;
+ GstSeekType start_type, stop_type;
+ gint64 start, stop;
+ gdouble rate;
+ gboolean update;
+
+ gst_event_parse_seek (event, &rate, &format, &flags,
+ &start_type, &start, &stop_type, &stop);
+
+ if (format != GST_FORMAT_TIME) {
+ GST_WARNING_OBJECT (self, "we only support seeking in TIME format");
+ gst_event_unref (event);
+ goto beach;
+ }
+
+ /* Convert that seek to a seeking in bytes at position 0,
+ FIXME: could use an index */
+ ret = gst_pad_push_event (self->sinkpad,
+ gst_event_new_seek (rate, GST_FORMAT_BYTES, flags,
+ GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_NONE, 0));
+
+ if (ret) {
+ /* Apply the seek to our segment */
+ gst_segment_do_seek (&self->segment, rate, format, flags,
+ start_type, start, stop_type, stop, &update);
+
+ GST_DEBUG_OBJECT (self, "segment after seek: %" GST_SEGMENT_FORMAT,
+ &self->segment);
+
+ self->need_segment = TRUE;
+ } else {
+ GST_WARNING_OBJECT (self, "seek to 0 bytes failed");
+ }
+
+ gst_event_unref (event);
+ break;
+ }
+ default:
+ ret = gst_pad_event_default (pad, parent, event);
+ break;
+ }
+
+beach:
+ return ret;
+}
+
+static gchar *
+gst_convert_to_utf8 (const gchar * str, gsize len, const gchar * encoding,
+ gsize * consumed, GError ** err)
+{
+ gchar *ret = NULL;
+
+ *consumed = 0;
+ /* The char cast is necessary in glib < 2.24 */
+ ret =
+ g_convert_with_fallback (str, len, "UTF-8", encoding, (char *) "*",
+ consumed, NULL, err);
+ if (ret == NULL)
+ return ret;
+
+ /* + 3 to skip UTF-8 BOM if it was added */
+ len = strlen (ret);
+ if (len >= 3 && (guint8) ret[0] == 0xEF && (guint8) ret[1] == 0xBB
+ && (guint8) ret[2] == 0xBF)
+ memmove (ret, ret + 3, len + 1 - 3);
+
+ return ret;
+}
+
+static gchar *
+detect_encoding (const gchar * str, gsize len)
+{
+ if (len >= 3 && (guint8) str[0] == 0xEF && (guint8) str[1] == 0xBB
+ && (guint8) str[2] == 0xBF)
+ return g_strdup ("UTF-8");
+
+ if (len >= 2 && (guint8) str[0] == 0xFE && (guint8) str[1] == 0xFF)
+ return g_strdup ("UTF-16BE");
+
+ if (len >= 2 && (guint8) str[0] == 0xFF && (guint8) str[1] == 0xFE)
+ return g_strdup ("UTF-16LE");
+
+ if (len >= 4 && (guint8) str[0] == 0x00 && (guint8) str[1] == 0x00
+ && (guint8) str[2] == 0xFE && (guint8) str[3] == 0xFF)
+ return g_strdup ("UTF-32BE");
+
+ if (len >= 4 && (guint8) str[0] == 0xFF && (guint8) str[1] == 0xFE
+ && (guint8) str[2] == 0x00 && (guint8) str[3] == 0x00)
+ return g_strdup ("UTF-32LE");
+
+ return NULL;
+}
+
+static gchar *
+convert_encoding (GstTtmlParse * self, const gchar * str, gsize len,
+ gsize * consumed)
+{
+ const gchar *encoding;
+ GError *err = NULL;
+ gchar *ret = NULL;
+
+ *consumed = 0;
+
+ /* First try any detected encoding */
+ if (self->detected_encoding) {
+ ret =
+ gst_convert_to_utf8 (str, len, self->detected_encoding, consumed, &err);
+
+ if (!err)
+ return ret;
+
+ GST_WARNING_OBJECT (self, "could not convert string from '%s' to UTF-8: %s",
+ self->detected_encoding, err->message);
+ g_free (self->detected_encoding);
+ self->detected_encoding = NULL;
+ g_error_free (err);
+ }
+
+ /* Otherwise check if it's UTF8 */
+ if (self->valid_utf8) {
+ if (g_utf8_validate (str, len, NULL)) {
+ GST_LOG_OBJECT (self, "valid UTF-8, no conversion needed");
+ *consumed = len;
+ return g_strndup (str, len);
+ }
+ GST_INFO_OBJECT (self, "invalid UTF-8!");
+ self->valid_utf8 = FALSE;
+ }
+
+ /* Else try fallback */
+ encoding = self->encoding;
+ if (encoding == NULL || *encoding == '\0') {
+ /* if local encoding is UTF-8 and no encoding specified
+ * via the environment variable, assume ISO-8859-15 */
+ if (g_get_charset (&encoding)) {
+ encoding = "ISO-8859-15";
+ }
+ }
+
+ ret = gst_convert_to_utf8 (str, len, encoding, consumed, &err);
+
+ if (err) {
+ GST_WARNING_OBJECT (self, "could not convert string from '%s' to UTF-8: %s",
+ encoding, err->message);
+ g_error_free (err);
+
+ /* invalid input encoding, fall back to ISO-8859-15 (always succeeds) */
+ ret = gst_convert_to_utf8 (str, len, "ISO-8859-15", consumed, NULL);
+ }
+
+ GST_LOG_OBJECT (self,
+ "successfully converted %" G_GSIZE_FORMAT " characters from %s to UTF-8"
+ "%s", len, encoding, (err) ? " , using ISO-8859-15 as fallback" : "");
+
+ return ret;
+}
+
+static GstCaps *
+gst_ttml_parse_get_src_caps (GstTtmlParse * self)
+{
+ GstCaps *caps;
+ GstCapsFeatures *features = gst_caps_features_new ("meta:GstSubtitleMeta",
+ NULL);
+
+ caps = gst_caps_new_empty_simple ("text/x-raw");
+ gst_caps_set_features (caps, 0, features);
+ return caps;
+}
+
+static void
+feed_textbuf (GstTtmlParse * self, GstBuffer * buf)
+{
+ gboolean discont;
+ gsize consumed;
+ gchar *input = NULL;
+ const guint8 *data;
+ gsize avail;
+
+ discont = GST_BUFFER_IS_DISCONT (buf);
+
+ if (GST_BUFFER_OFFSET_IS_VALID (buf) &&
+ GST_BUFFER_OFFSET (buf) != self->offset) {
+ self->offset = GST_BUFFER_OFFSET (buf);
+ discont = TRUE;
+ }
+
+ if (discont) {
+ GST_INFO ("discontinuity");
+ /* flush the parser state */
+ g_string_truncate (self->textbuf, 0);
+ gst_adapter_clear (self->adapter);
+ /* we could set a flag to make sure that the next buffer we push out also
+ * has the DISCONT flag set, but there's no point really given that it's
+ * subtitles which are discontinuous by nature. */
+ }
+
+ self->offset += gst_buffer_get_size (buf);
+
+ gst_adapter_push (self->adapter, buf);
+
+ avail = gst_adapter_available (self->adapter);
+ data = gst_adapter_map (self->adapter, avail);
+ input = convert_encoding (self, (const gchar *) data, avail, &consumed);
+
+ if (input && consumed > 0) {
+ if (self->textbuf) {
+ g_string_free (self->textbuf, TRUE);
+ self->textbuf = NULL;
+ }
+ self->textbuf = g_string_new (input);
+ gst_adapter_unmap (self->adapter);
+ gst_adapter_flush (self->adapter, consumed);
+ } else {
+ gst_adapter_unmap (self->adapter);
+ }
+
+ g_free (input);
+}
+
+static GstFlowReturn
+handle_buffer (GstTtmlParse * self, GstBuffer * buf)
+{
+ GstFlowReturn ret = GST_FLOW_OK;
+ GstCaps *caps = NULL;
+ GList *subtitle_list, *subtitle;
+ GstClockTime begin = GST_BUFFER_PTS (buf);
+ GstClockTime duration = GST_BUFFER_DURATION (buf);
+
+ if (self->first_buffer) {
+ GstMapInfo map;
+
+ gst_buffer_map (buf, &map, GST_MAP_READ);
+ self->detected_encoding = detect_encoding ((gchar *) map.data, map.size);
+ gst_buffer_unmap (buf, &map);
+ self->first_buffer = FALSE;
+ }
+
+ feed_textbuf (self, buf);
+
+ if (!(caps = gst_ttml_parse_get_src_caps (self)))
+ return GST_FLOW_EOS;
+
+ /* Push newsegment if needed */
+ if (self->need_segment) {
+ GST_LOG_OBJECT (self, "pushing newsegment event with %" GST_SEGMENT_FORMAT,
+ &self->segment);
+
+ gst_pad_push_event (self->srcpad, gst_event_new_segment (&self->segment));
+ self->need_segment = FALSE;
+ }
+
+ subtitle_list = ttml_parse (self->textbuf->str, begin, duration);
+
+ for (subtitle = subtitle_list; subtitle; subtitle = subtitle->next) {
+ GstBuffer *op_buffer = subtitle->data;
+ self->segment.position = GST_BUFFER_PTS (op_buffer);
+
+ ret = gst_pad_push (self->srcpad, op_buffer);
+
+ if (ret != GST_FLOW_OK)
+ GST_DEBUG_OBJECT (self, "flow: %s", gst_flow_get_name (ret));
+ }
+
+ g_list_free (subtitle_list);
+ return ret;
+}
+
+static GstFlowReturn
+gst_ttml_parse_chain (GstPad * sinkpad, GstObject * parent, GstBuffer * buf)
+{
+ GstTtmlParse *self = GST_TTML_PARSE (parent);
+ return handle_buffer (self, buf);
+}
+
+static gboolean
+gst_ttml_parse_sink_event (GstPad * pad, GstObject * parent, GstEvent * event)
+{
+ GstTtmlParse *self = GST_TTML_PARSE (parent);
+ gboolean ret = FALSE;
+
+ GST_DEBUG ("Handling %s event", GST_EVENT_TYPE_NAME (event));
+
+ switch (GST_EVENT_TYPE (event)) {
+ case GST_EVENT_SEGMENT:
+ {
+ const GstSegment *s;
+ gst_event_parse_segment (event, &s);
+ if (s->format == GST_FORMAT_TIME)
+ gst_event_copy_segment (event, &self->segment);
+ GST_DEBUG_OBJECT (self, "newsegment (%s)",
+ gst_format_get_name (self->segment.format));
+
+ /* if not time format, we'll either start with a 0 timestamp anyway or
+ * it's following a seek in which case we'll have saved the requested
+ * seek segment and don't want to overwrite it (remember that on a seek
+ * we always just seek back to the start in BYTES format and just throw
+ * away all text that's before the requested position; if the subtitles
+ * come from an upstream demuxer, it won't be able to handle our BYTES
+ * seek request and instead send us a newsegment from the seek request
+ * it received via its video pads instead, so all is fine then too) */
+ ret = TRUE;
+ gst_event_unref (event);
+ break;
+ }
+ default:
+ ret = gst_pad_event_default (pad, parent, event);
+ break;
+ }
+
+ return ret;
+}
+
+static GstStateChangeReturn
+gst_ttml_parse_change_state (GstElement * element, GstStateChange transition)
+{
+ GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;
+ GstTtmlParse *self = GST_TTML_PARSE (element);
+
+ switch (transition) {
+ case GST_STATE_CHANGE_READY_TO_PAUSED:
+ /* format detection will init the parser state */
+ self->offset = 0;
+ self->valid_utf8 = TRUE;
+ self->first_buffer = TRUE;
+ g_free (self->detected_encoding);
+ self->detected_encoding = NULL;
+ g_string_truncate (self->textbuf, 0);
+ gst_adapter_clear (self->adapter);
+ break;
+ default:
+ break;
+ }
+
+ ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
+ if (ret == GST_STATE_CHANGE_FAILURE)
+ return ret;
+
+ switch (transition) {
+ case GST_STATE_CHANGE_PAUSED_TO_READY:
+ break;
+ default:
+ break;
+ }
+
+ return ret;
+}
diff --git a/ext/ttml/gstttmlparse.h b/ext/ttml/gstttmlparse.h
new file mode 100644
index 000000000..f81fd5f8e
--- /dev/null
+++ b/ext/ttml/gstttmlparse.h
@@ -0,0 +1,76 @@
+/* GStreamer
+ * Copyright (C) <2002> David A. Schleef <ds@schleef.org>
+ * Copyright (C) <1999> Erik Walthinsen <omega@cse.ogi.edu>
+ * Copyright (C) <2015> British Broadcasting Corporation
+ *
+ * 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., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __GST_TTML_PARSE_H__
+#define __GST_TTML_PARSE_H__
+
+#include <gst/gst.h>
+#include <gst/base/gstadapter.h>
+
+G_BEGIN_DECLS
+
+#define GST_TYPE_TTML_PARSE \
+ (gst_ttml_parse_get_type ())
+#define GST_TTML_PARSE(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_TTML_PARSE, GstTtmlParse))
+#define GST_TTML_PARSE_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_TTML_PARSE, GstTtmlParseClass))
+#define GST_IS_TTML_PARSE(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_TTML_PARSE))
+#define GST_IS_TTML_PARSE_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_TTML_PARSE))
+
+typedef struct _GstTtmlParse GstTtmlParse;
+typedef struct _GstTtmlParseClass GstTtmlParseClass;
+
+struct _GstTtmlParse {
+ GstElement element;
+
+ GstPad *sinkpad, *srcpad;
+
+ /* contains the input in the input encoding */
+ GstAdapter *adapter;
+ /* contains the UTF-8 decoded input */
+ GString *textbuf;
+
+ /* seek */
+ guint64 offset;
+
+ /* Segment */
+ GstSegment segment;
+ gboolean need_segment;
+
+ gboolean valid_utf8;
+ gchar *detected_encoding;
+ gchar *encoding;
+
+ gboolean first_buffer;
+};
+
+struct _GstTtmlParseClass {
+ GstElementClass parent_class;
+};
+
+GType gst_ttml_parse_get_type (void);
+
+G_END_DECLS
+
+#endif /* __GST_TTML_PARSE_H__ */
diff --git a/ext/ttml/gstttmlplugin.c b/ext/ttml/gstttmlplugin.c
new file mode 100644
index 000000000..cc64bcc4a
--- /dev/null
+++ b/ext/ttml/gstttmlplugin.c
@@ -0,0 +1,53 @@
+/* GStreamer
+ * Copyright (C) <1999> Erik Walthinsen <omega@cse.ogi.edu>
+ * Copyright (C) 2004 Ronald S. Bultje <rbultje@ronald.bitfreak.net>
+ * Copyright (C) 2006 Tim-Philipp Müller <tim centricular net>
+ * Copyright (C) <2015> British Broadcasting Corporation <dash@rd.bbc.co.uk>
+ *
+ * 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., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "gstttmlparse.h"
+#include "gstttmlrender.h"
+
+GST_DEBUG_CATEGORY (ttmlparse_debug);
+GST_DEBUG_CATEGORY (ttmlrender_debug);
+
+static gboolean
+plugin_init (GstPlugin * plugin)
+{
+ if (!gst_element_register (plugin, "ttmlparse", GST_RANK_PRIMARY,
+ GST_TYPE_TTML_PARSE))
+ return FALSE;
+ if (!gst_element_register (plugin, "ttmlrender", GST_RANK_PRIMARY,
+ GST_TYPE_TTML_RENDER))
+ return FALSE;
+
+ GST_DEBUG_CATEGORY_INIT (ttmlparse_debug, "ttmlparse", 0, "TTML parser");
+ GST_DEBUG_CATEGORY_INIT (ttmlrender_debug, "ttmlrender", 0, "TTML renderer");
+
+ return TRUE;
+}
+
+GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
+ GST_VERSION_MINOR,
+ ttmlsubs,
+ "TTML subtitle handling",
+ plugin_init, VERSION, "LGPL", "gst-ttml", "http://www.bbc.co.uk/rd")
diff --git a/ext/ttml/gstttmlrender.c b/ext/ttml/gstttmlrender.c
new file mode 100644
index 000000000..91acfb428
--- /dev/null
+++ b/ext/ttml/gstttmlrender.c
@@ -0,0 +1,2456 @@
+/* GStreamer
+ * Copyright (C) <1999> Erik Walthinsen <omega@cse.ogi.edu>
+ * Copyright (C) <2003> David Schleef <ds@schleef.org>
+ * Copyright (C) <2006> Julien Moutte <julien@moutte.net>
+ * Copyright (C) <2006> Zeeshan Ali <zeeshan.ali@nokia.com>
+ * Copyright (C) <2006-2008> Tim-Philipp Müller <tim centricular net>
+ * Copyright (C) <2009> Young-Ho Cha <ganadist@gmail.com>
+ * Copyright (C) <2015> British Broadcasting Corporation <dash@rd.bbc.co.uk>
+ *
+ * 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., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * SECTION:element-ttmlrender
+ *
+ * Renders timed text on top of a video stream. It receives text in buffers
+ * from a ttmlparse element; each text string is in its own #GstMemory within
+ * the GstBuffer, and the styling and layout associated with each text string
+ * is in metadata attached to the #GstBuffer.
+ *
+ * <refsect2>
+ * <title>Example launch lines</title>
+ * |[
+ * 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
+ * ]| Parse and render TTML subtitles contained in a single XML file over an
+ * MP4 stream containing H.264 video and AAC audio:
+ * </refsect2>
+ */
+
+#include <gst/video/video.h>
+#include <gst/video/gstvideometa.h>
+#include <gst/video/video-overlay-composition.h>
+#include <pango/pangocairo.h>
+
+#include <string.h>
+#include <math.h>
+
+#include "gstttmlrender.h"
+#include "subtitle.h"
+#include "subtitlemeta.h"
+
+#define VIDEO_FORMATS GST_VIDEO_OVERLAY_COMPOSITION_BLEND_FORMATS
+
+#define TTML_RENDER_CAPS GST_VIDEO_CAPS_MAKE (VIDEO_FORMATS)
+
+#define TTML_RENDER_ALL_CAPS TTML_RENDER_CAPS ";" \
+ GST_VIDEO_CAPS_MAKE_WITH_FEATURES ("ANY", GST_VIDEO_FORMATS_ALL)
+
+GST_DEBUG_CATEGORY_EXTERN (ttmlrender_debug);
+#define GST_CAT_DEFAULT ttmlrender_debug
+
+static GstStaticCaps sw_template_caps = GST_STATIC_CAPS (TTML_RENDER_CAPS);
+
+static GstStaticPadTemplate src_template_factory =
+GST_STATIC_PAD_TEMPLATE ("src",
+ GST_PAD_SRC,
+ GST_PAD_ALWAYS,
+ GST_STATIC_CAPS (TTML_RENDER_ALL_CAPS)
+ );
+
+static GstStaticPadTemplate video_sink_template_factory =
+GST_STATIC_PAD_TEMPLATE ("video_sink",
+ GST_PAD_SINK,
+ GST_PAD_ALWAYS,
+ GST_STATIC_CAPS (TTML_RENDER_ALL_CAPS)
+ );
+
+static GstStaticPadTemplate text_sink_template_factory =
+GST_STATIC_PAD_TEMPLATE ("text_sink",
+ GST_PAD_SINK,
+ GST_PAD_ALWAYS,
+ GST_STATIC_CAPS ("text/x-raw(meta:GstSubtitleMeta)")
+ );
+
+#define GST_TTML_RENDER_GET_LOCK(ov) (&GST_TTML_RENDER (ov)->lock)
+#define GST_TTML_RENDER_GET_COND(ov) (&GST_TTML_RENDER (ov)->cond)
+#define GST_TTML_RENDER_LOCK(ov) (g_mutex_lock (GST_TTML_RENDER_GET_LOCK (ov)))
+#define GST_TTML_RENDER_UNLOCK(ov) (g_mutex_unlock (GST_TTML_RENDER_GET_LOCK (ov)))
+#define GST_TTML_RENDER_WAIT(ov) (g_cond_wait (GST_TTML_RENDER_GET_COND (ov), GST_TTML_RENDER_GET_LOCK (ov)))
+#define GST_TTML_RENDER_SIGNAL(ov) (g_cond_signal (GST_TTML_RENDER_GET_COND (ov)))
+#define GST_TTML_RENDER_BROADCAST(ov)(g_cond_broadcast (GST_TTML_RENDER_GET_COND (ov)))
+
+static GstElementClass *parent_class = NULL;
+static void gst_ttml_render_base_init (gpointer g_class);
+static void gst_ttml_render_class_init (GstTtmlRenderClass * klass);
+static void gst_ttml_render_init (GstTtmlRender * render,
+ GstTtmlRenderClass * klass);
+
+static GstStateChangeReturn gst_ttml_render_change_state (GstElement *
+ element, GstStateChange transition);
+
+static GstCaps *gst_ttml_render_get_videosink_caps (GstPad * pad,
+ GstTtmlRender * render, GstCaps * filter);
+static GstCaps *gst_ttml_render_get_src_caps (GstPad * pad,
+ GstTtmlRender * render, GstCaps * filter);
+static gboolean gst_ttml_render_setcaps (GstTtmlRender * render,
+ GstCaps * caps);
+static gboolean gst_ttml_render_src_event (GstPad * pad,
+ GstObject * parent, GstEvent * event);
+static gboolean gst_ttml_render_src_query (GstPad * pad,
+ GstObject * parent, GstQuery * query);
+
+static gboolean gst_ttml_render_video_event (GstPad * pad,
+ GstObject * parent, GstEvent * event);
+static gboolean gst_ttml_render_video_query (GstPad * pad,
+ GstObject * parent, GstQuery * query);
+static GstFlowReturn gst_ttml_render_video_chain (GstPad * pad,
+ GstObject * parent, GstBuffer * buffer);
+
+static gboolean gst_ttml_render_text_event (GstPad * pad,
+ GstObject * parent, GstEvent * event);
+static GstFlowReturn gst_ttml_render_text_chain (GstPad * pad,
+ GstObject * parent, GstBuffer * buffer);
+static GstPadLinkReturn gst_ttml_render_text_pad_link (GstPad * pad,
+ GstObject * parent, GstPad * peer);
+static void gst_ttml_render_text_pad_unlink (GstPad * pad, GstObject * parent);
+static void gst_ttml_render_pop_text (GstTtmlRender * render);
+
+static void gst_ttml_render_finalize (GObject * object);
+
+static gboolean gst_ttml_render_can_handle_caps (GstCaps * incaps);
+
+static GstTtmlRenderRenderedImage *gst_ttml_render_rendered_image_new
+ (GstBuffer * image, gint x, gint y, guint width, guint height);
+static GstTtmlRenderRenderedImage *gst_ttml_render_rendered_image_new_empty
+ (void);
+static GstTtmlRenderRenderedImage *gst_ttml_render_rendered_image_copy
+ (GstTtmlRenderRenderedImage * image);
+static void gst_ttml_render_rendered_image_free
+ (GstTtmlRenderRenderedImage * image);
+
+GType
+gst_ttml_render_get_type (void)
+{
+ static GType type = 0;
+
+ if (g_once_init_enter ((gsize *) & type)) {
+ static const GTypeInfo info = {
+ sizeof (GstTtmlRenderClass),
+ (GBaseInitFunc) gst_ttml_render_base_init,
+ NULL,
+ (GClassInitFunc) gst_ttml_render_class_init,
+ NULL,
+ NULL,
+ sizeof (GstTtmlRender),
+ 0,
+ (GInstanceInitFunc) gst_ttml_render_init,
+ };
+
+ g_once_init_leave ((gsize *) & type,
+ g_type_register_static (GST_TYPE_ELEMENT, "GstTtmlRender", &info, 0));
+ }
+
+ return type;
+}
+
+static void
+gst_ttml_render_base_init (gpointer g_class)
+{
+ GstTtmlRenderClass *klass = GST_TTML_RENDER_CLASS (g_class);
+ PangoFontMap *fontmap;
+
+ /* Only lock for the subclasses here, the base class
+ * doesn't have this mutex yet and it's not necessary
+ * here */
+ if (klass->pango_lock)
+ g_mutex_lock (klass->pango_lock);
+ fontmap = pango_cairo_font_map_get_default ();
+ klass->pango_context =
+ pango_font_map_create_context (PANGO_FONT_MAP (fontmap));
+ if (klass->pango_lock)
+ g_mutex_unlock (klass->pango_lock);
+}
+
+static void
+gst_ttml_render_class_init (GstTtmlRenderClass * klass)
+{
+ GObjectClass *gobject_class;
+ GstElementClass *gstelement_class;
+
+ gobject_class = (GObjectClass *) klass;
+ gstelement_class = (GstElementClass *) klass;
+
+ parent_class = g_type_class_peek_parent (klass);
+
+ gobject_class->finalize = gst_ttml_render_finalize;
+
+ gst_element_class_add_pad_template (gstelement_class,
+ gst_static_pad_template_get (&src_template_factory));
+ gst_element_class_add_pad_template (gstelement_class,
+ gst_static_pad_template_get (&video_sink_template_factory));
+ gst_element_class_add_pad_template (gstelement_class,
+ gst_static_pad_template_get (&text_sink_template_factory));
+
+ gst_element_class_set_static_metadata (gstelement_class,
+ "TTML subtitle renderer", "Overlay/Subtitle",
+ "Renders timed-text subtitles on top of video buffers",
+ "David Schleef <ds@schleef.org>, Zeeshan Ali <zeeshan.ali@nokia.com>, "
+ "Chris Bass <dash@rd.bbc.co.uk>");
+
+ gstelement_class->change_state =
+ GST_DEBUG_FUNCPTR (gst_ttml_render_change_state);
+
+ klass->pango_lock = g_slice_new (GMutex);
+ g_mutex_init (klass->pango_lock);
+}
+
+static void
+gst_ttml_render_finalize (GObject * object)
+{
+ GstTtmlRender *render = GST_TTML_RENDER (object);
+
+ if (render->compositions) {
+ g_list_free_full (render->compositions,
+ (GDestroyNotify) gst_video_overlay_composition_unref);
+ render->compositions = NULL;
+ }
+
+ if (render->text_buffer) {
+ gst_buffer_unref (render->text_buffer);
+ render->text_buffer = NULL;
+ }
+
+ g_mutex_clear (&render->lock);
+ g_cond_clear (&render->cond);
+
+ G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+gst_ttml_render_init (GstTtmlRender * render, GstTtmlRenderClass * klass)
+{
+ GstPadTemplate *template;
+
+ /* video sink */
+ template = gst_static_pad_template_get (&video_sink_template_factory);
+ render->video_sinkpad = gst_pad_new_from_template (template, "video_sink");
+ gst_object_unref (template);
+ gst_pad_set_event_function (render->video_sinkpad,
+ GST_DEBUG_FUNCPTR (gst_ttml_render_video_event));
+ gst_pad_set_chain_function (render->video_sinkpad,
+ GST_DEBUG_FUNCPTR (gst_ttml_render_video_chain));
+ gst_pad_set_query_function (render->video_sinkpad,
+ GST_DEBUG_FUNCPTR (gst_ttml_render_video_query));
+ GST_PAD_SET_PROXY_ALLOCATION (render->video_sinkpad);
+ gst_element_add_pad (GST_ELEMENT (render), render->video_sinkpad);
+
+ template =
+ gst_element_class_get_pad_template (GST_ELEMENT_CLASS (klass),
+ "text_sink");
+ if (template) {
+ /* text sink */
+ render->text_sinkpad = gst_pad_new_from_template (template, "text_sink");
+
+ gst_pad_set_event_function (render->text_sinkpad,
+ GST_DEBUG_FUNCPTR (gst_ttml_render_text_event));
+ gst_pad_set_chain_function (render->text_sinkpad,
+ GST_DEBUG_FUNCPTR (gst_ttml_render_text_chain));
+ gst_pad_set_link_function (render->text_sinkpad,
+ GST_DEBUG_FUNCPTR (gst_ttml_render_text_pad_link));
+ gst_pad_set_unlink_function (render->text_sinkpad,
+ GST_DEBUG_FUNCPTR (gst_ttml_render_text_pad_unlink));
+ gst_element_add_pad (GST_ELEMENT (render), render->text_sinkpad);
+ }
+
+ /* (video) source */
+ template = gst_static_pad_template_get (&src_template_factory);
+ render->srcpad = gst_pad_new_from_template (template, "src");
+ gst_object_unref (template);
+ gst_pad_set_event_function (render->srcpad,
+ GST_DEBUG_FUNCPTR (gst_ttml_render_src_event));
+ gst_pad_set_query_function (render->srcpad,
+ GST_DEBUG_FUNCPTR (gst_ttml_render_src_query));
+ gst_element_add_pad (GST_ELEMENT (render), render->srcpad);
+
+ g_mutex_lock (GST_TTML_RENDER_GET_CLASS (render)->pango_lock);
+
+ render->wait_text = TRUE;
+ render->need_render = TRUE;
+ render->text_buffer = NULL;
+ render->text_linked = FALSE;
+
+ render->compositions = NULL;
+
+ g_mutex_init (&render->lock);
+ g_cond_init (&render->cond);
+ gst_segment_init (&render->segment, GST_FORMAT_TIME);
+ g_mutex_unlock (GST_TTML_RENDER_GET_CLASS (render)->pango_lock);
+}
+
+
+/* only negotiate/query video render composition support for now */
+static gboolean
+gst_ttml_render_negotiate (GstTtmlRender * render, GstCaps * caps)
+{
+ GstQuery *query;
+ gboolean attach = FALSE;
+ gboolean caps_has_meta = TRUE;
+ gboolean ret;
+ GstCapsFeatures *f;
+ GstCaps *original_caps;
+ gboolean original_has_meta = FALSE;
+ gboolean allocation_ret = TRUE;
+
+ GST_DEBUG_OBJECT (render, "performing negotiation");
+
+ if (!caps)
+ caps = gst_pad_get_current_caps (render->video_sinkpad);
+ else
+ gst_caps_ref (caps);
+
+ if (!caps || gst_caps_is_empty (caps))
+ goto no_format;
+
+ original_caps = caps;
+
+ /* Try to use the render meta if possible */
+ f = gst_caps_get_features (caps, 0);
+
+ /* if the caps doesn't have the render meta, we query if downstream
+ * accepts it before trying the version without the meta
+ * If upstream already is using the meta then we can only use it */
+ if (!f
+ || !gst_caps_features_contains (f,
+ GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION)) {
+ GstCaps *overlay_caps;
+
+ /* In this case we added the meta, but we can work without it
+ * so preserve the original caps so we can use it as a fallback */
+ overlay_caps = gst_caps_copy (caps);
+
+ f = gst_caps_get_features (overlay_caps, 0);
+ gst_caps_features_add (f,
+ GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION);
+
+ ret = gst_pad_peer_query_accept_caps (render->srcpad, overlay_caps);
+ GST_DEBUG_OBJECT (render, "Downstream accepts the render meta: %d", ret);
+ if (ret) {
+ gst_caps_unref (caps);
+ caps = overlay_caps;
+
+ } else {
+ /* fallback to the original */
+ gst_caps_unref (overlay_caps);
+ caps_has_meta = FALSE;
+ }
+ } else {
+ original_has_meta = TRUE;
+ }
+ GST_DEBUG_OBJECT (render, "Using caps %" GST_PTR_FORMAT, caps);
+ ret = gst_pad_set_caps (render->srcpad, caps);
+
+ if (ret) {
+ /* find supported meta */
+ query = gst_query_new_allocation (caps, FALSE);
+
+ if (!gst_pad_peer_query (render->srcpad, query)) {
+ /* no problem, we use the query defaults */
+ GST_DEBUG_OBJECT (render, "ALLOCATION query failed");
+ allocation_ret = FALSE;
+ }
+
+ if (caps_has_meta && gst_query_find_allocation_meta (query,
+ GST_VIDEO_OVERLAY_COMPOSITION_META_API_TYPE, NULL))
+ attach = TRUE;
+
+ gst_query_unref (query);
+ }
+
+ if (!allocation_ret && render->video_flushing) {
+ ret = FALSE;
+ } else if (original_caps && !original_has_meta && !attach) {
+ if (caps_has_meta) {
+ /* Some elements (fakesink) claim to accept the meta on caps but won't
+ put it in the allocation query result, this leads below
+ check to fail. Prevent this by removing the meta from caps */
+ gst_caps_unref (caps);
+ caps = gst_caps_ref (original_caps);
+ ret = gst_pad_set_caps (render->srcpad, caps);
+ if (ret && !gst_ttml_render_can_handle_caps (caps))
+ ret = FALSE;
+ }
+ }
+
+ if (!ret) {
+ GST_DEBUG_OBJECT (render, "negotiation failed, schedule reconfigure");
+ gst_pad_mark_reconfigure (render->srcpad);
+ }
+
+ gst_caps_unref (caps);
+
+ return ret;
+
+no_format:
+ {
+ if (caps)
+ gst_caps_unref (caps);
+ return FALSE;
+ }
+}
+
+static gboolean
+gst_ttml_render_can_handle_caps (GstCaps * incaps)
+{
+ gboolean ret;
+ GstCaps *caps;
+ static GstStaticCaps static_caps = GST_STATIC_CAPS (TTML_RENDER_CAPS);
+
+ caps = gst_static_caps_get (&static_caps);
+ ret = gst_caps_is_subset (incaps, caps);
+ gst_caps_unref (caps);
+
+ return ret;
+}
+
+static gboolean
+gst_ttml_render_setcaps (GstTtmlRender * render, GstCaps * caps)
+{
+ GstVideoInfo info;
+ gboolean ret = FALSE;
+
+ if (!gst_video_info_from_caps (&info, caps))
+ goto invalid_caps;
+
+ render->info = info;
+ render->format = GST_VIDEO_INFO_FORMAT (&info);
+ render->width = GST_VIDEO_INFO_WIDTH (&info);
+ render->height = GST_VIDEO_INFO_HEIGHT (&info);
+
+ ret = gst_ttml_render_negotiate (render, caps);
+
+ GST_TTML_RENDER_LOCK (render);
+ g_mutex_lock (GST_TTML_RENDER_GET_CLASS (render)->pango_lock);
+ if (!gst_ttml_render_can_handle_caps (caps)) {
+ GST_DEBUG_OBJECT (render, "unsupported caps %" GST_PTR_FORMAT, caps);
+ ret = FALSE;
+ }
+
+ g_mutex_unlock (GST_TTML_RENDER_GET_CLASS (render)->pango_lock);
+ GST_TTML_RENDER_UNLOCK (render);
+
+ return ret;
+
+ /* ERRORS */
+invalid_caps:
+ {
+ GST_DEBUG_OBJECT (render, "could not parse caps");
+ return FALSE;
+ }
+}
+
+
+static gboolean
+gst_ttml_render_src_query (GstPad * pad, GstObject * parent, GstQuery * query)
+{
+ gboolean ret = FALSE;
+ GstTtmlRender *render;
+
+ render = GST_TTML_RENDER (parent);
+
+ switch (GST_QUERY_TYPE (query)) {
+ case GST_QUERY_CAPS:
+ {
+ GstCaps *filter, *caps;
+
+ gst_query_parse_caps (query, &filter);
+ caps = gst_ttml_render_get_src_caps (pad, render, filter);
+ gst_query_set_caps_result (query, caps);
+ gst_caps_unref (caps);
+ ret = TRUE;
+ break;
+ }
+ default:
+ ret = gst_pad_query_default (pad, parent, query);
+ break;
+ }
+
+ return ret;
+}
+
+static gboolean
+gst_ttml_render_src_event (GstPad * pad, GstObject * parent, GstEvent * event)
+{
+ GstTtmlRender *render;
+ gboolean ret;
+
+ render = GST_TTML_RENDER (parent);
+
+ if (render->text_linked) {
+ gst_event_ref (event);
+ ret = gst_pad_push_event (render->video_sinkpad, event);
+ gst_pad_push_event (render->text_sinkpad, event);
+ } else {
+ ret = gst_pad_push_event (render->video_sinkpad, event);
+ }
+
+ return ret;
+}
+
+/**
+ * gst_ttml_render_add_feature_and_intersect:
+ *
+ * Creates a new #GstCaps containing the (given caps +
+ * given caps feature) + (given caps intersected by the
+ * given filter).
+ *
+ * Returns: the new #GstCaps
+ */
+static GstCaps *
+gst_ttml_render_add_feature_and_intersect (GstCaps * caps,
+ const gchar * feature, GstCaps * filter)
+{
+ int i, caps_size;
+ GstCaps *new_caps;
+
+ new_caps = gst_caps_copy (caps);
+
+ caps_size = gst_caps_get_size (new_caps);
+ for (i = 0; i < caps_size; i++) {
+ GstCapsFeatures *features = gst_caps_get_features (new_caps, i);
+
+ if (!gst_caps_features_is_any (features)) {
+ gst_caps_features_add (features, feature);
+ }
+ }
+
+ gst_caps_append (new_caps, gst_caps_intersect_full (caps,
+ filter, GST_CAPS_INTERSECT_FIRST));
+
+ return new_caps;
+}
+
+/**
+ * gst_ttml_render_intersect_by_feature:
+ *
+ * Creates a new #GstCaps based on the following filtering rule.
+ *
+ * For each individual caps contained in given caps, if the
+ * caps uses the given caps feature, keep a version of the caps
+ * with the feature and an another one without. Otherwise, intersect
+ * the caps with the given filter.
+ *
+ * Returns: the new #GstCaps
+ */
+static GstCaps *
+gst_ttml_render_intersect_by_feature (GstCaps * caps,
+ const gchar * feature, GstCaps * filter)
+{
+ int i, caps_size;
+ GstCaps *new_caps;
+
+ new_caps = gst_caps_new_empty ();
+
+ caps_size = gst_caps_get_size (caps);
+ for (i = 0; i < caps_size; i++) {
+ GstStructure *caps_structure = gst_caps_get_structure (caps, i);
+ GstCapsFeatures *caps_features =
+ gst_caps_features_copy (gst_caps_get_features (caps, i));
+ GstCaps *filtered_caps;
+ GstCaps *simple_caps =
+ gst_caps_new_full (gst_structure_copy (caps_structure), NULL);
+ gst_caps_set_features (simple_caps, 0, caps_features);
+
+ if (gst_caps_features_contains (caps_features, feature)) {
+ gst_caps_append (new_caps, gst_caps_copy (simple_caps));
+
+ gst_caps_features_remove (caps_features, feature);
+ filtered_caps = gst_caps_ref (simple_caps);
+ } else {
+ filtered_caps = gst_caps_intersect_full (simple_caps, filter,
+ GST_CAPS_INTERSECT_FIRST);
+ }
+
+ gst_caps_unref (simple_caps);
+ gst_caps_append (new_caps, filtered_caps);
+ }
+
+ return new_caps;
+}
+
+static GstCaps *
+gst_ttml_render_get_videosink_caps (GstPad * pad,
+ GstTtmlRender * render, GstCaps * filter)
+{
+ GstPad *srcpad = render->srcpad;
+ GstCaps *peer_caps = NULL, *caps = NULL, *overlay_filter = NULL;
+
+ if (G_UNLIKELY (!render))
+ return gst_pad_get_pad_template_caps (pad);
+
+ if (filter) {
+ /* filter caps + composition feature + filter caps
+ * filtered by the software caps. */
+ GstCaps *sw_caps = gst_static_caps_get (&sw_template_caps);
+ overlay_filter = gst_ttml_render_add_feature_and_intersect (filter,
+ GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION, sw_caps);
+ gst_caps_unref (sw_caps);
+
+ GST_DEBUG_OBJECT (render, "render filter %" GST_PTR_FORMAT, overlay_filter);
+ }
+
+ peer_caps = gst_pad_peer_query_caps (srcpad, overlay_filter);
+
+ if (overlay_filter)
+ gst_caps_unref (overlay_filter);
+
+ if (peer_caps) {
+
+ GST_DEBUG_OBJECT (pad, "peer caps %" GST_PTR_FORMAT, peer_caps);
+
+ if (gst_caps_is_any (peer_caps)) {
+ /* if peer returns ANY caps, return filtered src pad template caps */
+ caps = gst_caps_copy (gst_pad_get_pad_template_caps (srcpad));
+ } else {
+
+ /* duplicate caps which contains the composition into one version with
+ * the meta and one without. Filter the other caps by the software caps */
+ GstCaps *sw_caps = gst_static_caps_get (&sw_template_caps);
+ caps = gst_ttml_render_intersect_by_feature (peer_caps,
+ GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION, sw_caps);
+ gst_caps_unref (sw_caps);
+ }
+
+ gst_caps_unref (peer_caps);
+
+ } else {
+ /* no peer, our padtemplate is enough then */
+ caps = gst_pad_get_pad_template_caps (pad);
+ }
+
+ if (filter) {
+ GstCaps *intersection = gst_caps_intersect_full (filter, caps,
+ GST_CAPS_INTERSECT_FIRST);
+ gst_caps_unref (caps);
+ caps = intersection;
+ }
+
+ GST_DEBUG_OBJECT (render, "returning %" GST_PTR_FORMAT, caps);
+
+ return caps;
+}
+
+static GstCaps *
+gst_ttml_render_get_src_caps (GstPad * pad, GstTtmlRender * render,
+ GstCaps * filter)
+{
+ GstPad *sinkpad = render->video_sinkpad;
+ GstCaps *peer_caps = NULL, *caps = NULL, *overlay_filter = NULL;
+
+ if (G_UNLIKELY (!render))
+ return gst_pad_get_pad_template_caps (pad);
+
+ if (filter) {
+ /* duplicate filter caps which contains the composition into one version
+ * with the meta and one without. Filter the other caps by the software
+ * caps */
+ GstCaps *sw_caps = gst_static_caps_get (&sw_template_caps);
+ overlay_filter =
+ gst_ttml_render_intersect_by_feature (filter,
+ GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION, sw_caps);
+ gst_caps_unref (sw_caps);
+ }
+
+ peer_caps = gst_pad_peer_query_caps (sinkpad, overlay_filter);
+
+ if (overlay_filter)
+ gst_caps_unref (overlay_filter);
+
+ if (peer_caps) {
+
+ GST_DEBUG_OBJECT (pad, "peer caps %" GST_PTR_FORMAT, peer_caps);
+
+ if (gst_caps_is_any (peer_caps)) {
+
+ /* if peer returns ANY caps, return filtered sink pad template caps */
+ caps = gst_caps_copy (gst_pad_get_pad_template_caps (sinkpad));
+
+ } else {
+
+ /* return upstream caps + composition feature + upstream caps
+ * filtered by the software caps. */
+ GstCaps *sw_caps = gst_static_caps_get (&sw_template_caps);
+ caps = gst_ttml_render_add_feature_and_intersect (peer_caps,
+ GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION, sw_caps);
+ gst_caps_unref (sw_caps);
+ }
+
+ gst_caps_unref (peer_caps);
+
+ } else {
+ /* no peer, our padtemplate is enough then */
+ caps = gst_pad_get_pad_template_caps (pad);
+ }
+
+ if (filter) {
+ GstCaps *intersection;
+
+ intersection =
+ gst_caps_intersect_full (filter, caps, GST_CAPS_INTERSECT_FIRST);
+ gst_caps_unref (caps);
+ caps = intersection;
+ }
+ GST_DEBUG_OBJECT (render, "returning %" GST_PTR_FORMAT, caps);
+
+ return caps;
+}
+
+
+static GstFlowReturn
+gst_ttml_render_push_frame (GstTtmlRender * render, GstBuffer * video_frame)
+{
+ GstVideoFrame frame;
+ GList *compositions = render->compositions;
+
+ if (compositions == NULL) {
+ GST_CAT_DEBUG (ttmlrender_debug, "No compositions.");
+ goto done;
+ }
+
+ if (gst_pad_check_reconfigure (render->srcpad))
+ gst_ttml_render_negotiate (render, NULL);
+
+ video_frame = gst_buffer_make_writable (video_frame);
+
+ if (!gst_video_frame_map (&frame, &render->info, video_frame,
+ GST_MAP_READWRITE))
+ goto invalid_frame;
+
+ while (compositions) {
+ GstVideoOverlayComposition *composition = compositions->data;
+ gst_video_overlay_composition_blend (composition, &frame);
+ compositions = compositions->next;
+ }
+
+ gst_video_frame_unmap (&frame);
+
+done:
+
+ return gst_pad_push (render->srcpad, video_frame);
+
+ /* ERRORS */
+invalid_frame:
+ {
+ gst_buffer_unref (video_frame);
+ GST_DEBUG_OBJECT (render, "received invalid buffer");
+ return GST_FLOW_OK;
+ }
+}
+
+static GstPadLinkReturn
+gst_ttml_render_text_pad_link (GstPad * pad, GstObject * parent, GstPad * peer)
+{
+ GstTtmlRender *render;
+
+ render = GST_TTML_RENDER (parent);
+ if (G_UNLIKELY (!render))
+ return GST_PAD_LINK_REFUSED;
+
+ GST_DEBUG_OBJECT (render, "Text pad linked");
+
+ render->text_linked = TRUE;
+
+ return GST_PAD_LINK_OK;
+}
+
+static void
+gst_ttml_render_text_pad_unlink (GstPad * pad, GstObject * parent)
+{
+ GstTtmlRender *render;
+
+ /* don't use gst_pad_get_parent() here, will deadlock */
+ render = GST_TTML_RENDER (parent);
+
+ GST_DEBUG_OBJECT (render, "Text pad unlinked");
+
+ render->text_linked = FALSE;
+
+ gst_segment_init (&render->text_segment, GST_FORMAT_UNDEFINED);
+}
+
+static gboolean
+gst_ttml_render_text_event (GstPad * pad, GstObject * parent, GstEvent * event)
+{
+ gboolean ret = FALSE;
+ GstTtmlRender *render = NULL;
+
+ render = GST_TTML_RENDER (parent);
+
+ GST_LOG_OBJECT (pad, "received event %s", GST_EVENT_TYPE_NAME (event));
+
+ switch (GST_EVENT_TYPE (event)) {
+ case GST_EVENT_SEGMENT:
+ {
+ const GstSegment *segment;
+
+ render->text_eos = FALSE;
+
+ gst_event_parse_segment (event, &segment);
+
+ if (segment->format == GST_FORMAT_TIME) {
+ GST_TTML_RENDER_LOCK (render);
+ gst_segment_copy_into (segment, &render->text_segment);
+ GST_DEBUG_OBJECT (render, "TEXT SEGMENT now: %" GST_SEGMENT_FORMAT,
+ &render->text_segment);
+ GST_TTML_RENDER_UNLOCK (render);
+ } else {
+ GST_ELEMENT_WARNING (render, STREAM, MUX, (NULL),
+ ("received non-TIME newsegment event on text input"));
+ }
+
+ gst_event_unref (event);
+ ret = TRUE;
+
+ /* wake up the video chain, it might be waiting for a text buffer or
+ * a text segment update */
+ GST_TTML_RENDER_LOCK (render);
+ GST_TTML_RENDER_BROADCAST (render);
+ GST_TTML_RENDER_UNLOCK (render);
+ break;
+ }
+ case GST_EVENT_GAP:
+ {
+ GstClockTime start, duration;
+
+ gst_event_parse_gap (event, &start, &duration);
+ if (GST_CLOCK_TIME_IS_VALID (duration))
+ start += duration;
+ /* we do not expect another buffer until after gap,
+ * so that is our position now */
+ render->text_segment.position = start;
+
+ /* wake up the video chain, it might be waiting for a text buffer or
+ * a text segment update */
+ GST_TTML_RENDER_LOCK (render);
+ GST_TTML_RENDER_BROADCAST (render);
+ GST_TTML_RENDER_UNLOCK (render);
+
+ gst_event_unref (event);
+ ret = TRUE;
+ break;
+ }
+ case GST_EVENT_FLUSH_STOP:
+ GST_TTML_RENDER_LOCK (render);
+ GST_INFO_OBJECT (render, "text flush stop");
+ render->text_flushing = FALSE;
+ render->text_eos = FALSE;
+ gst_ttml_render_pop_text (render);
+ gst_segment_init (&render->text_segment, GST_FORMAT_TIME);
+ GST_TTML_RENDER_UNLOCK (render);
+ gst_event_unref (event);
+ ret = TRUE;
+ break;
+ case GST_EVENT_FLUSH_START:
+ GST_TTML_RENDER_LOCK (render);
+ GST_INFO_OBJECT (render, "text flush start");
+ render->text_flushing = TRUE;
+ GST_TTML_RENDER_BROADCAST (render);
+ GST_TTML_RENDER_UNLOCK (render);
+ gst_event_unref (event);
+ ret = TRUE;
+ break;
+ case GST_EVENT_EOS:
+ GST_TTML_RENDER_LOCK (render);
+ render->text_eos = TRUE;
+ GST_INFO_OBJECT (render, "text EOS");
+ /* wake up the video chain, it might be waiting for a text buffer or
+ * a text segment update */
+ GST_TTML_RENDER_BROADCAST (render);
+ GST_TTML_RENDER_UNLOCK (render);
+ gst_event_unref (event);
+ ret = TRUE;
+ break;
+ default:
+ ret = gst_pad_event_default (pad, parent, event);
+ break;
+ }
+
+ return ret;
+}
+
+static gboolean
+gst_ttml_render_video_event (GstPad * pad, GstObject * parent, GstEvent * event)
+{
+ gboolean ret = FALSE;
+ GstTtmlRender *render = NULL;
+
+ render = GST_TTML_RENDER (parent);
+
+ GST_DEBUG_OBJECT (pad, "received event %s", GST_EVENT_TYPE_NAME (event));
+
+ switch (GST_EVENT_TYPE (event)) {
+ case GST_EVENT_CAPS:
+ {
+ GstCaps *caps;
+ gint prev_width = render->width;
+ gint prev_height = render->height;
+
+ gst_event_parse_caps (event, &caps);
+ ret = gst_ttml_render_setcaps (render, caps);
+ if (render->width != prev_width || render->height != prev_height)
+ render->need_render = TRUE;
+ gst_event_unref (event);
+ break;
+ }
+ case GST_EVENT_SEGMENT:
+ {
+ const GstSegment *segment;
+
+ GST_DEBUG_OBJECT (render, "received new segment");
+
+ gst_event_parse_segment (event, &segment);
+
+ if (segment->format == GST_FORMAT_TIME) {
+ GST_DEBUG_OBJECT (render, "VIDEO SEGMENT now: %" GST_SEGMENT_FORMAT,
+ &render->segment);
+
+ gst_segment_copy_into (segment, &render->segment);
+ } else {
+ GST_ELEMENT_WARNING (render, STREAM, MUX, (NULL),
+ ("received non-TIME newsegment event on video input"));
+ }
+
+ ret = gst_pad_event_default (pad, parent, event);
+ break;
+ }
+ case GST_EVENT_EOS:
+ GST_TTML_RENDER_LOCK (render);
+ GST_INFO_OBJECT (render, "video EOS");
+ render->video_eos = TRUE;
+ GST_TTML_RENDER_UNLOCK (render);
+ ret = gst_pad_event_default (pad, parent, event);
+ break;
+ case GST_EVENT_FLUSH_START:
+ GST_TTML_RENDER_LOCK (render);
+ GST_INFO_OBJECT (render, "video flush start");
+ render->video_flushing = TRUE;
+ GST_TTML_RENDER_BROADCAST (render);
+ GST_TTML_RENDER_UNLOCK (render);
+ ret = gst_pad_event_default (pad, parent, event);
+ break;
+ case GST_EVENT_FLUSH_STOP:
+ GST_TTML_RENDER_LOCK (render);
+ GST_INFO_OBJECT (render, "video flush stop");
+ render->video_flushing = FALSE;
+ render->video_eos = FALSE;
+ gst_segment_init (&render->segment, GST_FORMAT_TIME);
+ GST_TTML_RENDER_UNLOCK (render);
+ ret = gst_pad_event_default (pad, parent, event);
+ break;
+ default:
+ ret = gst_pad_event_default (pad, parent, event);
+ break;
+ }
+
+ return ret;
+}
+
+static gboolean
+gst_ttml_render_video_query (GstPad * pad, GstObject * parent, GstQuery * query)
+{
+ gboolean ret = FALSE;
+ GstTtmlRender *render;
+
+ render = GST_TTML_RENDER (parent);
+
+ switch (GST_QUERY_TYPE (query)) {
+ case GST_QUERY_CAPS:
+ {
+ GstCaps *filter, *caps;
+
+ gst_query_parse_caps (query, &filter);
+ caps = gst_ttml_render_get_videosink_caps (pad, render, filter);
+ gst_query_set_caps_result (query, caps);
+ gst_caps_unref (caps);
+ ret = TRUE;
+ break;
+ }
+ default:
+ ret = gst_pad_query_default (pad, parent, query);
+ break;
+ }
+
+ return ret;
+}
+
+/* Called with lock held */
+static void
+gst_ttml_render_pop_text (GstTtmlRender * render)
+{
+ g_return_if_fail (GST_IS_TTML_RENDER (render));
+
+ if (render->text_buffer) {
+ GST_DEBUG_OBJECT (render, "releasing text buffer %p", render->text_buffer);
+ gst_buffer_unref (render->text_buffer);
+ render->text_buffer = NULL;
+ }
+
+ /* Let the text task know we used that buffer */
+ GST_TTML_RENDER_BROADCAST (render);
+}
+
+/* We receive text buffers here. If they are out of segment we just ignore them.
+ If the buffer is in our segment we keep it internally except if another one
+ is already waiting here, in that case we wait that it gets kicked out */
+static GstFlowReturn
+gst_ttml_render_text_chain (GstPad * pad, GstObject * parent,
+ GstBuffer * buffer)
+{
+ GstFlowReturn ret = GST_FLOW_OK;
+ GstTtmlRender *render = NULL;
+ gboolean in_seg = FALSE;
+ guint64 clip_start = 0, clip_stop = 0;
+
+ render = GST_TTML_RENDER (parent);
+
+ GST_TTML_RENDER_LOCK (render);
+
+ if (render->text_flushing) {
+ GST_TTML_RENDER_UNLOCK (render);
+ ret = GST_FLOW_FLUSHING;
+ GST_LOG_OBJECT (render, "text flushing");
+ goto beach;
+ }
+
+ if (render->text_eos) {
+ GST_TTML_RENDER_UNLOCK (render);
+ ret = GST_FLOW_EOS;
+ GST_LOG_OBJECT (render, "text EOS");
+ goto beach;
+ }
+
+ GST_LOG_OBJECT (render, "%" GST_SEGMENT_FORMAT " BUFFER: ts=%"
+ GST_TIME_FORMAT ", end=%" GST_TIME_FORMAT, &render->segment,
+ GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (buffer)),
+ GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (buffer) +
+ GST_BUFFER_DURATION (buffer)));
+
+ if (G_LIKELY (GST_BUFFER_TIMESTAMP_IS_VALID (buffer))) {
+ GstClockTime stop;
+
+ if (G_LIKELY (GST_BUFFER_DURATION_IS_VALID (buffer)))
+ stop = GST_BUFFER_TIMESTAMP (buffer) + GST_BUFFER_DURATION (buffer);
+ else
+ stop = GST_CLOCK_TIME_NONE;
+
+ in_seg = gst_segment_clip (&render->text_segment, GST_FORMAT_TIME,
+ GST_BUFFER_TIMESTAMP (buffer), stop, &clip_start, &clip_stop);
+ } else {
+ in_seg = TRUE;
+ }
+
+ if (in_seg) {
+ if (GST_BUFFER_TIMESTAMP_IS_VALID (buffer))
+ GST_BUFFER_TIMESTAMP (buffer) = clip_start;
+ else if (GST_BUFFER_DURATION_IS_VALID (buffer))
+ GST_BUFFER_DURATION (buffer) = clip_stop - clip_start;
+
+ /* Wait for the previous buffer to go away */
+ while (render->text_buffer != NULL) {
+ GST_DEBUG ("Pad %s:%s has a buffer queued, waiting",
+ GST_DEBUG_PAD_NAME (pad));
+ GST_TTML_RENDER_WAIT (render);
+ GST_DEBUG ("Pad %s:%s resuming", GST_DEBUG_PAD_NAME (pad));
+ if (render->text_flushing) {
+ GST_TTML_RENDER_UNLOCK (render);
+ ret = GST_FLOW_FLUSHING;
+ goto beach;
+ }
+ }
+
+ if (GST_BUFFER_TIMESTAMP_IS_VALID (buffer))
+ render->text_segment.position = clip_start;
+
+ render->text_buffer = buffer;
+ /* That's a new text buffer we need to render */
+ render->need_render = TRUE;
+
+ /* in case the video chain is waiting for a text buffer, wake it up */
+ GST_TTML_RENDER_BROADCAST (render);
+ }
+
+ GST_TTML_RENDER_UNLOCK (render);
+
+beach:
+
+ return ret;
+}
+
+
+/* Free returned string after use. */
+static gchar *
+gst_ttml_render_color_to_string (GstSubtitleColor color)
+{
+#if PANGO_VERSION_CHECK (1,38,0)
+ return g_strdup_printf ("#%02x%02x%02x%02x",
+ color.r, color.g, color.b, color.a);
+#else
+ return g_strdup_printf ("#%02x%02x%02x", color.r, color.g, color.b);
+#endif
+}
+
+
+static GstBuffer *
+gst_ttml_render_draw_rectangle (guint width, guint height,
+ GstSubtitleColor color)
+{
+ GstMapInfo map;
+ cairo_surface_t *surface;
+ cairo_t *cairo_state;
+ GstBuffer *buffer = gst_buffer_new_allocate (NULL, 4 * width * height, NULL);
+
+ gst_buffer_map (buffer, &map, GST_MAP_READWRITE);
+ surface = cairo_image_surface_create_for_data (map.data,
+ CAIRO_FORMAT_ARGB32, width, height, width * 4);
+ cairo_state = cairo_create (surface);
+
+ /* clear surface */
+ cairo_set_operator (cairo_state, CAIRO_OPERATOR_CLEAR);
+ cairo_paint (cairo_state);
+ cairo_set_operator (cairo_state, CAIRO_OPERATOR_OVER);
+
+ cairo_save (cairo_state);
+ cairo_set_source_rgba (cairo_state, color.r / 255.0, color.g / 255.0,
+ color.b / 255.0, color.a / 255.0);
+ cairo_paint (cairo_state);
+ cairo_restore (cairo_state);
+ cairo_destroy (cairo_state);
+ cairo_surface_destroy (surface);
+ gst_buffer_unmap (buffer, &map);
+
+ return buffer;
+}
+
+
+typedef struct
+{
+ guint first_char;
+ guint last_char;
+} TextRange;
+
+static void
+_text_range_free (TextRange * range)
+{
+ g_slice_free (TextRange, range);
+}
+
+
+/* Choose fonts for generic fontnames based upon IMSC1 and HbbTV specs. */
+static gchar *
+gst_ttml_render_resolve_generic_fontname (const gchar * name)
+{
+ if ((g_strcmp0 (name, "default") == 0)) {
+ return
+ g_strdup ("TiresiasScreenfont,Liberation Mono,Courier New,monospace");
+ } else if ((g_strcmp0 (name, "monospace") == 0)) {
+ return g_strdup ("Letter Gothic,Liberation Mono,Courier New,monospace");
+ } else if ((g_strcmp0 (name, "sansSerif") == 0)) {
+ return g_strdup ("TiresiasScreenfont,sans");
+ } else if ((g_strcmp0 (name, "serif") == 0)) {
+ return g_strdup ("serif");
+ } else if ((g_strcmp0 (name, "monospaceSansSerif") == 0)) {
+ return g_strdup ("Letter Gothic,monospace");
+ } else if ((g_strcmp0 (name, "monospaceSerif") == 0)) {
+ return g_strdup ("Courier New,Liberation Mono,monospace");
+ } else if ((g_strcmp0 (name, "proportionalSansSerif") == 0)) {
+ return g_strdup ("TiresiasScreenfont,Arial,Helvetica,Liberation Sans,sans");
+ } else if ((g_strcmp0 (name, "proportionalSerif") == 0)) {
+ return g_strdup ("serif");
+ } else {
+ return NULL;
+ }
+}
+
+
+static gchar *
+gst_ttml_render_get_text_from_buffer (GstBuffer * buf, guint index)
+{
+ GstMapInfo map;
+ GstMemory *mem;
+ gchar *buf_text = NULL;
+
+ mem = gst_buffer_get_memory (buf, index);
+ if (!mem) {
+ GST_CAT_ERROR (ttmlrender_debug, "Failed to access memory at index %u.",
+ index);
+ return NULL;
+ }
+
+ if (!gst_memory_map (mem, &map, GST_MAP_READ)) {
+ GST_CAT_ERROR (ttmlrender_debug, "Failed to map memory at index %u.",
+ index);
+ goto map_fail;
+ }
+
+ buf_text = g_strndup ((const gchar *) map.data, map.size);
+ if (!g_utf8_validate (buf_text, -1, NULL)) {
+ GST_CAT_ERROR (ttmlrender_debug, "Text in buffer us not valid UTF-8");
+ g_free (buf_text);
+ buf_text = NULL;
+ }
+
+ gst_memory_unmap (mem, &map);
+map_fail:
+ gst_memory_unref (mem);
+ return buf_text;
+}
+
+
+typedef struct
+{
+ const GstSubtitleElement *element;
+ gchar *text;
+} UnifiedElement;
+
+
+static void
+_unified_element_free (UnifiedElement * unified_element)
+{
+ g_free (unified_element->text);
+ g_slice_free (UnifiedElement, unified_element);
+}
+
+
+typedef struct
+{
+ GPtrArray *unified_elements;
+} UnifiedBlock;
+
+
+static void
+_unified_block_free (UnifiedBlock * unified_block)
+{
+ g_ptr_array_unref (unified_block->unified_elements);
+ g_slice_free (UnifiedBlock, unified_block);
+}
+
+
+static UnifiedElement *
+_unified_block_get_element (UnifiedBlock * block, guint index)
+{
+ if (index >= block->unified_elements->len)
+ return NULL;
+ else
+ return g_ptr_array_index (block->unified_elements, index);
+}
+
+
+static void
+gst_ttml_render_handle_whitespace (UnifiedBlock * block)
+{
+ UnifiedElement *last = NULL;
+ UnifiedElement *cur = _unified_block_get_element (block, 0);
+ UnifiedElement *next = _unified_block_get_element (block, 1);
+ guint i;
+
+ for (i = 2; cur; ++i) {
+ if (cur->element->suppress_whitespace) {
+ if (!last || (g_strcmp0 (last->text, "\n") == 0)) {
+ /* Strip leading whitespace. */
+ if (cur->text[0] == 0x20) {
+ gchar *tmp = cur->text;
+ GST_CAT_LOG (ttmlrender_debug, "Stripping leading whitespace.");
+ cur->text = g_strdup (cur->text + 1);
+ g_free (tmp);
+ }
+ }
+ if (!next || (g_strcmp0 (next->text, "\n") == 0)) {
+ /* Strip trailing whitespace. */
+ if (cur->text[strlen (cur->text) - 1] == 0x20) {
+ gchar *tmp = cur->text;
+ GST_CAT_LOG (ttmlrender_debug, "Stripping trailing whitespace.");
+ cur->text = g_strndup (cur->text, strlen (cur->text) - 1);
+ g_free (tmp);
+ }
+ }
+ }
+ last = cur;
+ cur = next;
+ next = _unified_block_get_element (block, i);
+ }
+}
+
+
+static UnifiedBlock *
+gst_ttml_render_unify_block (const GstSubtitleBlock * block, GstBuffer * buf)
+{
+ guint i;
+ UnifiedBlock *ret = g_slice_new0 (UnifiedBlock);
+ ret->unified_elements =
+ g_ptr_array_new_with_free_func ((GDestroyNotify) _unified_element_free);
+
+ for (i = 0; i < gst_subtitle_block_get_element_count (block); ++i) {
+ UnifiedElement *ue = g_slice_new0 (UnifiedElement);
+ ue->element = gst_subtitle_block_get_element (block, i);
+ ue->text =
+ gst_ttml_render_get_text_from_buffer (buf, ue->element->text_index);
+ g_ptr_array_add (ret->unified_elements, ue);
+ }
+ return ret;
+}
+
+
+/* From the elements within @block, generate a string of the subtitle text
+ * marked-up using pango-markup. Also, store the ranges of characters belonging
+ * to the text of each element in @text_ranges. */
+static gchar *
+gst_ttml_render_generate_marked_up_string (GstTtmlRender * render,
+ const GstSubtitleBlock * block, GstBuffer * text_buf,
+ GPtrArray * text_ranges)
+{
+ gchar *escaped_text, *joined_text, *old_text, *font_family, *font_size,
+ *fgcolor;
+ const gchar *font_style, *font_weight, *underline;
+ guint total_text_length = 0U;
+ guint element_count = gst_subtitle_block_get_element_count (block);
+ UnifiedBlock *unified_block;
+ guint i;
+
+ joined_text = g_strdup ("");
+ unified_block = gst_ttml_render_unify_block (block, text_buf);
+ gst_ttml_render_handle_whitespace (unified_block);
+
+ for (i = 0; i < element_count; ++i) {
+ TextRange *range = g_slice_new0 (TextRange);
+ UnifiedElement *unified_element =
+ _unified_block_get_element (unified_block, i);
+
+ escaped_text = g_markup_escape_text (unified_element->text, -1);
+ GST_CAT_DEBUG (ttmlrender_debug, "Escaped text is: \"%s\"", escaped_text);
+ range->first_char = total_text_length;
+
+ fgcolor =
+ gst_ttml_render_color_to_string (unified_element->element->
+ style_set->color);
+ font_size =
+ g_strdup_printf ("%u",
+ (guint) (round (unified_element->element->style_set->font_size *
+ render->height)));
+ font_family =
+ gst_ttml_render_resolve_generic_fontname (unified_element->
+ element->style_set->font_family);
+ if (!font_family)
+ font_family = g_strdup (unified_element->element->style_set->font_family);
+ font_style =
+ (unified_element->element->style_set->font_style ==
+ GST_SUBTITLE_FONT_STYLE_NORMAL) ? "normal" : "italic";
+ font_weight =
+ (unified_element->element->style_set->font_weight ==
+ GST_SUBTITLE_FONT_WEIGHT_NORMAL) ? "normal" : "bold";
+ underline =
+ (unified_element->element->style_set->text_decoration ==
+ GST_SUBTITLE_TEXT_DECORATION_UNDERLINE) ? "single" : "none";
+
+ old_text = joined_text;
+ joined_text = g_strconcat (joined_text,
+ "<span "
+ "fgcolor=\"", fgcolor, "\" ",
+ "font=\"", font_size, "px\" ",
+ "font_family=\"", font_family, "\" ",
+ "font_style=\"", font_style, "\" ",
+ "font_weight=\"", font_weight, "\" ",
+ "underline=\"", underline, "\" ", ">", escaped_text, "</span>", NULL);
+ GST_CAT_DEBUG (ttmlrender_debug, "Joined text is now: %s", joined_text);
+
+ total_text_length += strlen (unified_element->text);
+ range->last_char = total_text_length - 1;
+ GST_CAT_DEBUG (ttmlrender_debug,
+ "First character index: %u; last character " "index: %u",
+ range->first_char, range->last_char);
+ g_ptr_array_insert (text_ranges, i, range);
+
+ g_free (old_text);
+ g_free (escaped_text);
+ g_free (fgcolor);
+ g_free (font_family);
+ g_free (font_size);
+ }
+
+ _unified_block_free (unified_block);
+ return joined_text;
+}
+
+
+/* Render the text in a pango-markup string. */
+static GstTtmlRenderRenderedText *
+gst_ttml_render_draw_text (GstTtmlRender * render, const gchar * text,
+ guint max_width, PangoAlignment alignment, guint line_height,
+ guint max_font_size, gboolean wrap)
+{
+ GstTtmlRenderClass *class;
+ GstTtmlRenderRenderedText *ret;
+ cairo_surface_t *surface, *cropped_surface;
+ cairo_t *cairo_state, *cropped_state;
+ GstMapInfo map;
+ PangoRectangle logical_rect, ink_rect;
+ gint spacing = 0;
+ guint buf_width, buf_height;
+ gint stride;
+ PangoLayoutLine *line;
+ PangoRectangle line_extents;
+ gint bounding_box_x1, bounding_box_x2, bounding_box_y1, bounding_box_y2;
+
+ ret = g_slice_new0 (GstTtmlRenderRenderedText);
+ ret->text_image = gst_ttml_render_rendered_image_new_empty ();
+
+ class = GST_TTML_RENDER_GET_CLASS (render);
+ ret->layout = pango_layout_new (class->pango_context);
+
+ pango_layout_set_markup (ret->layout, text, strlen (text));
+ GST_CAT_DEBUG (ttmlrender_debug, "Layout text: %s",
+ pango_layout_get_text (ret->layout));
+ if (wrap) {
+ pango_layout_set_width (ret->layout, max_width * PANGO_SCALE);
+ pango_layout_set_wrap (ret->layout, PANGO_WRAP_WORD_CHAR);
+ } else {
+ pango_layout_set_width (ret->layout, -1);
+ }
+
+ pango_layout_set_alignment (ret->layout, alignment);
+ line = pango_layout_get_line_readonly (ret->layout, 0);
+ pango_layout_line_get_pixel_extents (line, NULL, &line_extents);
+
+ GST_CAT_LOG (ttmlrender_debug, "Requested line_height: %u", line_height);
+ spacing = line_height - line_extents.height;
+ pango_layout_set_spacing (ret->layout, PANGO_SCALE * spacing);
+ GST_CAT_LOG (ttmlrender_debug, "Line spacing set to %d",
+ pango_layout_get_spacing (ret->layout) / PANGO_SCALE);
+
+ pango_layout_get_pixel_extents (ret->layout, &ink_rect, &logical_rect);
+ GST_CAT_DEBUG (ttmlrender_debug, "logical_rect.x: %d logical_rect.y: %d "
+ "logical_rect.width: %d logical_rect.height: %d", logical_rect.x,
+ logical_rect.y, logical_rect.width, logical_rect.height);
+
+ bounding_box_x1 = MIN (logical_rect.x, ink_rect.x);
+ bounding_box_x2 = MAX (logical_rect.x + logical_rect.width,
+ ink_rect.x + ink_rect.width);
+ bounding_box_y1 = MIN (logical_rect.y, ink_rect.y);
+ bounding_box_y2 = MAX (logical_rect.y + logical_rect.height,
+ ink_rect.y + ink_rect.height);
+
+ surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, bounding_box_x2,
+ bounding_box_y2);
+ cairo_state = cairo_create (surface);
+ cairo_set_operator (cairo_state, CAIRO_OPERATOR_CLEAR);
+ cairo_paint (cairo_state);
+ cairo_set_operator (cairo_state, CAIRO_OPERATOR_OVER);
+
+ /* Render layout. */
+ cairo_save (cairo_state);
+ pango_cairo_show_layout (cairo_state, ret->layout);
+ cairo_restore (cairo_state);
+
+ buf_width = bounding_box_x2 - bounding_box_x1;
+ buf_height = (bounding_box_y2 - bounding_box_y1) + spacing;
+ GST_CAT_DEBUG (ttmlrender_debug, "Output buffer width: %u height: %u",
+ buf_width, buf_height);
+
+ /* Depending on whether the text is wrapped and its alignment, the image
+ * created by rendering a PangoLayout will contain more than just the
+ * rendered text: it may also contain blankspace around the rendered text.
+ * The following code crops blankspace from around the rendered text,
+ * returning only the rendered text itself in a GstBuffer. */
+ ret->text_image->image =
+ gst_buffer_new_allocate (NULL, 4 * buf_width * buf_height, NULL);
+ gst_buffer_memset (ret->text_image->image, 0, 0U, 4 * buf_width * buf_height);
+ gst_buffer_map (ret->text_image->image, &map, GST_MAP_READWRITE);
+
+ stride = cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, buf_width);
+ cropped_surface =
+ cairo_image_surface_create_for_data (map.data, CAIRO_FORMAT_ARGB32,
+ buf_width, buf_height, stride);
+ cropped_state = cairo_create (cropped_surface);
+ cairo_set_source_surface (cropped_state, surface, -bounding_box_x1,
+ -(bounding_box_y1 - spacing / 2.0));
+ cairo_rectangle (cropped_state, 0, 0, buf_width, buf_height);
+ cairo_fill (cropped_state);
+
+ cairo_destroy (cairo_state);
+ cairo_surface_destroy (surface);
+ cairo_destroy (cropped_state);
+ cairo_surface_destroy (cropped_surface);
+ gst_buffer_unmap (ret->text_image->image, &map);
+
+ ret->text_image->width = buf_width;
+ ret->text_image->height = buf_height;
+ ret->horiz_offset = bounding_box_x1;
+
+ return ret;
+}
+
+
+/* If any of an array of elements has line wrapping enabled, return TRUE. */
+static gboolean
+gst_ttml_render_elements_are_wrapped (GPtrArray * elements)
+{
+ GstSubtitleElement *element;
+ guint i;
+
+ for (i = 0; i < elements->len; ++i) {
+ element = g_ptr_array_index (elements, i);
+ if (element->style_set->wrap_option == GST_SUBTITLE_WRAPPING_ON)
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+
+/* Return the maximum font size used in an array of elements. */
+static gdouble
+gst_ttml_render_get_max_font_size (GPtrArray * elements)
+{
+ GstSubtitleElement *element;
+ guint i;
+ gdouble max_size = 0.0;
+
+ for (i = 0; i < elements->len; ++i) {
+ element = g_ptr_array_index (elements, i);
+ if (element->style_set->font_size > max_size)
+ max_size = element->style_set->font_size;
+ }
+
+ return max_size;
+}
+
+
+static GstTtmlRenderRenderedImage *
+gst_ttml_render_rendered_image_new (GstBuffer * image, gint x, gint y,
+ guint width, guint height)
+{
+ GstTtmlRenderRenderedImage *ret;
+
+ ret = g_slice_new0 (GstTtmlRenderRenderedImage);
+
+ ret->image = image;
+ ret->x = x;
+ ret->y = y;
+ ret->width = width;
+ ret->height = height;
+
+ return ret;
+}
+
+
+static GstTtmlRenderRenderedImage *
+gst_ttml_render_rendered_image_new_empty (void)
+{
+ return gst_ttml_render_rendered_image_new (NULL, 0, 0, 0, 0);
+}
+
+
+static inline GstTtmlRenderRenderedImage *
+gst_ttml_render_rendered_image_copy (GstTtmlRenderRenderedImage * image)
+{
+ GstTtmlRenderRenderedImage *ret = g_slice_new0 (GstTtmlRenderRenderedImage);
+
+ ret->image = gst_buffer_ref (image->image);
+ ret->x = image->x;
+ ret->y = image->y;
+ ret->width = image->width;
+ ret->height = image->height;
+
+ return ret;
+}
+
+
+static void
+gst_ttml_render_rendered_image_free (GstTtmlRenderRenderedImage * image)
+{
+ if (!image)
+ return;
+ gst_buffer_unref (image->image);
+ g_slice_free (GstTtmlRenderRenderedImage, image);
+}
+
+
+/* The order of arguments is significant: @image2 will be rendered on top of
+ * @image1. */
+static GstTtmlRenderRenderedImage *
+gst_ttml_render_rendered_image_combine (GstTtmlRenderRenderedImage * image1,
+ GstTtmlRenderRenderedImage * image2)
+{
+ GstTtmlRenderRenderedImage *ret;
+ GstMapInfo map1, map2, map_dest;
+ cairo_surface_t *sfc1, *sfc2, *sfc_dest;
+ cairo_t *state_dest;
+
+ if (image1 && !image2)
+ return gst_ttml_render_rendered_image_copy (image1);
+ if (image2 && !image1)
+ return gst_ttml_render_rendered_image_copy (image2);
+
+ ret = g_slice_new0 (GstTtmlRenderRenderedImage);
+
+ /* Work out dimensions of combined image. */
+ ret->x = MIN (image1->x, image2->x);
+ ret->y = MIN (image1->y, image2->y);
+ ret->width = MAX (image1->x + image1->width, image2->x + image2->width)
+ - ret->x;
+ ret->height = MAX (image1->y + image1->height, image2->y + image2->height)
+ - ret->y;
+
+ GST_CAT_LOG (ttmlrender_debug, "Dimensions of combined image: x:%u y:%u "
+ "width:%u height:%u", ret->x, ret->y, ret->width, ret->height);
+
+ /* Create cairo_surface from src images. */
+ gst_buffer_map (image1->image, &map1, GST_MAP_READ);
+ sfc1 =
+ cairo_image_surface_create_for_data (map1.data, CAIRO_FORMAT_ARGB32,
+ image1->width, image1->height,
+ cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, image1->width));
+
+ gst_buffer_map (image2->image, &map2, GST_MAP_READ);
+ sfc2 =
+ cairo_image_surface_create_for_data (map2.data, CAIRO_FORMAT_ARGB32,
+ image2->width, image2->height,
+ cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, image2->width));
+
+ /* Create cairo_surface for resultant image. */
+ ret->image = gst_buffer_new_allocate (NULL, 4 * ret->width * ret->height,
+ NULL);
+ gst_buffer_memset (ret->image, 0, 0U, 4 * ret->width * ret->height);
+ gst_buffer_map (ret->image, &map_dest, GST_MAP_READWRITE);
+ sfc_dest =
+ cairo_image_surface_create_for_data (map_dest.data, CAIRO_FORMAT_ARGB32,
+ ret->width, ret->height,
+ cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, ret->width));
+ state_dest = cairo_create (sfc_dest);
+
+ /* Blend image1 into destination surface. */
+ cairo_set_source_surface (state_dest, sfc1, image1->x - ret->x,
+ image1->y - ret->y);
+ cairo_rectangle (state_dest, image1->x - ret->x, image1->y - ret->y,
+ image1->width, image1->height);
+ cairo_fill (state_dest);
+
+ /* Blend image2 into destination surface. */
+ cairo_set_source_surface (state_dest, sfc2, image2->x - ret->x,
+ image2->y - ret->y);
+ cairo_rectangle (state_dest, image2->x - ret->x, image2->y - ret->y,
+ image2->width, image2->height);
+ cairo_fill (state_dest);
+
+ /* Return destination image. */
+ cairo_destroy (state_dest);
+ cairo_surface_destroy (sfc1);
+ cairo_surface_destroy (sfc2);
+ cairo_surface_destroy (sfc_dest);
+ gst_buffer_unmap (image1->image, &map1);
+ gst_buffer_unmap (image2->image, &map2);
+ gst_buffer_unmap (ret->image, &map_dest);
+
+ return ret;
+}
+
+
+static GstTtmlRenderRenderedImage *
+gst_ttml_render_rendered_image_crop (GstTtmlRenderRenderedImage * image,
+ gint x, gint y, guint width, guint height)
+{
+ GstTtmlRenderRenderedImage *ret;
+ GstMapInfo map_src, map_dest;
+ cairo_surface_t *sfc_src, *sfc_dest;
+ cairo_t *state_dest;
+
+ if ((x <= image->x) && (y <= image->y) && (width >= image->width)
+ && (height >= image->height))
+ return gst_ttml_render_rendered_image_copy (image);
+
+ if (image->x >= (x + (gint) width)
+ || (image->x + (gint) image->width) <= x
+ || image->y >= (y + (gint) height)
+ || (image->y + (gint) image->height) <= y) {
+ GST_CAT_WARNING (ttmlrender_debug,
+ "Crop rectangle doesn't intersect image.");
+ return NULL;
+ }
+
+ ret = g_slice_new0 (GstTtmlRenderRenderedImage);
+
+ ret->x = MAX (image->x, x);
+ ret->y = MAX (image->y, y);
+ ret->width = MIN ((image->x + image->width) - ret->x, (x + width) - ret->x);
+ ret->height = MIN ((image->y + image->height) - ret->y,
+ (y + height) - ret->y);
+
+ GST_CAT_LOG (ttmlrender_debug, "Dimensions of cropped image: x:%u y:%u "
+ "width:%u height:%u", ret->x, ret->y, ret->width, ret->height);
+
+ /* Create cairo_surface from src image. */
+ gst_buffer_map (image->image, &map_src, GST_MAP_READ);
+ sfc_src =
+ cairo_image_surface_create_for_data (map_src.data, CAIRO_FORMAT_ARGB32,
+ image->width, image->height,
+ cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, image->width));
+
+ /* Create cairo_surface for cropped image. */
+ ret->image = gst_buffer_new_allocate (NULL, 4 * ret->width * ret->height,
+ NULL);
+ gst_buffer_memset (ret->image, 0, 0U, 4 * ret->width * ret->height);
+ gst_buffer_map (ret->image, &map_dest, GST_MAP_READWRITE);
+ sfc_dest =
+ cairo_image_surface_create_for_data (map_dest.data, CAIRO_FORMAT_ARGB32,
+ ret->width, ret->height,
+ cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, ret->width));
+ state_dest = cairo_create (sfc_dest);
+
+ /* Copy section of image1 into destination surface. */
+ cairo_set_source_surface (state_dest, sfc_src, (image->x - ret->x),
+ (image->y - ret->y));
+ cairo_rectangle (state_dest, 0, 0, ret->width, ret->height);
+ cairo_fill (state_dest);
+
+ cairo_destroy (state_dest);
+ cairo_surface_destroy (sfc_src);
+ cairo_surface_destroy (sfc_dest);
+ gst_buffer_unmap (image->image, &map_src);
+ gst_buffer_unmap (ret->image, &map_dest);
+
+ return ret;
+}
+
+
+static gboolean
+gst_ttml_render_color_is_transparent (GstSubtitleColor * color)
+{
+ return (color->a == 0);
+}
+
+
+/* Render the background rectangles to be placed behind each element. */
+static GstTtmlRenderRenderedImage *
+gst_ttml_render_render_element_backgrounds (const GstSubtitleBlock * block,
+ GPtrArray * char_ranges, PangoLayout * layout, guint origin_x,
+ guint origin_y, guint line_height, guint line_padding, guint horiz_offset)
+{
+ gint first_line, last_line, cur_line;
+ guint padding;
+ PangoLayoutLine *line;
+ PangoRectangle first_char_pos, last_char_pos, line_extents;
+ TextRange *range;
+ const GstSubtitleElement *element;
+ guint rect_width;
+ GstBuffer *rectangle;
+ guint first_char_start, last_char_end;
+ guint i;
+ GstTtmlRenderRenderedImage *ret = NULL;
+
+ for (i = 0; i < char_ranges->len; ++i) {
+ range = g_ptr_array_index (char_ranges, i);
+ element = gst_subtitle_block_get_element (block, i);
+
+ GST_CAT_LOG (ttmlrender_debug, "First char index: %u Last char index: %u",
+ range->first_char, range->last_char);
+ pango_layout_index_to_pos (layout, range->first_char, &first_char_pos);
+ pango_layout_index_to_pos (layout, range->last_char, &last_char_pos);
+ pango_layout_index_to_line_x (layout, range->first_char, 1,
+ &first_line, NULL);
+ pango_layout_index_to_line_x (layout, range->last_char, 0,
+ &last_line, NULL);
+
+ first_char_start = PANGO_PIXELS (first_char_pos.x) - horiz_offset;
+ last_char_end = PANGO_PIXELS (last_char_pos.x + last_char_pos.width)
+ - horiz_offset;
+
+ GST_CAT_LOG (ttmlrender_debug, "First char start: %u Last char end: %u",
+ first_char_start, last_char_end);
+ GST_CAT_LOG (ttmlrender_debug, "First line: %u Last line: %u", first_line,
+ last_line);
+
+ for (cur_line = first_line; cur_line <= last_line; ++cur_line) {
+ guint line_start, line_end;
+ guint area_start, area_end;
+ gint first_char_index;
+ PangoRectangle line_pos;
+ padding = 0;
+
+ line = pango_layout_get_line (layout, cur_line);
+ pango_layout_line_get_pixel_extents (line, NULL, &line_extents);
+
+ pango_layout_line_x_to_index (line, 0, &first_char_index, NULL);
+ pango_layout_index_to_pos (layout, first_char_index, &line_pos);
+ GST_CAT_LOG (ttmlrender_debug, "First char index:%d position_X:%d "
+ "position_Y:%d", first_char_index, PANGO_PIXELS (line_pos.x),
+ PANGO_PIXELS (line_pos.y));
+
+ line_start = PANGO_PIXELS (line_pos.x) - horiz_offset;
+ line_end = (PANGO_PIXELS (line_pos.x) + line_extents.width)
+ - horiz_offset;
+
+ GST_CAT_LOG (ttmlrender_debug, "line_extents.x:%d line_extents.y:%d "
+ "line_extents.width:%d line_extents.height:%d", line_extents.x,
+ line_extents.y, line_extents.width, line_extents.height);
+ GST_CAT_LOG (ttmlrender_debug, "cur_line:%u line start:%u line end:%u "
+ "first_char_start: %u last_char_end: %u", cur_line, line_start,
+ line_end, first_char_start, last_char_end);
+
+ if ((cur_line == first_line) && (first_char_start != line_start)) {
+ area_start = first_char_start + line_padding;
+ GST_CAT_LOG (ttmlrender_debug,
+ "First line, but there is preceding text in line.");
+ } else {
+ GST_CAT_LOG (ttmlrender_debug,
+ "Area contains first text on the line; adding padding...");
+ ++padding;
+ area_start = line_start;
+ }
+
+ if ((cur_line == last_line) && (last_char_end != line_end)) {
+ GST_CAT_LOG (ttmlrender_debug,
+ "Last line, but there is following text in line.");
+ area_end = last_char_end + line_padding;
+ } else {
+ GST_CAT_LOG (ttmlrender_debug,
+ "Area contains last text on the line; adding padding...");
+ ++padding;
+ area_end = line_end + (2 * line_padding);
+ }
+
+ rect_width = (area_end - area_start);
+
+ if (rect_width > 0) { /* <br>s will result in zero-width rectangle */
+ GstTtmlRenderRenderedImage *image, *tmp;
+ rectangle = gst_ttml_render_draw_rectangle (rect_width, line_height,
+ element->style_set->background_color);
+ image = gst_ttml_render_rendered_image_new (rectangle,
+ origin_x + area_start,
+ origin_y + (cur_line * line_height), rect_width, line_height);
+ tmp = ret;
+ ret = gst_ttml_render_rendered_image_combine (ret, image);
+ if (tmp)
+ gst_ttml_render_rendered_image_free (tmp);
+ gst_ttml_render_rendered_image_free (image);
+ }
+ }
+ }
+
+ return ret;
+}
+
+
+static PangoAlignment
+gst_ttml_render_get_alignment (GstSubtitleStyleSet * style_set)
+{
+ PangoAlignment align = PANGO_ALIGN_LEFT;
+
+ switch (style_set->multi_row_align) {
+ case GST_SUBTITLE_MULTI_ROW_ALIGN_START:
+ align = PANGO_ALIGN_LEFT;
+ break;
+ case GST_SUBTITLE_MULTI_ROW_ALIGN_CENTER:
+ align = PANGO_ALIGN_CENTER;
+ break;
+ case GST_SUBTITLE_MULTI_ROW_ALIGN_END:
+ align = PANGO_ALIGN_RIGHT;
+ break;
+ case GST_SUBTITLE_MULTI_ROW_ALIGN_AUTO:
+ switch (style_set->text_align) {
+ case GST_SUBTITLE_TEXT_ALIGN_START:
+ case GST_SUBTITLE_TEXT_ALIGN_LEFT:
+ align = PANGO_ALIGN_LEFT;
+ break;
+ case GST_SUBTITLE_TEXT_ALIGN_CENTER:
+ align = PANGO_ALIGN_CENTER;
+ break;
+ case GST_SUBTITLE_TEXT_ALIGN_END:
+ case GST_SUBTITLE_TEXT_ALIGN_RIGHT:
+ align = PANGO_ALIGN_RIGHT;
+ break;
+ default:
+ GST_CAT_ERROR (ttmlrender_debug, "Illegal textAlign value (%d)",
+ style_set->text_align);
+ break;
+ }
+ break;
+ default:
+ GST_CAT_ERROR (ttmlrender_debug, "Illegal multiRowAlign value (%d)",
+ style_set->multi_row_align);
+ break;
+ }
+ return align;
+}
+
+
+static GstTtmlRenderRenderedImage *
+gst_ttml_render_stitch_blocks (GList * blocks)
+{
+ guint vert_offset = 0;
+ GList *block_entry;
+ GstTtmlRenderRenderedImage *ret = NULL;
+
+ for (block_entry = g_list_first (blocks); block_entry;
+ block_entry = block_entry->next) {
+ GstTtmlRenderRenderedImage *block, *tmp;
+ block = (GstTtmlRenderRenderedImage *) block_entry->data;
+ tmp = ret;
+
+ block->y += vert_offset;
+ GST_CAT_LOG (ttmlrender_debug, "Rendering block at vertical offset %u",
+ vert_offset);
+ vert_offset = block->y + block->height;
+ ret = gst_ttml_render_rendered_image_combine (ret, block);
+ if (tmp)
+ gst_ttml_render_rendered_image_free (tmp);
+ }
+
+ if (ret) {
+ GST_CAT_LOG (ttmlrender_debug, "Height of stitched image: %u", ret->height);
+ ret->image = gst_buffer_make_writable (ret->image);
+ }
+ return ret;
+}
+
+
+static void
+gst_ttml_render_rendered_text_free (GstTtmlRenderRenderedText * text)
+{
+ if (text->text_image)
+ gst_ttml_render_rendered_image_free (text->text_image);
+ if (text->layout)
+ g_object_unref (text->layout);
+ g_slice_free (GstTtmlRenderRenderedText, text);
+}
+
+
+static GstTtmlRenderRenderedImage *
+gst_ttml_render_render_text_block (GstTtmlRender * render,
+ const GstSubtitleBlock * block, GstBuffer * text_buf, guint width,
+ gboolean overflow)
+{
+ GPtrArray *char_ranges =
+ g_ptr_array_new_with_free_func ((GDestroyNotify) _text_range_free);
+ gchar *marked_up_string;
+ PangoAlignment alignment;
+ guint max_font_size;
+ guint line_height;
+ guint line_padding;
+ gint text_offset = 0;
+ GstTtmlRenderRenderedText *rendered_text;
+ GstTtmlRenderRenderedImage *backgrounds = NULL;
+ GstTtmlRenderRenderedImage *ret;
+
+ /* Join text from elements to form a single marked-up string. */
+ marked_up_string = gst_ttml_render_generate_marked_up_string (render, block,
+ text_buf, char_ranges);
+
+ max_font_size = (guint) (gst_ttml_render_get_max_font_size (block->elements)
+ * render->height);
+ GST_CAT_DEBUG (ttmlrender_debug, "Max font size: %u", max_font_size);
+ line_height = (guint) round (block->style_set->line_height * max_font_size);
+
+ line_padding = (guint) (block->style_set->line_padding * render->width);
+ alignment = gst_ttml_render_get_alignment (block->style_set);
+
+ /* Render text to buffer. */
+ rendered_text = gst_ttml_render_draw_text (render, marked_up_string,
+ (width - (2 * line_padding)), alignment, line_height, max_font_size,
+ gst_ttml_render_elements_are_wrapped (block->elements));
+
+ switch (block->style_set->text_align) {
+ case GST_SUBTITLE_TEXT_ALIGN_START:
+ case GST_SUBTITLE_TEXT_ALIGN_LEFT:
+ text_offset = line_padding;
+ break;
+ case GST_SUBTITLE_TEXT_ALIGN_CENTER:
+ text_offset = ((gint) width - rendered_text->text_image->width);
+ text_offset /= 2;
+ break;
+ case GST_SUBTITLE_TEXT_ALIGN_END:
+ case GST_SUBTITLE_TEXT_ALIGN_RIGHT:
+ text_offset = (gint) width
+ - (rendered_text->text_image->width + line_padding);
+ break;
+ }
+
+ rendered_text->text_image->x = text_offset;
+
+ /* Render background rectangles, if any. */
+ backgrounds = gst_ttml_render_render_element_backgrounds (block, char_ranges,
+ rendered_text->layout, text_offset - line_padding, 0,
+ (guint) round (block->style_set->line_height * max_font_size),
+ line_padding, rendered_text->horiz_offset);
+
+ /* Render block background, if non-transparent. */
+ if (!gst_ttml_render_color_is_transparent (&block->style_set->
+ background_color)) {
+ GstTtmlRenderRenderedImage *block_background;
+ GstTtmlRenderRenderedImage *tmp = backgrounds;
+
+ GstBuffer *block_bg_image = gst_ttml_render_draw_rectangle (width,
+ backgrounds->height, block->style_set->background_color);
+ block_background = gst_ttml_render_rendered_image_new (block_bg_image, 0,
+ 0, width, backgrounds->height);
+ backgrounds = gst_ttml_render_rendered_image_combine (block_background,
+ backgrounds);
+ gst_ttml_render_rendered_image_free (tmp);
+ gst_ttml_render_rendered_image_free (block_background);
+ }
+
+ /* Combine text and background images. */
+ ret = gst_ttml_render_rendered_image_combine (backgrounds,
+ rendered_text->text_image);
+ gst_ttml_render_rendered_image_free (backgrounds);
+ gst_ttml_render_rendered_text_free (rendered_text);
+
+ g_free (marked_up_string);
+ g_ptr_array_unref (char_ranges);
+ GST_CAT_DEBUG (ttmlrender_debug, "block width: %u block height: %u",
+ ret->width, ret->height);
+ return ret;
+}
+
+
+static GstVideoOverlayComposition *
+gst_ttml_render_compose_overlay (GstTtmlRenderRenderedImage * image)
+{
+ GstVideoOverlayRectangle *rectangle;
+ GstVideoOverlayComposition *ret = NULL;
+
+ gst_buffer_add_video_meta (image->image, GST_VIDEO_FRAME_FLAG_NONE,
+ GST_VIDEO_OVERLAY_COMPOSITION_FORMAT_RGB, image->width, image->height);
+
+ rectangle = gst_video_overlay_rectangle_new_raw (image->image, image->x,
+ image->y, image->width, image->height,
+ GST_VIDEO_OVERLAY_FORMAT_FLAG_PREMULTIPLIED_ALPHA);
+
+ ret = gst_video_overlay_composition_new (rectangle);
+ gst_video_overlay_rectangle_unref (rectangle);
+ return ret;
+}
+
+
+static GstVideoOverlayComposition *
+gst_ttml_render_render_text_region (GstTtmlRender * render,
+ GstSubtitleRegion * region, GstBuffer * text_buf)
+{
+ GList *blocks = NULL;
+ guint region_x, region_y, region_width, region_height;
+ guint window_x, window_y, window_width, window_height;
+ guint padding_start, padding_end, padding_before, padding_after;
+ GstTtmlRenderRenderedImage *region_image = NULL;
+ GstTtmlRenderRenderedImage *blocks_image;
+ GstVideoOverlayComposition *ret = NULL;
+ guint i;
+
+ region_width = (guint) (round (region->style_set->extent_w * render->width));
+ region_height =
+ (guint) (round (region->style_set->extent_h * render->height));
+ region_x = (guint) (round (region->style_set->origin_x * render->width));
+ region_y = (guint) (round (region->style_set->origin_y * render->height));
+
+ padding_start =
+ (guint) (round (region->style_set->padding_start * render->width));
+ padding_end =
+ (guint) (round (region->style_set->padding_end * render->width));
+ padding_before =
+ (guint) (round (region->style_set->padding_before * render->height));
+ padding_after =
+ (guint) (round (region->style_set->padding_after * render->height));
+
+ /* "window" here refers to the section of the region that we're allowed to
+ * render into, i.e., the region minus padding. */
+ window_x = region_x + padding_start;
+ window_y = region_y + padding_before;
+ window_width = region_width - (padding_start + padding_end);
+ window_height = region_height - (padding_before + padding_after);
+
+ GST_CAT_DEBUG (ttmlrender_debug,
+ "Padding: start: %u end: %u before: %u after: %u",
+ padding_start, padding_end, padding_before, padding_after);
+
+ /* Render region background, if non-transparent. */
+ if (!gst_ttml_render_color_is_transparent (&region->style_set->
+ background_color)) {
+ GstBuffer *bg_rect;
+
+ bg_rect = gst_ttml_render_draw_rectangle (region_width, region_height,
+ region->style_set->background_color);
+ region_image = gst_ttml_render_rendered_image_new (bg_rect, region_x,
+ region_y, region_width, region_height);
+ }
+
+ /* Render each block and append to list. */
+ for (i = 0; i < gst_subtitle_region_get_block_count (region); ++i) {
+ const GstSubtitleBlock *block;
+ GstTtmlRenderRenderedImage *rendered_block;
+
+ block = gst_subtitle_region_get_block (region, i);
+ rendered_block = gst_ttml_render_render_text_block (render, block, text_buf,
+ window_width, TRUE);
+
+ blocks = g_list_append (blocks, rendered_block);
+ }
+
+ if (blocks) {
+ GstTtmlRenderRenderedImage *tmp;
+
+ blocks_image = gst_ttml_render_stitch_blocks (blocks);
+ g_list_free_full (blocks,
+ (GDestroyNotify) gst_ttml_render_rendered_image_free);
+ blocks_image->x += window_x;
+
+ switch (region->style_set->display_align) {
+ case GST_SUBTITLE_DISPLAY_ALIGN_BEFORE:
+ blocks_image->y = window_y;
+ break;
+ case GST_SUBTITLE_DISPLAY_ALIGN_CENTER:
+ blocks_image->y = region_y + ((gint) ((region_height + padding_before)
+ - (padding_after + blocks_image->height))) / 2;
+ break;
+ case GST_SUBTITLE_DISPLAY_ALIGN_AFTER:
+ blocks_image->y = (region_y + region_height)
+ - (padding_after + blocks_image->height);
+ break;
+ }
+
+ if ((region->style_set->overflow == GST_SUBTITLE_OVERFLOW_MODE_HIDDEN)
+ && ((blocks_image->height > window_height)
+ || (blocks_image->width > window_width))) {
+ GstTtmlRenderRenderedImage *tmp = blocks_image;
+ blocks_image = gst_ttml_render_rendered_image_crop (blocks_image,
+ window_x, window_y, window_width, window_height);
+ gst_ttml_render_rendered_image_free (tmp);
+ }
+
+ tmp = region_image;
+ region_image = gst_ttml_render_rendered_image_combine (region_image,
+ blocks_image);
+ if (tmp)
+ gst_ttml_render_rendered_image_free (tmp);
+ gst_ttml_render_rendered_image_free (blocks_image);
+ }
+
+ GST_CAT_DEBUG (ttmlrender_debug, "Height of rendered region: %u",
+ region_image->height);
+
+ ret = gst_ttml_render_compose_overlay (region_image);
+ gst_ttml_render_rendered_image_free (region_image);
+ return ret;
+}
+
+
+static GstFlowReturn
+gst_ttml_render_video_chain (GstPad * pad, GstObject * parent,
+ GstBuffer * buffer)
+{
+ GstTtmlRender *render;
+ GstFlowReturn ret = GST_FLOW_OK;
+ gboolean in_seg = FALSE;
+ guint64 start, stop, clip_start = 0, clip_stop = 0;
+ gchar *text = NULL;
+
+ render = GST_TTML_RENDER (parent);
+
+ if (!GST_BUFFER_TIMESTAMP_IS_VALID (buffer))
+ goto missing_timestamp;
+
+ /* ignore buffers that are outside of the current segment */
+ start = GST_BUFFER_TIMESTAMP (buffer);
+
+ if (!GST_BUFFER_DURATION_IS_VALID (buffer)) {
+ stop = GST_CLOCK_TIME_NONE;
+ } else {
+ stop = start + GST_BUFFER_DURATION (buffer);
+ }
+
+ GST_LOG_OBJECT (render, "%" GST_SEGMENT_FORMAT " BUFFER: ts=%"
+ GST_TIME_FORMAT ", end=%" GST_TIME_FORMAT, &render->segment,
+ GST_TIME_ARGS (start), GST_TIME_ARGS (stop));
+
+ /* segment_clip() will adjust start unconditionally to segment_start if
+ * no stop time is provided, so handle this ourselves */
+ if (stop == GST_CLOCK_TIME_NONE && start < render->segment.start)
+ goto out_of_segment;
+
+ in_seg = gst_segment_clip (&render->segment, GST_FORMAT_TIME, start, stop,
+ &clip_start, &clip_stop);
+
+ if (!in_seg)
+ goto out_of_segment;
+
+ /* if the buffer is only partially in the segment, fix up stamps */
+ if (clip_start != start || (stop != -1 && clip_stop != stop)) {
+ GST_DEBUG_OBJECT (render, "clipping buffer timestamp/duration to segment");
+ buffer = gst_buffer_make_writable (buffer);
+ GST_BUFFER_TIMESTAMP (buffer) = clip_start;
+ if (stop != -1)
+ GST_BUFFER_DURATION (buffer) = clip_stop - clip_start;
+ }
+
+ /* now, after we've done the clipping, fix up end time if there's no
+ * duration (we only use those estimated values internally though, we
+ * don't want to set bogus values on the buffer itself) */
+ if (stop == -1) {
+ if (render->info.fps_n && render->info.fps_d) {
+ GST_DEBUG_OBJECT (render, "estimating duration based on framerate");
+ stop = start + gst_util_uint64_scale_int (GST_SECOND,
+ render->info.fps_d, render->info.fps_n);
+ } else {
+ GST_LOG_OBJECT (render, "no duration, assuming minimal duration");
+ stop = start + 1; /* we need to assume some interval */
+ }
+ }
+
+ gst_object_sync_values (GST_OBJECT (render), GST_BUFFER_TIMESTAMP (buffer));
+
+wait_for_text_buf:
+
+ GST_TTML_RENDER_LOCK (render);
+
+ if (render->video_flushing)
+ goto flushing;
+
+ if (render->video_eos)
+ goto have_eos;
+
+ /* Text pad not linked; push input video frame */
+ if (!render->text_linked) {
+ GST_LOG_OBJECT (render, "Text pad not linked");
+ GST_TTML_RENDER_UNLOCK (render);
+ ret = gst_pad_push (render->srcpad, buffer);
+ goto not_linked;
+ }
+
+ /* Text pad linked, check if we have a text buffer queued */
+ if (render->text_buffer) {
+ gboolean pop_text = FALSE, valid_text_time = TRUE;
+ GstClockTime text_start = GST_CLOCK_TIME_NONE;
+ GstClockTime text_end = GST_CLOCK_TIME_NONE;
+ GstClockTime text_running_time = GST_CLOCK_TIME_NONE;
+ GstClockTime text_running_time_end = GST_CLOCK_TIME_NONE;
+ GstClockTime vid_running_time, vid_running_time_end;
+
+ /* if the text buffer isn't stamped right, pop it off the
+ * queue and display it for the current video frame only */
+ if (!GST_BUFFER_TIMESTAMP_IS_VALID (render->text_buffer) ||
+ !GST_BUFFER_DURATION_IS_VALID (render->text_buffer)) {
+ GST_WARNING_OBJECT (render,
+ "Got text buffer with invalid timestamp or duration");
+ pop_text = TRUE;
+ valid_text_time = FALSE;
+ } else {
+ text_start = GST_BUFFER_TIMESTAMP (render->text_buffer);
+ text_end = text_start + GST_BUFFER_DURATION (render->text_buffer);
+ }
+
+ vid_running_time =
+ gst_segment_to_running_time (&render->segment, GST_FORMAT_TIME, start);
+ vid_running_time_end =
+ gst_segment_to_running_time (&render->segment, GST_FORMAT_TIME, stop);
+
+ /* If timestamp and duration are valid */
+ if (valid_text_time) {
+ text_running_time =
+ gst_segment_to_running_time (&render->text_segment,
+ GST_FORMAT_TIME, text_start);
+ text_running_time_end =
+ gst_segment_to_running_time (&render->text_segment,
+ GST_FORMAT_TIME, text_end);
+ }
+
+ GST_LOG_OBJECT (render, "T: %" GST_TIME_FORMAT " - %" GST_TIME_FORMAT,
+ GST_TIME_ARGS (text_running_time),
+ GST_TIME_ARGS (text_running_time_end));
+ GST_LOG_OBJECT (render, "V: %" GST_TIME_FORMAT " - %" GST_TIME_FORMAT,
+ GST_TIME_ARGS (vid_running_time), GST_TIME_ARGS (vid_running_time_end));
+
+ /* Text too old or in the future */
+ if (valid_text_time && text_running_time_end <= vid_running_time) {
+ /* text buffer too old, get rid of it and do nothing */
+ GST_LOG_OBJECT (render, "text buffer too old, popping");
+ pop_text = FALSE;
+ gst_ttml_render_pop_text (render);
+ GST_TTML_RENDER_UNLOCK (render);
+ goto wait_for_text_buf;
+ } else if (valid_text_time && vid_running_time_end <= text_running_time) {
+ GST_LOG_OBJECT (render, "text in future, pushing video buf");
+ GST_TTML_RENDER_UNLOCK (render);
+ /* Push the video frame */
+ ret = gst_pad_push (render->srcpad, buffer);
+ } else {
+ if (render->need_render) {
+ GstSubtitleRegion *region = NULL;
+ GstSubtitleMeta *subtitle_meta = NULL;
+ guint i;
+
+ if (render->compositions) {
+ g_list_free_full (render->compositions,
+ (GDestroyNotify) gst_video_overlay_composition_unref);
+ render->compositions = NULL;
+ }
+
+ subtitle_meta = gst_buffer_get_subtitle_meta (render->text_buffer);
+ g_assert (subtitle_meta != NULL);
+
+ for (i = 0; i < subtitle_meta->regions->len; ++i) {
+ GstVideoOverlayComposition *composition;
+ region = g_ptr_array_index (subtitle_meta->regions, i);
+ g_assert (region != NULL);
+ composition = gst_ttml_render_render_text_region (render, region,
+ render->text_buffer);
+ render->compositions = g_list_append (render->compositions,
+ composition);
+ }
+ render->need_render = FALSE;
+ }
+
+ GST_TTML_RENDER_UNLOCK (render);
+ ret = gst_ttml_render_push_frame (render, buffer);
+
+ if (valid_text_time && text_running_time_end <= vid_running_time_end) {
+ GST_LOG_OBJECT (render, "text buffer not needed any longer");
+ pop_text = TRUE;
+ }
+ }
+ if (pop_text) {
+ GST_TTML_RENDER_LOCK (render);
+ gst_ttml_render_pop_text (render);
+ GST_TTML_RENDER_UNLOCK (render);
+ }
+ } else {
+ gboolean wait_for_text_buf = TRUE;
+
+ if (render->text_eos)
+ wait_for_text_buf = FALSE;
+
+ if (!render->wait_text)
+ wait_for_text_buf = FALSE;
+
+ /* Text pad linked, but no text buffer available - what now? */
+ if (render->text_segment.format == GST_FORMAT_TIME) {
+ GstClockTime text_start_running_time, text_position_running_time;
+ GstClockTime vid_running_time;
+
+ vid_running_time =
+ gst_segment_to_running_time (&render->segment, GST_FORMAT_TIME,
+ GST_BUFFER_TIMESTAMP (buffer));
+ text_start_running_time =
+ gst_segment_to_running_time (&render->text_segment,
+ GST_FORMAT_TIME, render->text_segment.start);
+ text_position_running_time =
+ gst_segment_to_running_time (&render->text_segment,
+ GST_FORMAT_TIME, render->text_segment.position);
+
+ if ((GST_CLOCK_TIME_IS_VALID (text_start_running_time) &&
+ vid_running_time < text_start_running_time) ||
+ (GST_CLOCK_TIME_IS_VALID (text_position_running_time) &&
+ vid_running_time < text_position_running_time)) {
+ wait_for_text_buf = FALSE;
+ }
+ }
+
+ if (wait_for_text_buf) {
+ GST_DEBUG_OBJECT (render, "no text buffer, need to wait for one");
+ GST_TTML_RENDER_WAIT (render);
+ GST_DEBUG_OBJECT (render, "resuming");
+ GST_TTML_RENDER_UNLOCK (render);
+ goto wait_for_text_buf;
+ } else {
+ GST_TTML_RENDER_UNLOCK (render);
+ GST_LOG_OBJECT (render, "no need to wait for a text buffer");
+ ret = gst_pad_push (render->srcpad, buffer);
+ }
+ }
+
+not_linked:
+ g_free (text);
+
+ /* Update position */
+ render->segment.position = clip_start;
+
+ return ret;
+
+missing_timestamp:
+ {
+ GST_WARNING_OBJECT (render, "buffer without timestamp, discarding");
+ gst_buffer_unref (buffer);
+ return GST_FLOW_OK;
+ }
+
+flushing:
+ {
+ GST_TTML_RENDER_UNLOCK (render);
+ GST_DEBUG_OBJECT (render, "flushing, discarding buffer");
+ gst_buffer_unref (buffer);
+ return GST_FLOW_FLUSHING;
+ }
+have_eos:
+ {
+ GST_TTML_RENDER_UNLOCK (render);
+ GST_DEBUG_OBJECT (render, "eos, discarding buffer");
+ gst_buffer_unref (buffer);
+ return GST_FLOW_EOS;
+ }
+out_of_segment:
+ {
+ GST_DEBUG_OBJECT (render, "buffer out of segment, discarding");
+ gst_buffer_unref (buffer);
+ return GST_FLOW_OK;
+ }
+}
+
+static GstStateChangeReturn
+gst_ttml_render_change_state (GstElement * element, GstStateChange transition)
+{
+ GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;
+ GstTtmlRender *render = GST_TTML_RENDER (element);
+
+ switch (transition) {
+ case GST_STATE_CHANGE_PAUSED_TO_READY:
+ GST_TTML_RENDER_LOCK (render);
+ render->text_flushing = TRUE;
+ render->video_flushing = TRUE;
+ /* pop_text will broadcast on the GCond and thus also make the video
+ * chain exit if it's waiting for a text buffer */
+ gst_ttml_render_pop_text (render);
+ GST_TTML_RENDER_UNLOCK (render);
+ break;
+ default:
+ break;
+ }
+
+ ret = parent_class->change_state (element, transition);
+ if (ret == GST_STATE_CHANGE_FAILURE)
+ return ret;
+
+ switch (transition) {
+ case GST_STATE_CHANGE_READY_TO_PAUSED:
+ GST_TTML_RENDER_LOCK (render);
+ render->text_flushing = FALSE;
+ render->video_flushing = FALSE;
+ render->video_eos = FALSE;
+ render->text_eos = FALSE;
+ gst_segment_init (&render->segment, GST_FORMAT_TIME);
+ gst_segment_init (&render->text_segment, GST_FORMAT_TIME);
+ GST_TTML_RENDER_UNLOCK (render);
+ break;
+ default:
+ break;
+ }
+
+ return ret;
+}
diff --git a/ext/ttml/gstttmlrender.h b/ext/ttml/gstttmlrender.h
new file mode 100644
index 000000000..9a1fba10f
--- /dev/null
+++ b/ext/ttml/gstttmlrender.h
@@ -0,0 +1,124 @@
+/* GStreamer
+ * Copyright (C) <1999> Erik Walthinsen <omega@cse.ogi.edu>
+ * Copyright (C) <2003> David Schleef <ds@schleef.org>
+ * Copyright (C) <2006> Julien Moutte <julien@moutte.net>
+ * Copyright (C) <2006> Zeeshan Ali <zeeshan.ali@nokia.com>
+ * Copyright (C) <2006-2008> Tim-Philipp Müller <tim centricular net>
+ * Copyright (C) <2009> Young-Ho Cha <ganadist@gmail.com>
+ * Copyright (C) <2015> British Broadcasting Corporation <dash@rd.bbc.co.uk>
+ *
+ * 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., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __GST_TTML_RENDER_H__
+#define __GST_TTML_RENDER_H__
+
+#include <gst/gst.h>
+#include <gst/video/video.h>
+#include <pango/pango.h>
+
+G_BEGIN_DECLS
+
+#define GST_TYPE_TTML_RENDER (gst_ttml_render_get_type())
+#define GST_TTML_RENDER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),\
+ GST_TYPE_TTML_RENDER, GstTtmlRender))
+#define GST_TTML_RENDER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),\
+ GST_TYPE_TTML_RENDER, \
+ GstTtmlRenderClass))
+#define GST_TTML_RENDER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj),\
+ GST_TYPE_TTML_RENDER, \
+ GstTtmlRenderClass))
+#define GST_IS_TTML_RENDER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),\
+ GST_TYPE_TTML_RENDER))
+#define GST_IS_TTML_RENDER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass),\
+ GST_TYPE_TTML_RENDER))
+
+typedef struct _GstTtmlRender GstTtmlRender;
+typedef struct _GstTtmlRenderClass GstTtmlRenderClass;
+typedef struct _GstTtmlRenderRenderedImage GstTtmlRenderRenderedImage;
+typedef struct _GstTtmlRenderRenderedText GstTtmlRenderRenderedText;
+
+struct _GstTtmlRenderRenderedImage {
+ GstBuffer *image;
+ gint x;
+ gint y;
+ guint width;
+ guint height;
+};
+
+struct _GstTtmlRenderRenderedText {
+ GstTtmlRenderRenderedImage *text_image;
+
+ /* In order to get the positions of characters within a paragraph rendered by
+ * pango we need to retain a reference to the PangoLayout object that was
+ * used to render that paragraph. */
+ PangoLayout *layout;
+
+ /* The coordinates in @layout will be offset horizontally with respect to the
+ * position of those characters in @text_image. Store that offset here so
+ * that the information in @layout can be used to locate the position and
+ * extent of text areas in @text_image. */
+ guint horiz_offset;
+};
+
+
+struct _GstTtmlRender {
+ GstElement element;
+
+ GstPad *video_sinkpad;
+ GstPad *text_sinkpad;
+ GstPad *srcpad;
+
+ GstSegment segment;
+ GstSegment text_segment;
+ GstBuffer *text_buffer;
+ gboolean text_linked;
+ gboolean video_flushing;
+ gboolean video_eos;
+ gboolean text_flushing;
+ gboolean text_eos;
+
+ GMutex lock;
+ GCond cond; /* to signal removal of a queued text
+ * buffer, arrival of a text buffer,
+ * a text segment update, or a change
+ * in status (e.g. shutdown, flushing) */
+
+ GstVideoInfo info;
+ GstVideoFormat format;
+ gint width;
+ gint height;
+
+ gboolean want_background;
+ gboolean wait_text;
+
+ gboolean need_render;
+
+ GList * compositions;
+};
+
+struct _GstTtmlRenderClass {
+ GstElementClass parent_class;
+
+ PangoContext *pango_context;
+ GMutex *pango_lock;
+};
+
+GType gst_ttml_render_get_type(void) G_GNUC_CONST;
+
+G_END_DECLS
+
+#endif /* __GST_TTML_RENDER_H */
diff --git a/ext/ttml/subtitle.c b/ext/ttml/subtitle.c
new file mode 100644
index 000000000..2beac79a8
--- /dev/null
+++ b/ext/ttml/subtitle.c
@@ -0,0 +1,312 @@
+/* GStreamer
+ * Copyright (C) <2015> British Broadcasting Corporation
+ * Author: Chris Bass <dash@rd.bbc.co.uk>
+ *
+ * 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., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * SECTION:gstsubtitle
+ * @short_description: Library for describing sets of static subtitles.
+ *
+ * This library enables the description of static text scenes made up of a
+ * number of regions, which may contain a number of block and inline text
+ * elements. It is derived from the concepts and features defined in the Timed
+ * Text Markup Language 1 (TTML1), Second Edition
+ * (http://www.w3.org/TR/ttaf1-dfxp), and the EBU-TT-D profile of TTML1
+ * (https://tech.ebu.ch/files/live/sites/tech/files/shared/tech/tech3380.pdf).
+ */
+
+#include "subtitle.h"
+
+/**
+ * gst_subtitle_style_set_new:
+ *
+ * Create a new #GstSubtitleStyleSet with default values for all properties.
+ *
+ * Returns: (transfer full): A newly-allocated #GstSubtitleStyleSet.
+ */
+GstSubtitleStyleSet *
+gst_subtitle_style_set_new (void)
+{
+ GstSubtitleStyleSet *ret = g_slice_new0 (GstSubtitleStyleSet);
+ GstSubtitleColor white = { 255, 255, 255, 255 };
+ GstSubtitleColor transparent = { 0, 0, 0, 0 };
+
+ ret->font_family = g_strdup ("default");
+ ret->font_size = 1.0;
+ ret->line_height = 1.25;
+ ret->color = white;
+ ret->background_color = transparent;
+ ret->line_padding = 0.0;
+ ret->origin_x = ret->origin_y = 0.0;
+ ret->extent_w = ret->extent_h = 0.0;
+ ret->padding_start = ret->padding_end
+ = ret->padding_before = ret->padding_after = 0.0;
+
+ return ret;
+}
+
+/**
+ * gst_subtitle_style_set_free:
+ * @style_set: A #GstSubtitleStyleSet.
+ *
+ * Free @style_set and its associated memory.
+ */
+void
+gst_subtitle_style_set_free (GstSubtitleStyleSet * style_set)
+{
+ g_return_if_fail (style_set != NULL);
+ g_free (style_set->font_family);
+ g_slice_free (GstSubtitleStyleSet, style_set);
+}
+
+
+static void
+_gst_subtitle_element_free (GstSubtitleElement * element)
+{
+ g_return_if_fail (element != NULL);
+ gst_subtitle_style_set_free (element->style_set);
+ g_slice_free (GstSubtitleElement, element);
+}
+
+GST_DEFINE_MINI_OBJECT_TYPE (GstSubtitleElement, gst_subtitle_element);
+
+/**
+ * gst_subtitle_element_new:
+ * @style_set: (transfer full): A #GstSubtitleStyleSet that defines the styling
+ * and layout associated with this inline text element.
+ * @text_index: The index within a #GstBuffer of the #GstMemory that contains
+ * the text of this inline text element.
+ *
+ * Allocates a new #GstSubtitleElement.
+ *
+ * Returns: (transfer full): A newly-allocated #GstSubtitleElement. Unref
+ * with gst_subtitle_element_unref() when no longer needed.
+ */
+GstSubtitleElement *
+gst_subtitle_element_new (GstSubtitleStyleSet * style_set,
+ guint text_index, gboolean suppress_whitespace)
+{
+ GstSubtitleElement *element;
+
+ g_return_val_if_fail (style_set != NULL, NULL);
+
+ element = g_slice_new0 (GstSubtitleElement);
+ gst_mini_object_init (GST_MINI_OBJECT_CAST (element), 0,
+ gst_subtitle_element_get_type (), NULL, NULL,
+ (GstMiniObjectFreeFunction) _gst_subtitle_element_free);
+
+ element->style_set = style_set;
+ element->text_index = text_index;
+ element->suppress_whitespace = suppress_whitespace;
+
+ return element;
+}
+
+static void
+_gst_subtitle_block_free (GstSubtitleBlock * block)
+{
+ g_return_if_fail (block != NULL);
+ gst_subtitle_style_set_free (block->style_set);
+ g_ptr_array_unref (block->elements);
+ g_slice_free (GstSubtitleBlock, block);
+}
+
+GST_DEFINE_MINI_OBJECT_TYPE (GstSubtitleBlock, gst_subtitle_block);
+
+
+/**
+ * gst_subtitle_block_new:
+ * @style_set: (transfer full): A #GstSubtitleStyleSet that defines the styling
+ * and layout associated with this block of text elements.
+ *
+ * Allocates a new #GstSubtitleBlock.
+ *
+ * Returns: (transfer full): A newly-allocated #GstSubtitleBlock. Unref
+ * with gst_subtitle_block_unref() when no longer needed.
+ */
+GstSubtitleBlock *
+gst_subtitle_block_new (GstSubtitleStyleSet * style_set)
+{
+ GstSubtitleBlock *block;
+
+ g_return_val_if_fail (style_set != NULL, NULL);
+
+ block = g_slice_new0 (GstSubtitleBlock);
+ gst_mini_object_init (GST_MINI_OBJECT_CAST (block), 0,
+ gst_subtitle_block_get_type (), NULL, NULL,
+ (GstMiniObjectFreeFunction) _gst_subtitle_block_free);
+
+ block->style_set = style_set;
+ block->elements = g_ptr_array_new_with_free_func (
+ (GDestroyNotify) gst_subtitle_element_unref);
+
+ return block;
+}
+
+/**
+ * gst_subtitle_block_add_element:
+ * @block: A #GstSubtitleBlock.
+ * @element: (transfer full): A #GstSubtitleElement to add.
+ *
+ * Adds a #GstSubtitleElement to @block.
+ */
+void
+gst_subtitle_block_add_element (GstSubtitleBlock * block,
+ GstSubtitleElement * element)
+{
+ g_return_if_fail (block != NULL);
+ g_return_if_fail (element != NULL);
+
+ g_ptr_array_add (block->elements, element);
+}
+
+/**
+ * gst_subtitle_block_get_element_count:
+ * @block: A #GstSubtitleBlock.
+ *
+ * Returns: The number of #GstSubtitleElements in @block.
+ */
+guint
+gst_subtitle_block_get_element_count (const GstSubtitleBlock * block)
+{
+ g_return_val_if_fail (block != NULL, 0);
+
+ return block->elements->len;
+}
+
+/**
+ * gst_subtitle_block_get_element:
+ * @block: A #GstSubtitleBlock.
+ * @index: Index of the element to get.
+ *
+ * Gets the #GstSubtitleElement at @index in the array of elements held by
+ * @block.
+ *
+ * Returns: (transfer none): The #GstSubtitleElement at @index in the array of
+ * elements held by @block, or %NULL if @index is out-of-bounds. The
+ * function does not return a reference; the caller should obtain a reference
+ * using gst_subtitle_element_ref(), if needed.
+ */
+const GstSubtitleElement *
+gst_subtitle_block_get_element (const GstSubtitleBlock * block, guint index)
+{
+ g_return_val_if_fail (block != NULL, NULL);
+
+ if (index >= block->elements->len)
+ return NULL;
+ else
+ return g_ptr_array_index (block->elements, index);
+}
+
+static void
+_gst_subtitle_region_free (GstSubtitleRegion * region)
+{
+ g_return_if_fail (region != NULL);
+ gst_subtitle_style_set_free (region->style_set);
+ g_ptr_array_unref (region->blocks);
+ g_slice_free (GstSubtitleRegion, region);
+}
+
+GST_DEFINE_MINI_OBJECT_TYPE (GstSubtitleRegion, gst_subtitle_region);
+
+
+/**
+ * gst_subtitle_region_new:
+ * @style_set: (transfer full): A #GstSubtitleStyleSet that defines the styling
+ * and layout associated with this region.
+ *
+ * Allocates a new #GstSubtitleRegion.
+ *
+ * Returns: (transfer full): A newly-allocated #GstSubtitleRegion. Unref
+ * with gst_subtitle_region_unref() when no longer needed.
+ */
+GstSubtitleRegion *
+gst_subtitle_region_new (GstSubtitleStyleSet * style_set)
+{
+ GstSubtitleRegion *region;
+
+ g_return_val_if_fail (style_set != NULL, NULL);
+
+ region = g_slice_new0 (GstSubtitleRegion);
+ gst_mini_object_init (GST_MINI_OBJECT_CAST (region), 0,
+ gst_subtitle_region_get_type (), NULL, NULL,
+ (GstMiniObjectFreeFunction) _gst_subtitle_region_free);
+
+ region->style_set = style_set;
+ region->blocks = g_ptr_array_new_with_free_func (
+ (GDestroyNotify) gst_subtitle_block_unref);
+
+ return region;
+}
+
+/**
+ * gst_subtitle_region_add_block:
+ * @region: A #GstSubtitleRegion.
+ * @block: (transfer full): A #GstSubtitleBlock which should be added
+ * to @region's array of blocks.
+ *
+ * Adds a #GstSubtitleBlock to the end of the array of blocks held by @region.
+ * @region will take ownership of @block, and will unref it when @region
+ * is freed.
+ */
+void
+gst_subtitle_region_add_block (GstSubtitleRegion * region,
+ GstSubtitleBlock * block)
+{
+ g_return_if_fail (region != NULL);
+ g_return_if_fail (block != NULL);
+
+ g_ptr_array_add (region->blocks, block);
+}
+
+/**
+ * gst_subtitle_region_get_block_count:
+ * @region: A #GstSubtitleRegion.
+ *
+ * Returns: The number of blocks in @region.
+ */
+guint
+gst_subtitle_region_get_block_count (const GstSubtitleRegion * region)
+{
+ g_return_val_if_fail (region != NULL, 0);
+
+ return region->blocks->len;
+}
+
+/**
+ * gst_subtitle_region_get_block:
+ * @region: A #GstSubtitleRegion.
+ * @index: Index of the block to get.
+ *
+ * Gets the block at @index in the array of blocks held by @region.
+ *
+ * Returns: (transfer none): The #GstSubtitleBlock at @index in the array of
+ * blocks held by @region, or %NULL if @index is out-of-bounds. The
+ * function does not return a reference; the caller should obtain a reference
+ * using gst_subtitle_block_ref(), if needed.
+ */
+const GstSubtitleBlock *
+gst_subtitle_region_get_block (const GstSubtitleRegion * region, guint index)
+{
+ g_return_val_if_fail (region != NULL, NULL);
+
+ if (index >= region->blocks->len)
+ return NULL;
+ else
+ return g_ptr_array_index (region->blocks, index);
+}
diff --git a/ext/ttml/subtitle.h b/ext/ttml/subtitle.h
new file mode 100644
index 000000000..95333d354
--- /dev/null
+++ b/ext/ttml/subtitle.h
@@ -0,0 +1,592 @@
+/* GStreamer
+ * Copyright (C) <2015> British Broadcasting Corporation
+ * Author: Chris Bass <dash@rd.bbc.co.uk>
+ *
+ * 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., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __GST_SUBTITLE_H__
+#define __GST_SUBTITLE_H__
+
+#include <glib.h>
+#include <gst/gst.h>
+#include <gst/gstminiobject.h>
+
+G_BEGIN_DECLS
+
+typedef struct _GstSubtitleColor GstSubtitleColor;
+typedef struct _GstSubtitleStyleSet GstSubtitleStyleSet;
+typedef struct _GstSubtitleElement GstSubtitleElement;
+typedef struct _GstSubtitleBlock GstSubtitleBlock;
+typedef struct _GstSubtitleRegion GstSubtitleRegion;
+
+/**
+ * GstSubtitleWritingMode:
+ * @GST_SUBTITLE_WRITING_MODE_LRTB: Text progression is left-to-right,
+ * top-to-bottom.
+ * @GST_SUBTITLE_WRITING_MODE_RLTB: Text progression is right-to-left,
+ * top-to-bottom.
+ * @GST_SUBTITLE_WRITING_MODE_TBRL: Text progression is top-to-bottom,
+ * right-to-left.
+ * @GST_SUBTITLE_WRITING_MODE_TBLR: Text progression is top-to-bottom,
+ * left-to-right.
+ *
+ * Writing mode of text content. The values define the direction of progression
+ * of both inline text (#GstSubtitleElements) and blocks of text
+ * (#GstSubtitleBlocks).
+ */
+typedef enum {
+ GST_SUBTITLE_WRITING_MODE_LRTB,
+ GST_SUBTITLE_WRITING_MODE_RLTB,
+ GST_SUBTITLE_WRITING_MODE_TBRL,
+ GST_SUBTITLE_WRITING_MODE_TBLR
+} GstSubtitleWritingMode;
+
+/**
+ * GstSubtitleDisplayAlign:
+ * @GST_SUBTITLE_DISPLAY_ALIGN_BEFORE: Blocks should be aligned at the start of
+ * the containing region.
+ * @GST_SUBTITLE_DISPLAY_ALIGN_CENTER: Blocks should be aligned in the center
+ * of the containing region.
+ * @GST_SUBTITLE_DISPLAY_ALIGN_AFTER: Blocks should be aligned to the end of
+ * the containing region.
+ *
+ * Defines the alignment of text blocks within a region in the direction in
+ * which blocks are being stacked. For text that is written left-to-right and
+ * top-to-bottom, this corresponds to the vertical alignment of text blocks.
+ */
+typedef enum {
+ GST_SUBTITLE_DISPLAY_ALIGN_BEFORE,
+ GST_SUBTITLE_DISPLAY_ALIGN_CENTER,
+ GST_SUBTITLE_DISPLAY_ALIGN_AFTER
+} GstSubtitleDisplayAlign;
+
+/**
+ * GstSubtitleBackgroundMode:
+ * @GST_SUBTITLE_BACKGROUND_MODE_ALWAYS: Background rectangle should be visible
+ * at all times.
+ * @GST_SUBTITLE_BACKGROUND_MODE_WHEN_ACTIVE: Background rectangle should be
+ * visible only when text is rendered into the corresponding region.
+ *
+ * Defines whether the background rectangle of a region should be visible at
+ * all times or only when text is rendered within it.
+ */
+typedef enum {
+ GST_SUBTITLE_BACKGROUND_MODE_ALWAYS,
+ GST_SUBTITLE_BACKGROUND_MODE_WHEN_ACTIVE
+} GstSubtitleBackgroundMode;
+
+/**
+ * GstSubtitleOverflowMode:
+ * @GST_SUBTITLE_OVERFLOW_MODE_HIDDEN: If text and/or background rectangles
+ * flowed into the region overflow the bounds of that region, they should
+ * be clipped at the region boundary.
+ * @GST_SUBTITLE_OVERFLOW_MODE_VISIBLE: If text and/or background rectangles
+ * flowed into the region overflow the bounds of that region, they should be
+ * allowed to overflow the region boundary.
+ *
+ * Defines what should happen to text that overflows its containing region.
+ */
+typedef enum {
+ GST_SUBTITLE_OVERFLOW_MODE_HIDDEN,
+ GST_SUBTITLE_OVERFLOW_MODE_VISIBLE
+} GstSubtitleOverflowMode;
+
+/**
+ * GstSubtitleColor:
+ * @r: Red value.
+ * @g: Green value.
+ * @b: Blue value.
+ * @a: Alpha value (0 = totally transparent; 255 = totally opaque).
+ *
+ * Describes an RGBA color.
+ */
+struct _GstSubtitleColor {
+ guint8 r;
+ guint8 g;
+ guint8 b;
+ guint8 a;
+};
+
+/**
+ * GstSubtitleTextDirection:
+ * @GST_SUBTITLE_TEXT_DIRECTION_LTR: Text direction is left-to-right.
+ * @GST_SUBTITLE_TEXT_DIRECTION_RTL: Text direction is right-to-left.
+ *
+ * Defines the progression direction of unicode text that is being treated by
+ * the unicode bidirectional algorithm as embedded or overidden (see
+ * http://unicode.org/reports/tr9/ for more details of the unicode
+ * bidirectional algorithm).
+ */
+typedef enum {
+ GST_SUBTITLE_TEXT_DIRECTION_LTR,
+ GST_SUBTITLE_TEXT_DIRECTION_RTL
+} GstSubtitleTextDirection;
+
+/**
+ * GstSubtitleTextAlign:
+ * @GST_SUBTITLE_TEXT_ALIGN_START: Text areas should be rendered at the
+ * start of the block area, with respect to the direction in which text is
+ * being rendered. For text that is rendered left-to-right this corresponds to
+ * the left of the block area; for text that is rendered right-to-left this
+ * corresponds to the right of the block area.
+ * @GST_SUBTITLE_TEXT_ALIGN_LEFT: Text areas should be rendered at the left of
+ * the block area.
+ * @GST_SUBTITLE_TEXT_ALIGN_CENTER: Text areas should be rendered at the center
+ * of the block area.
+ * @GST_SUBTITLE_TEXT_ALIGN_RIGHT: Text areas should be rendered at the right
+ * of the block area.
+ * @GST_SUBTITLE_TEXT_ALIGN_END: Text areas should be rendered at the end of
+ * the block area, with respect to the direction in which text is being
+ * rendered. For text that is rendered left-to-right this corresponds to the
+ * right of the block area; for text that is rendered right-to-left this
+ * corresponds to end of the block area.
+ *
+ * Defines how inline text areas within a block should be aligned within the
+ * block area.
+ */
+typedef enum {
+ GST_SUBTITLE_TEXT_ALIGN_START,
+ GST_SUBTITLE_TEXT_ALIGN_LEFT,
+ GST_SUBTITLE_TEXT_ALIGN_CENTER,
+ GST_SUBTITLE_TEXT_ALIGN_RIGHT,
+ GST_SUBTITLE_TEXT_ALIGN_END
+} GstSubtitleTextAlign;
+
+/**
+ * GstSubtitleFontStyle:
+ * @GST_SUBTITLE_FONT_STYLE_NORMAL: Normal font style.
+ * @GST_SUBTITLE_FONT_STYLE_ITALIC: Italic font style.
+ *
+ * Defines styling that should be applied to the glyphs of a font used to
+ * render text within an inline text element.
+ */
+typedef enum {
+ GST_SUBTITLE_FONT_STYLE_NORMAL,
+ GST_SUBTITLE_FONT_STYLE_ITALIC
+} GstSubtitleFontStyle;
+
+/**
+ * GstSubtitleFontWeight:
+ * @GST_SUBTITLE_FONT_WEIGHT_NORMAL: Normal weight.
+ * @GST_SUBTITLE_FONT_WEIGHT_BOLD: Bold weight.
+ *
+ * Defines the font weight that should be applied to the glyphs of a font used
+ * to render text within an inline text element.
+ */
+typedef enum {
+ GST_SUBTITLE_FONT_WEIGHT_NORMAL,
+ GST_SUBTITLE_FONT_WEIGHT_BOLD
+} GstSubtitleFontWeight;
+
+/**
+ * GstSubtitleTextDecoration:
+ * @GST_SUBTITLE_TEXT_DECORATION_NONE: Text should not be decorated.
+ * @GST_SUBTITLE_TEXT_DECORATION_UNDERLINE: Text should be underlined.
+ *
+ * Defines the decoration that should be applied to the glyphs of a font used
+ * to render text within an inline text element.
+ */
+typedef enum {
+ GST_SUBTITLE_TEXT_DECORATION_NONE,
+ GST_SUBTITLE_TEXT_DECORATION_UNDERLINE
+} GstSubtitleTextDecoration;
+
+/**
+ * GstSubtitleUnicodeBidi:
+ * @GST_SUBTITLE_UNICODE_BIDI_NORMAL: Text should progress according the the
+ * default behaviour of the Unicode bidirectional algorithm.
+ * @GST_SUBTITLE_UNICODE_BIDI_EMBED: Text should be treated as being embedded
+ * with a specific direction (given by a #GstSubtitleTextDecoration value
+ * defined elsewhere).
+ * @GST_SUBTITLE_UNICODE_BIDI_OVERRIDE: Text should be forced to have a
+ * specific direction (given by a #GstSubtitleTextDecoration value defined
+ * elsewhere).
+ *
+ * Defines directional embedding or override according to the Unicode
+ * bidirectional algorithm. See http://unicode.org/reports/tr9/ for more
+ * details of the Unicode bidirectional algorithm.
+ */
+typedef enum {
+ GST_SUBTITLE_UNICODE_BIDI_NORMAL,
+ GST_SUBTITLE_UNICODE_BIDI_EMBED,
+ GST_SUBTITLE_UNICODE_BIDI_OVERRIDE
+} GstSubtitleUnicodeBidi;
+
+/**
+ * GstSubtitleWrapping:
+ * @GST_SUBTITLE_WRAPPING_ON: Lines that overflow the region boundary should be
+ * wrapped.
+ * @GST_SUBTITLE_WRAPPING_OFF: Lines that overflow the region boundary should
+ * not be wrapped.
+ *
+ * Defines how a renderer should treat lines of text that overflow the boundary
+ * of the region into which they are being rendered.
+ */
+typedef enum {
+ GST_SUBTITLE_WRAPPING_ON,
+ GST_SUBTITLE_WRAPPING_OFF
+} GstSubtitleWrapping;
+
+/**
+ * GstSubtitleMultiRowAlign:
+ * @GST_SUBTITLE_MULTI_ROW_ALIGN_AUTO: Lines should be aligned according to the
+ * value of #GstSubtitleTextAlign associated with that text.
+ * @GST_SUBTITLE_MULTI_ROW_ALIGN_START: Lines should be aligned at their
+ * starting edge. The edge that is considered the starting edge depends upon
+ * the direction of that text.
+ * @GST_SUBTITLE_MULTI_ROW_ALIGN_CENTER: Lines should be center-aligned.
+ * @GST_SUBTITLE_MULTI_ROW_ALIGN_END: Lines should be aligned at their trailing
+ * edge. The edge that is considered the trailing edge depends upon the
+ * direction of that text.
+ *
+ * Defines how multiple 'rows' (i.e, lines) in a block should be aligned
+ * relative to each other.
+ *
+ * This is based upon the ebutts:multiRowAlign attribute defined in the
+ * EBU-TT-D specification.
+ */
+typedef enum {
+ GST_SUBTITLE_MULTI_ROW_ALIGN_AUTO,
+ GST_SUBTITLE_MULTI_ROW_ALIGN_START,
+ GST_SUBTITLE_MULTI_ROW_ALIGN_CENTER,
+ GST_SUBTITLE_MULTI_ROW_ALIGN_END
+} GstSubtitleMultiRowAlign;
+
+/**
+ * GstSubtitleStyleSet:
+ * @text_direction: Defines the direction of text that has been declared by the
+ * #GstSubtitleStyleSet:unicode_bidi attribute to be embbedded or overridden.
+ * Applies to both #GstSubtitleBlocks and #GstSubtitleElements.
+ * @font_family: The name of the font family that should be used to render the
+ * text of an inline element. Applies only to #GstSubtitleElements.
+ * @font_size: The size of the font that should be used to render the text
+ * of an inline element. The size is given as a multiple of the display height,
+ * where 1.0 equals the height of the display. Applies only to
+ * #GstSubtitleElements.
+ * @line_height: The inter-baseline separation between lines generated when
+ * rendering inline text elements within a block area. The height is given as a
+ * multiple of the the overall display height, where 1.0 equals the height of
+ * the display. Applies only to #GstSubtitleBlocks.
+ * @text_align: Controls the alignent of lines of text within a block area.
+ * Note that this attribute does not control the alignment of lines relative to
+ * each other within a block area: that is determined by
+ * #GstSubtitleStyleSet:multi_row_align. Applies only to #GstSubtitleBlocks.
+ * @color: The color that should be used when rendering the text of an inline
+ * element. Applies only to #GstSubtitleElements.
+ * @background_color: The color of the rectangle that should be rendered behind
+ * the contents of a #GstSubtitleRegion, #GstSubtitleBlock or
+ * #GstSubtitleElement.
+ * @font_style: The style of the font that should be used to render the text
+ * of an inline element. Applies only to #GstSubtitleElements.
+ * @font_weight: The weight of the font that should be used to render the text
+ * of an inline element. Applies only to #GstSubtitleElements.
+ * @text_decoration: The decoration that should be applied to the text of an
+ * inline element. Applies only to #GstSubtitleElements.
+ * @unicode_bidi: Controls how unicode text within a block or inline element
+ * should be treated by the unicode bidirectional algorithm. Applies to both
+ * #GstSubtitleBlocks and #GstSubtitleElements.
+ * @wrap_option: Defines whether or not automatic line breaking should apply to
+ * the lines generated when rendering a block of text elements. Applies only to
+ * #GstSubtitleBlocks.
+ * @multi_row_align: Defines how 'rows' (i.e., lines) within a text block
+ * should be aligned relative to each other. Note that this attribute does not
+ * determine how a block of text is aligned within that block area: that is
+ * determined by @text_align. Applies only to #GstSubtitleBlocks.
+ * @line_padding: Defines how much horizontal padding should be added on the
+ * start and end of each rendered line; this allows the insertion of space
+ * between the start/end of text lines and their background rectangles for
+ * better-looking subtitles. This is based upon the ebutts:linePadding
+ * attribute defined in the EBU-TT-D specification. Applies only to
+ * #GstSubtitleBlocks.
+ * @origin_x: The horizontal origin of a region into which text blocks may be
+ * rendered. Given as a multiple of the overall display width, where 1.0 equals
+ * the width of the display. Applies only to #GstSubtitleRegions.
+ * @origin_y: The vertical origin of a region into which text blocks may be
+ * rendered. Given as a multiple of the overall display height, where 1.0
+ * equals the height of the display. Applies only to #GstSubtitleRegions.
+ * @extent_w: The horizontal extent of a region into which text blocks may be
+ * rendered. Given as a multiple of the overall display width, where 1.0 equals
+ * the width of the display. Applies only to #GstSubtitleRegions.
+ * @extent_h: The vertical extent of a region into which text blocks may be
+ * rendered. Given as a multiple of the overall display height, where 1.0
+ * equals the height of the display. Applies only to #GstSubtitleRegions.
+ * @display_align: The alignment of generated text blocks in the direction in
+ * which blocks are being stacked. For text that flows left-to-right and
+ * top-to-bottom, for example, this corresponds to the vertical alignment of
+ * text blocks. Applies only to #GstSubtitleRegions.
+ * @padding_start: The horizontal indent of text from the leading edge of a
+ * region into which blocks may be rendered. Given as a multiple of the overall
+ * display width, where 1.0 equals the width of the display. Applies only to
+ * #GstSubtitleRegions.
+ * @padding_end: The horizontal indent of text from the trailing edge of a
+ * region into which blocks may be rendered. Given as a multiple of the overall
+ * display width, where 1.0 equals the width of the display. Applies only to
+ * #GstSubtitleRegions.
+ * @padding_before: The vertical indent of text from the top edge of a region
+ * into which blocks may be rendered. Given as a multiple of the overall
+ * display height, where 1.0 equals the height of the display. Applies only to
+ * #GstSubtitleRegions.
+ * @padding_after: The vertical indent of text from the bottom edge of a
+ * region into which blocks may be rendered. Given as a multiple of the overall
+ * display height, where 1.0 equals the height of the display. Applies only to
+ * #GstSubtitleRegions.
+ * @writing_mode: Defines the direction in which both inline elements and
+ * blocks should be stacked when rendered into an on-screen region. Applies
+ * only to #GstSubtitleRegions.
+ * @show_background: Defines whether the background of a region should be
+ * displayed at all times or only when it has text rendered into it. Applies
+ * only to #GstSubtitleRegions.
+ * @overflow: Defines what should happen if text and background rectangles
+ * generated by rendering text blocks overflow the size of their containing
+ * region. Applies only to #GstSubtitleRegions.
+ *
+ * Holds a set of attributes that describes the styling and layout that apply
+ * to #GstSubtitleRegion, #GstSubtitleBlock and/or #GstSubtitleElement objects.
+ *
+ * Note that, though each of the above object types have an associated
+ * #GstSubtitleStyleSet, not all attributes in a #GstSubtitleStyleSet type
+ * apply to all object types: #GstSubtitleStyleSet:overflow applies only to
+ * #GstSubtitleRegions, for example, while #GstSubtitleStyleSet:font_style
+ * applies only to #GstSubtitleElements. Some attributes apply to multiple
+ * object types: #GstSubtitleStyleSet:background_color, for example, applies to
+ * all object types. The types to which each attribute applies is given in the
+ * description of that attribute below.
+ */
+struct _GstSubtitleStyleSet {
+ GstSubtitleTextDirection text_direction;
+ gchar *font_family;
+ gdouble font_size;
+ gdouble line_height;
+ GstSubtitleTextAlign text_align;
+ GstSubtitleColor color;
+ GstSubtitleColor background_color;
+ GstSubtitleFontStyle font_style;
+ GstSubtitleFontWeight font_weight;
+ GstSubtitleTextDecoration text_decoration;
+ GstSubtitleUnicodeBidi unicode_bidi;
+ GstSubtitleWrapping wrap_option;
+ GstSubtitleMultiRowAlign multi_row_align;
+ gdouble line_padding;
+ gdouble origin_x, origin_y;
+ gdouble extent_w, extent_h;
+ GstSubtitleDisplayAlign display_align;
+ gdouble padding_start, padding_end, padding_before, padding_after;
+ GstSubtitleWritingMode writing_mode;
+ GstSubtitleBackgroundMode show_background;
+ GstSubtitleOverflowMode overflow;
+};
+
+GstSubtitleStyleSet * gst_subtitle_style_set_new (void);
+
+void gst_subtitle_style_set_free (GstSubtitleStyleSet * style_set);
+
+
+/**
+ * GstSubtitleElement:
+ * @mini_object: The parent #GstMiniObject.
+ * @style_set: Styling associated with this element.
+ * @text_index: Index into the #GstBuffer associated with this
+ * #GstSubtitleElement; the index identifies the #GstMemory within the
+ * #GstBuffer that holds the #GstSubtitleElement's text.
+ * @suppress_whitespace: Indicates whether or not a renderer should suppress
+ * whitespace in the element's text.
+ *
+ * Represents an inline text element.
+ *
+ * In TTML this would correspond to inline text resulting from a &lt;span&gt;
+ * element, an anonymous span (e.g., text within a &lt;p&gt; tag), or a
+ * &lt;br&gt; element.
+ */
+struct _GstSubtitleElement
+{
+ GstMiniObject mini_object;
+
+ GstSubtitleStyleSet *style_set;
+ guint text_index;
+ gboolean suppress_whitespace;
+
+ /*< private >*/
+ gpointer _gst_reserved[GST_PADDING];
+};
+
+GType gst_subtitle_element_get_type (void);
+
+GstSubtitleElement * gst_subtitle_element_new (GstSubtitleStyleSet * style_set,
+ guint text_index, gboolean suppress_whitespace);
+
+/**
+ * gst_subtitle_element_ref:
+ * @element: A #GstSubtitleElement.
+ *
+ * Increments the refcount of @element.
+ *
+ * Returns: (transfer full): @element.
+ */
+static inline GstSubtitleElement *
+gst_subtitle_element_ref (GstSubtitleElement * element)
+{
+ return (GstSubtitleElement *)
+ gst_mini_object_ref (GST_MINI_OBJECT_CAST (element));
+}
+
+/**
+ * gst_subtitle_element_unref:
+ * @element: (transfer full): A #GstSubtitleElement.
+ *
+ * Decrements the refcount of @element. If the refcount reaches 0, @element
+ * will be freed.
+ */
+static inline void
+gst_subtitle_element_unref (GstSubtitleElement * element)
+{
+ gst_mini_object_unref (GST_MINI_OBJECT_CAST (element));
+}
+
+
+/**
+ * GstSubtitleBlock:
+ * @mini_object: The parent #GstMiniObject.
+ * @style_set: Styling associated with this block.
+ *
+ * Represents a text block made up of one or more inline text elements (i.e.,
+ * one or more #GstSubtitleElements).
+ *
+ * In TTML this would correspond to the block of text resulting from the inline
+ * elements within a single &lt;p&gt;.
+ */
+struct _GstSubtitleBlock
+{
+ GstMiniObject mini_object;
+
+ GstSubtitleStyleSet *style_set;
+
+ /*< private >*/
+ GPtrArray *elements;
+ gpointer _gst_reserved[GST_PADDING];
+};
+
+GType gst_subtitle_block_get_type (void);
+
+GstSubtitleBlock * gst_subtitle_block_new (GstSubtitleStyleSet * style_set);
+
+void gst_subtitle_block_add_element (
+ GstSubtitleBlock * block,
+ GstSubtitleElement * element);
+
+guint gst_subtitle_block_get_element_count (const GstSubtitleBlock * block);
+
+const GstSubtitleElement * gst_subtitle_block_get_element (
+ const GstSubtitleBlock * block, guint index);
+
+/**
+ * gst_subtitle_block_ref:
+ * @block: A #GstSubtitleBlock.
+ *
+ * Increments the refcount of @block.
+ *
+ * Returns: (transfer full): @block.
+ */
+static inline GstSubtitleBlock *
+gst_subtitle_block_ref (GstSubtitleBlock * block)
+{
+ return (GstSubtitleBlock *)
+ gst_mini_object_ref (GST_MINI_OBJECT_CAST (block));
+}
+
+/**
+ * gst_subtitle_block_unref:
+ * @block: (transfer full): A #GstSubtitleBlock.
+ *
+ * Decrements the refcount of @block. If the refcount reaches 0, @block will
+ * be freed.
+ */
+static inline void
+gst_subtitle_block_unref (GstSubtitleBlock * block)
+{
+ gst_mini_object_unref (GST_MINI_OBJECT_CAST (block));
+}
+
+
+/**
+ * GstSubtitleRegion:
+ * @mini_object: The parent #GstMiniObject.
+ * @style_set: Styling associated with this region.
+ *
+ * Represents an on-screen region in which is displayed zero or more
+ * #GstSubtitleBlocks.
+ *
+ * In TTML this corresponds to a &lt;region&gt; into which zero or more
+ * &lt;p&gt;s may be rendered. A #GstSubtitleRegion allows a background
+ * rectangle to be displayed in a region area even if no text blocks are
+ * rendered into it, as per the behaviour allowed by TTML regions whose
+ * tts:showBackground style attribute is set to "always".
+ */
+struct _GstSubtitleRegion
+{
+ GstMiniObject mini_object;
+
+ GstSubtitleStyleSet *style_set;
+
+ /*< private >*/
+ GPtrArray *blocks;
+ gpointer _gst_reserved[GST_PADDING];
+};
+
+GType gst_subtitle_region_get_type (void);
+
+GstSubtitleRegion * gst_subtitle_region_new (GstSubtitleStyleSet * style_set);
+
+void gst_subtitle_region_add_block (
+ GstSubtitleRegion * region,
+ GstSubtitleBlock * block);
+
+guint gst_subtitle_region_get_block_count (const GstSubtitleRegion * region);
+
+const GstSubtitleBlock * gst_subtitle_region_get_block (
+ const GstSubtitleRegion * region, guint index);
+
+/**
+ * gst_subtitle_region_ref:
+ * @region: A #GstSubtitleRegion.
+ *
+ * Increments the refcount of @region.
+ *
+ * Returns: (transfer full): @region.
+ */
+static inline GstSubtitleRegion *
+gst_subtitle_region_ref (GstSubtitleRegion * region)
+{
+ return (GstSubtitleRegion *)
+ gst_mini_object_ref (GST_MINI_OBJECT_CAST (region));
+}
+
+/**
+ * gst_subtitle_region_unref:
+ * @region: (transfer full): A #GstSubtitleRegion.
+ *
+ * Decrements the refcount of @region. If the refcount reaches 0, @region will
+ * be freed.
+ */
+static inline void
+gst_subtitle_region_unref (GstSubtitleRegion * region)
+{
+ gst_mini_object_unref (GST_MINI_OBJECT_CAST (region));
+}
+
+G_END_DECLS
+
+#endif /* __GST_SUBTITLE_H__ */
diff --git a/ext/ttml/subtitlemeta.c b/ext/ttml/subtitlemeta.c
new file mode 100644
index 000000000..69da5f58b
--- /dev/null
+++ b/ext/ttml/subtitlemeta.c
@@ -0,0 +1,101 @@
+/* GStreamer
+ * Copyright (C) <2015> British Broadcasting Corporation
+ * Author: Chris Bass <dash@rd.bbc.co.uk>
+ *
+ * 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., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * SECTION:gstsubtitlemeta
+ * @short_description: Metadata class for timed-text subtitles.
+ *
+ * The GstSubtitleMeta class enables the layout and styling information needed
+ * to render subtitle text to be attached to a #GstBuffer containing that text.
+ */
+
+#include "subtitlemeta.h"
+
+GType
+gst_subtitle_meta_api_get_type (void)
+{
+ static volatile GType type;
+ static const gchar *tags[] = { "memory", NULL };
+
+ if (g_once_init_enter (&type)) {
+ GType _type = gst_meta_api_type_register ("GstSubtitleMetaAPI", tags);
+ g_once_init_leave (&type, _type);
+ }
+ return type;
+}
+
+gboolean
+gst_subtitle_meta_init (GstMeta * meta, gpointer params, GstBuffer * buffer)
+{
+ GstSubtitleMeta *subtitle_meta = (GstSubtitleMeta *) meta;
+
+ subtitle_meta->regions = NULL;
+ return TRUE;
+}
+
+void
+gst_subtitle_meta_free (GstMeta * meta, GstBuffer * buffer)
+{
+ GstSubtitleMeta *subtitle_meta = (GstSubtitleMeta *) meta;
+
+ if (subtitle_meta->regions)
+ g_ptr_array_unref (subtitle_meta->regions);
+}
+
+const GstMetaInfo *
+gst_subtitle_meta_get_info (void)
+{
+ static const GstMetaInfo *subtitle_meta_info = NULL;
+
+ if (g_once_init_enter (&subtitle_meta_info)) {
+ const GstMetaInfo *meta =
+ gst_meta_register (GST_SUBTITLE_META_API_TYPE, "GstSubtitleMeta",
+ sizeof (GstSubtitleMeta), gst_subtitle_meta_init,
+ gst_subtitle_meta_free, (GstMetaTransformFunction) NULL);
+ g_once_init_leave (&subtitle_meta_info, meta);
+ }
+ return subtitle_meta_info;
+}
+
+/**
+ * gst_buffer_add_subtitle_meta:
+ * @buffer: (transfer none): #GstBuffer holding subtitle text, to which
+ * subtitle metadata should be added.
+ * @regions: (transfer full): A #GPtrArray of #GstSubtitleRegions.
+ *
+ * Attaches subtitle metadata to a #GstBuffer.
+ *
+ * Returns: A pointer to the added #GstSubtitleMeta if successful; %NULL if
+ * unsuccessful.
+ */
+GstSubtitleMeta *
+gst_buffer_add_subtitle_meta (GstBuffer * buffer, GPtrArray * regions)
+{
+ GstSubtitleMeta *meta;
+
+ g_return_val_if_fail (GST_IS_BUFFER (buffer), NULL);
+ g_return_val_if_fail (regions != NULL, NULL);
+
+ meta = (GstSubtitleMeta *) gst_buffer_add_meta (buffer,
+ GST_SUBTITLE_META_INFO, NULL);
+
+ meta->regions = regions;
+ return meta;
+}
diff --git a/ext/ttml/subtitlemeta.h b/ext/ttml/subtitlemeta.h
new file mode 100644
index 000000000..e533451ea
--- /dev/null
+++ b/ext/ttml/subtitlemeta.h
@@ -0,0 +1,66 @@
+/* GStreamer
+ * Copyright (C) <2015> British Broadcasting Corporation
+ * Author: Chris Bass <dash@rd.bbc.co.uk>
+ *
+ * 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., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __GST_SUBTITLE_META_H__
+#define __GST_SUBTITLE_META_H__
+
+#include <gst/gst.h>
+#include "subtitle.h"
+
+G_BEGIN_DECLS
+
+typedef struct _GstSubtitleMeta GstSubtitleMeta;
+
+/**
+ * GstSubtitleMeta:
+ * @meta: The parent #GstMeta.
+ * @regions: The #GstSubtitleRegions containing layout and styling information
+ * needed to render the subtitle text contained in the associated #GstBuffer.
+ *
+ * Metadata type that describes the layout and styling of subtitle text
+ * contained in a #GstBuffer.
+ */
+struct _GstSubtitleMeta {
+ GstMeta meta;
+
+ GPtrArray *regions;
+};
+
+GType gst_subtitle_meta_api_get_type (void);
+#define GST_SUBTITLE_META_API_TYPE (gst_subtitle_meta_api_get_type())
+
+#define gst_buffer_get_subtitle_meta(b) \
+ ((GstSubtitleMeta*)gst_buffer_get_meta ((b), GST_SUBTITLE_META_API_TYPE))
+
+#define GST_SUBTITLE_META_INFO (gst_subtitle_meta_get_info())
+
+gboolean gst_subtitle_meta_init (GstMeta * meta, gpointer params,
+ GstBuffer * buffer);
+
+void gst_subtitle_meta_free (GstMeta * meta, GstBuffer * buffer);
+
+const GstMetaInfo * gst_subtitle_meta_get_info (void);
+
+GstSubtitleMeta * gst_buffer_add_subtitle_meta (GstBuffer * buffer,
+ GPtrArray * regions);
+
+G_END_DECLS
+
+#endif /* __GST_SUBTITLE_META_H__ */
diff --git a/ext/ttml/ttmlparse.c b/ext/ttml/ttmlparse.c
new file mode 100644
index 000000000..51bc5b623
--- /dev/null
+++ b/ext/ttml/ttmlparse.c
@@ -0,0 +1,1813 @@
+/* GStreamer TTML subtitle parser
+ * Copyright (C) <2015> British Broadcasting Corporation
+ * Authors:
+ * Chris Bass <dash@rd.bbc.co.uk>
+ * Peter Taylour <dash@rd.bbc.co.uk>
+ *
+ * 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., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/*
+ * Parses subtitle files encoded using the EBU-TT-D profile of TTML, as defined
+ * in https://tech.ebu.ch/files/live/sites/tech/files/shared/tech/tech3380.pdf
+ * and http://www.w3.org/TR/ttaf1-dfxp/, respectively.
+ */
+
+#include <glib.h>
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <math.h>
+#include <libxml/xmlmemory.h>
+#include <libxml/parser.h>
+
+#include "ttmlparse.h"
+#include "subtitle.h"
+#include "subtitlemeta.h"
+
+#define DEFAULT_CELLRES_X 32
+#define DEFAULT_CELLRES_Y 15
+#define MAX_FONT_FAMILY_NAME_LENGTH 128
+
+GST_DEBUG_CATEGORY_EXTERN (ttmlparse_debug);
+#define GST_CAT_DEFAULT ttmlparse_debug
+
+static gchar *ttml_get_xml_property (const xmlNode * node, const char *name);
+
+typedef struct _TtmlStyleSet TtmlStyleSet;
+typedef struct _TtmlElement TtmlElement;
+typedef struct _TtmlScene TtmlScene;
+
+typedef enum
+{
+ TTML_ELEMENT_TYPE_STYLE,
+ TTML_ELEMENT_TYPE_REGION,
+ TTML_ELEMENT_TYPE_BODY,
+ TTML_ELEMENT_TYPE_DIV,
+ TTML_ELEMENT_TYPE_P,
+ TTML_ELEMENT_TYPE_SPAN,
+ TTML_ELEMENT_TYPE_ANON_SPAN,
+ TTML_ELEMENT_TYPE_BR
+} TtmlElementType;
+
+typedef enum
+{
+ TTML_WHITESPACE_MODE_NONE,
+ TTML_WHITESPACE_MODE_DEFAULT,
+ TTML_WHITESPACE_MODE_PRESERVE,
+} TtmlWhitespaceMode;
+
+struct _TtmlElement
+{
+ TtmlElementType type;
+ gchar *id;
+ TtmlWhitespaceMode whitespace_mode;
+ gchar **styles;
+ gchar *region;
+ GstClockTime begin;
+ GstClockTime end;
+ TtmlStyleSet *style_set;
+ gchar *text;
+ guint text_index;
+};
+
+/* Represents a static scene consisting of one or more trees of elements that
+ * should be visible over a specific period of time. */
+struct _TtmlScene
+{
+ GstClockTime begin;
+ GstClockTime end;
+ GList *trees;
+ GstBuffer *buf;
+};
+
+struct _TtmlStyleSet
+{
+ GHashTable *table;
+};
+
+
+static TtmlStyleSet *
+ttml_style_set_new (void)
+{
+ TtmlStyleSet *ret = g_slice_new0 (TtmlStyleSet);
+ ret->table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+ return ret;
+}
+
+
+static void
+ttml_style_set_delete (TtmlStyleSet * style_set)
+{
+ if (style_set) {
+ g_hash_table_unref (style_set->table);
+ g_slice_free (TtmlStyleSet, style_set);
+ }
+}
+
+
+/* If attribute with name @attr_name already exists in @style_set, its value
+ * will be replaced by @attr_value. */
+static gboolean
+ttml_style_set_add_attr (TtmlStyleSet * style_set, const gchar * attr_name,
+ const gchar * attr_value)
+{
+ return g_hash_table_insert (style_set->table, g_strdup (attr_name),
+ g_strdup (attr_value));
+}
+
+
+static gboolean
+ttml_style_set_contains_attr (TtmlStyleSet * style_set, const gchar * attr_name)
+{
+ return g_hash_table_contains (style_set->table, attr_name);
+}
+
+
+static const gchar *
+ttml_style_set_get_attr (TtmlStyleSet * style_set, const gchar * attr_name)
+{
+ return g_hash_table_lookup (style_set->table, attr_name);
+}
+
+
+static guint8
+ttml_hex_pair_to_byte (const gchar * hex_pair)
+{
+ gint hi_digit, lo_digit;
+
+ hi_digit = g_ascii_xdigit_value (*hex_pair);
+ lo_digit = g_ascii_xdigit_value (*(hex_pair + 1));
+ return (hi_digit << 4) + lo_digit;
+}
+
+
+/* Color strings in EBU-TT-D can have the form "#RRBBGG" or "#RRBBGGAA". */
+static GstSubtitleColor
+ttml_parse_colorstring (const gchar * color)
+{
+ guint length;
+ const gchar *c = NULL;
+ GstSubtitleColor ret = { 0, 0, 0, 0 };
+
+ if (!color)
+ return ret;
+
+ length = strlen (color);
+ if (((length == 7) || (length == 9)) && *color == '#') {
+ c = color + 1;
+
+ ret.r = ttml_hex_pair_to_byte (c);
+ ret.g = ttml_hex_pair_to_byte (c + 2);
+ ret.b = ttml_hex_pair_to_byte (c + 4);
+
+ if (length == 7)
+ ret.a = G_MAXUINT8;
+ else
+ ret.a = ttml_hex_pair_to_byte (c + 6);
+
+ GST_CAT_LOG (ttmlparse_debug, "Returning color - r:%u b:%u g:%u a:%u",
+ ret.r, ret.b, ret.g, ret.a);
+ } else {
+ GST_CAT_ERROR (ttmlparse_debug, "Invalid color string: %s", color);
+ }
+
+ return ret;
+}
+
+
+static void
+ttml_style_set_print (TtmlStyleSet * style_set)
+{
+ GHashTableIter iter;
+ gpointer attr_name, attr_value;
+
+ if (!style_set) {
+ GST_CAT_LOG (ttmlparse_debug, "\t\t[NULL]");
+ return;
+ }
+
+ g_hash_table_iter_init (&iter, style_set->table);
+ while (g_hash_table_iter_next (&iter, &attr_name, &attr_value)) {
+ GST_CAT_LOG (ttmlparse_debug, "\t\t%s: %s", (const gchar *) attr_name,
+ (const gchar *) attr_value);
+ }
+}
+
+
+static TtmlStyleSet *
+ttml_parse_style_set (const xmlNode * node)
+{
+ TtmlStyleSet *s;
+ gchar *value = NULL;
+ xmlAttrPtr attr;
+
+ value = ttml_get_xml_property (node, "id");
+ if (!value) {
+ GST_CAT_ERROR (ttmlparse_debug, "styles must have an ID.");
+ return NULL;
+ }
+ g_free (value);
+
+ s = ttml_style_set_new ();
+
+ for (attr = node->properties; attr != NULL; attr = attr->next) {
+ if (attr->ns && ((g_strcmp0 ((const gchar *) attr->ns->prefix, "tts") == 0)
+ || (g_strcmp0 ((const gchar *) attr->ns->prefix, "ebutts") == 0))) {
+ ttml_style_set_add_attr (s, (const gchar *) attr->name,
+ (const gchar *) attr->children->content);
+ }
+ }
+
+ return s;
+}
+
+
+static void
+ttml_delete_element (TtmlElement * element)
+{
+ g_free ((gpointer) element->id);
+ if (element->styles)
+ g_strfreev (element->styles);
+ g_free ((gpointer) element->region);
+ ttml_style_set_delete (element->style_set);
+ g_free ((gpointer) element->text);
+ g_slice_free (TtmlElement, element);
+}
+
+
+static gchar *
+ttml_get_xml_property (const xmlNode * node, const char *name)
+{
+ xmlChar *xml_string = NULL;
+ gchar *gst_string = NULL;
+
+ g_return_val_if_fail (strlen (name) < 128, NULL);
+
+ xml_string = xmlGetProp (node, (xmlChar *) name);
+ if (!xml_string)
+ return NULL;
+ gst_string = g_strdup ((gchar *) xml_string);
+ xmlFree (xml_string);
+ return gst_string;
+}
+
+
+/* EBU-TT-D timecodes have format hours:minutes:seconds[.fraction] */
+static GstClockTime
+ttml_parse_timecode (const gchar * timestring)
+{
+ gchar **strings;
+ guint64 hours = 0, minutes = 0, seconds = 0, milliseconds = 0;
+ GstClockTime time = GST_CLOCK_TIME_NONE;
+
+ GST_CAT_LOG (ttmlparse_debug, "time string: %s", timestring);
+
+ strings = g_strsplit (timestring, ":", 3);
+ if (g_strv_length (strings) != 3U) {
+ GST_CAT_ERROR (ttmlparse_debug, "badly formatted time string: %s",
+ timestring);
+ return time;
+ }
+
+ hours = g_ascii_strtoull (strings[0], NULL, 10U);
+ minutes = g_ascii_strtoull (strings[1], NULL, 10U);
+ if (g_strstr_len (strings[2], -1, ".")) {
+ guint n_digits;
+ gchar **substrings = g_strsplit (strings[2], ".", 2);
+ seconds = g_ascii_strtoull (substrings[0], NULL, 10U);
+ n_digits = strlen (substrings[1]);
+ milliseconds = g_ascii_strtoull (substrings[1], NULL, 10U);
+ milliseconds =
+ (guint64) (milliseconds * pow (10.0, (3 - (double) n_digits)));
+ g_strfreev (substrings);
+ } else {
+ seconds = g_ascii_strtoull (strings[2], NULL, 10U);
+ }
+
+ if (minutes > 59 || seconds > 60) {
+ GST_CAT_ERROR (ttmlparse_debug, "invalid time string "
+ "(minutes or seconds out-of-bounds): %s\n", timestring);
+ }
+
+ g_strfreev (strings);
+ GST_CAT_LOG (ttmlparse_debug,
+ "hours: %" G_GUINT64_FORMAT " minutes: %" G_GUINT64_FORMAT
+ " seconds: %" G_GUINT64_FORMAT " milliseconds: %" G_GUINT64_FORMAT "",
+ hours, minutes, seconds, milliseconds);
+
+ time = hours * GST_SECOND * 3600
+ + minutes * GST_SECOND * 60
+ + seconds * GST_SECOND + milliseconds * GST_MSECOND;
+
+ return time;
+}
+
+
+static TtmlElement *
+ttml_parse_element (const xmlNode * node)
+{
+ TtmlElement *element;
+ TtmlElementType type;
+ gchar *value;
+
+ GST_CAT_DEBUG (ttmlparse_debug, "Element name: %s",
+ (const char *) node->name);
+ if ((g_strcmp0 ((const char *) node->name, "style") == 0)) {
+ type = TTML_ELEMENT_TYPE_STYLE;
+ } else if ((g_strcmp0 ((const char *) node->name, "region") == 0)) {
+ type = TTML_ELEMENT_TYPE_REGION;
+ } else if ((g_strcmp0 ((const char *) node->name, "body") == 0)) {
+ type = TTML_ELEMENT_TYPE_BODY;
+ } else if ((g_strcmp0 ((const char *) node->name, "div") == 0)) {
+ type = TTML_ELEMENT_TYPE_DIV;
+ } else if ((g_strcmp0 ((const char *) node->name, "p") == 0)) {
+ type = TTML_ELEMENT_TYPE_P;
+ } else if ((g_strcmp0 ((const char *) node->name, "span") == 0)) {
+ type = TTML_ELEMENT_TYPE_SPAN;
+ } else if ((g_strcmp0 ((const char *) node->name, "text") == 0)) {
+ type = TTML_ELEMENT_TYPE_ANON_SPAN;
+ } else if ((g_strcmp0 ((const char *) node->name, "br") == 0)) {
+ type = TTML_ELEMENT_TYPE_BR;
+ } else {
+ return NULL;
+ }
+
+ element = g_slice_new0 (TtmlElement);
+ element->type = type;
+
+ if ((value = ttml_get_xml_property (node, "id"))) {
+ element->id = g_strdup (value);
+ g_free (value);
+ }
+
+ if ((value = ttml_get_xml_property (node, "style"))) {
+ element->styles = g_strsplit (value, " ", 0);
+ GST_CAT_DEBUG (ttmlparse_debug, "%u style(s) referenced in element.",
+ g_strv_length (element->styles));
+ g_free (value);
+ }
+
+ if (element->type == TTML_ELEMENT_TYPE_STYLE
+ || element->type == TTML_ELEMENT_TYPE_REGION) {
+ TtmlStyleSet *ss;
+ ss = ttml_parse_style_set (node);
+ if (ss)
+ element->style_set = ss;
+ else
+ GST_CAT_WARNING (ttmlparse_debug,
+ "Style or Region contains no styling attributes.");
+ }
+
+ if ((value = ttml_get_xml_property (node, "region"))) {
+ element->region = g_strdup (value);
+ g_free (value);
+ }
+
+ if ((value = ttml_get_xml_property (node, "begin"))) {
+ element->begin = ttml_parse_timecode (value);
+ g_free (value);
+ } else {
+ element->begin = GST_CLOCK_TIME_NONE;
+ }
+
+ if ((value = ttml_get_xml_property (node, "end"))) {
+ element->end = ttml_parse_timecode (value);
+ g_free (value);
+ } else {
+ element->end = GST_CLOCK_TIME_NONE;
+ }
+
+ if (node->content) {
+ GST_CAT_LOG (ttmlparse_debug, "Node content: %s", node->content);
+ element->text = g_strdup ((const gchar *) node->content);
+ }
+
+ if ((value = ttml_get_xml_property (node, "space"))) {
+ if (g_strcmp0 (value, "preserve") == 0)
+ element->whitespace_mode = TTML_WHITESPACE_MODE_PRESERVE;
+ else if (g_strcmp0 (value, "default") == 0)
+ element->whitespace_mode = TTML_WHITESPACE_MODE_DEFAULT;
+ g_free (value);
+ }
+
+ return element;
+}
+
+
+static GNode *
+ttml_parse_body (const xmlNode * node)
+{
+ GNode *ret;
+ TtmlElement *element;
+
+ GST_CAT_LOG (ttmlparse_debug, "parsing node %s", node->name);
+ element = ttml_parse_element (node);
+ if (element)
+ ret = g_node_new (element);
+ else
+ return NULL;
+
+ for (node = node->children; node != NULL; node = node->next) {
+ GNode *descendants = NULL;
+ if (!xmlIsBlankNode (node) && (descendants = ttml_parse_body (node)))
+ g_node_append (ret, descendants);
+ }
+
+ return ret;
+}
+
+
+/* Update the fields of a GstSubtitleStyleSet, @style_set, according to the
+ * values defined in a TtmlStyleSet, @tss, and a given cell resolution. */
+static void
+ttml_update_style_set (GstSubtitleStyleSet * style_set, TtmlStyleSet * tss,
+ guint cellres_x, guint cellres_y)
+{
+ const gchar *attr;
+
+ if ((attr = ttml_style_set_get_attr (tss, "textDirection"))) {
+ if (g_strcmp0 (attr, "rtl") == 0)
+ style_set->text_direction = GST_SUBTITLE_TEXT_DIRECTION_RTL;
+ else
+ style_set->text_direction = GST_SUBTITLE_TEXT_DIRECTION_LTR;
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "fontFamily"))) {
+ if (strlen (attr) <= MAX_FONT_FAMILY_NAME_LENGTH) {
+ g_free (style_set->font_family);
+ style_set->font_family = g_strdup (attr);
+ } else {
+ GST_CAT_WARNING (ttmlparse_debug,
+ "Ignoring font family name as it's overly long.");
+ }
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "fontSize"))) {
+ style_set->font_size = g_ascii_strtod (attr, NULL) / 100.0;
+ }
+ style_set->font_size *= (1.0 / cellres_y);
+
+ if ((attr = ttml_style_set_get_attr (tss, "lineHeight"))) {
+ /* The TTML spec (section 8.2.12) recommends using a line height of 125%
+ * when "normal" is specified. */
+ if (g_strcmp0 (attr, "normal") == 0)
+ style_set->line_height = 1.25;
+ else
+ style_set->line_height = g_ascii_strtod (attr, NULL) / 100.0;
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "textAlign"))) {
+ if (g_strcmp0 (attr, "left") == 0)
+ style_set->text_align = GST_SUBTITLE_TEXT_ALIGN_LEFT;
+ else if (g_strcmp0 (attr, "center") == 0)
+ style_set->text_align = GST_SUBTITLE_TEXT_ALIGN_CENTER;
+ else if (g_strcmp0 (attr, "right") == 0)
+ style_set->text_align = GST_SUBTITLE_TEXT_ALIGN_RIGHT;
+ else if (g_strcmp0 (attr, "end") == 0)
+ style_set->text_align = GST_SUBTITLE_TEXT_ALIGN_END;
+ else
+ style_set->text_align = GST_SUBTITLE_TEXT_ALIGN_START;
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "color"))) {
+ style_set->color = ttml_parse_colorstring (attr);
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "backgroundColor"))) {
+ style_set->background_color = ttml_parse_colorstring (attr);
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "fontStyle"))) {
+ if (g_strcmp0 (attr, "italic") == 0)
+ style_set->font_style = GST_SUBTITLE_FONT_STYLE_ITALIC;
+ else
+ style_set->font_style = GST_SUBTITLE_FONT_STYLE_NORMAL;
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "fontWeight"))) {
+ if (g_strcmp0 (attr, "bold") == 0)
+ style_set->font_weight = GST_SUBTITLE_FONT_WEIGHT_BOLD;
+ else
+ style_set->font_weight = GST_SUBTITLE_FONT_WEIGHT_NORMAL;
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "textDecoration"))) {
+ if (g_strcmp0 (attr, "underline") == 0)
+ style_set->text_decoration = GST_SUBTITLE_TEXT_DECORATION_UNDERLINE;
+ else
+ style_set->text_decoration = GST_SUBTITLE_TEXT_DECORATION_NONE;
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "unicodeBidi"))) {
+ if (g_strcmp0 (attr, "embed") == 0)
+ style_set->unicode_bidi = GST_SUBTITLE_UNICODE_BIDI_EMBED;
+ else if (g_strcmp0 (attr, "bidiOverride") == 0)
+ style_set->unicode_bidi = GST_SUBTITLE_UNICODE_BIDI_OVERRIDE;
+ else
+ style_set->unicode_bidi = GST_SUBTITLE_UNICODE_BIDI_NORMAL;
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "wrapOption"))) {
+ if (g_strcmp0 (attr, "noWrap") == 0)
+ style_set->wrap_option = GST_SUBTITLE_WRAPPING_OFF;
+ else
+ style_set->wrap_option = GST_SUBTITLE_WRAPPING_ON;
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "multiRowAlign"))) {
+ if (g_strcmp0 (attr, "start") == 0)
+ style_set->multi_row_align = GST_SUBTITLE_MULTI_ROW_ALIGN_START;
+ else if (g_strcmp0 (attr, "center") == 0)
+ style_set->multi_row_align = GST_SUBTITLE_MULTI_ROW_ALIGN_CENTER;
+ else if (g_strcmp0 (attr, "end") == 0)
+ style_set->multi_row_align = GST_SUBTITLE_MULTI_ROW_ALIGN_END;
+ else
+ style_set->multi_row_align = GST_SUBTITLE_MULTI_ROW_ALIGN_AUTO;
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "linePadding"))) {
+ style_set->line_padding = g_ascii_strtod (attr, NULL);
+ style_set->line_padding *= (1.0 / cellres_x);
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "origin"))) {
+ gchar *c;
+ style_set->origin_x = g_ascii_strtod (attr, &c) / 100.0;
+ while (!g_ascii_isdigit (*c) && *c != '+' && *c != '-')
+ ++c;
+ style_set->origin_y = g_ascii_strtod (c, NULL) / 100.0;
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "extent"))) {
+ gchar *c;
+ style_set->extent_w = g_ascii_strtod (attr, &c) / 100.0;
+ if ((style_set->origin_x + style_set->extent_w) > 1.0) {
+ style_set->extent_w = 1.0 - style_set->origin_x;
+ }
+ while (!g_ascii_isdigit (*c) && *c != '+' && *c != '-')
+ ++c;
+ style_set->extent_h = g_ascii_strtod (c, NULL) / 100.0;
+ if ((style_set->origin_y + style_set->extent_h) > 1.0) {
+ style_set->extent_h = 1.0 - style_set->origin_y;
+ }
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "displayAlign"))) {
+ if (g_strcmp0 (attr, "center") == 0)
+ style_set->display_align = GST_SUBTITLE_DISPLAY_ALIGN_CENTER;
+ else if (g_strcmp0 (attr, "after") == 0)
+ style_set->display_align = GST_SUBTITLE_DISPLAY_ALIGN_AFTER;
+ else
+ style_set->display_align = GST_SUBTITLE_DISPLAY_ALIGN_BEFORE;
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "padding"))) {
+ gchar **decimals;
+ guint n_decimals;
+ guint i;
+
+ decimals = g_strsplit (attr, "%", 0);
+ n_decimals = g_strv_length (decimals) - 1;
+ for (i = 0; i < n_decimals; ++i)
+ g_strstrip (decimals[i]);
+
+ switch (n_decimals) {
+ case 1:
+ style_set->padding_start = style_set->padding_end =
+ style_set->padding_before = style_set->padding_after =
+ g_ascii_strtod (decimals[0], NULL) / 100.0;
+ break;
+
+ case 2:
+ style_set->padding_before = style_set->padding_after =
+ g_ascii_strtod (decimals[0], NULL) / 100.0;
+ style_set->padding_start = style_set->padding_end =
+ g_ascii_strtod (decimals[1], NULL) / 100.0;
+ break;
+
+ case 3:
+ style_set->padding_before = g_ascii_strtod (decimals[0], NULL) / 100.0;
+ style_set->padding_start = style_set->padding_end =
+ g_ascii_strtod (decimals[1], NULL) / 100.0;
+ style_set->padding_after = g_ascii_strtod (decimals[2], NULL) / 100.0;
+ break;
+
+ case 4:
+ style_set->padding_before = g_ascii_strtod (decimals[0], NULL) / 100.0;
+ style_set->padding_end = g_ascii_strtod (decimals[1], NULL) / 100.0;
+ style_set->padding_after = g_ascii_strtod (decimals[2], NULL) / 100.0;
+ style_set->padding_start = g_ascii_strtod (decimals[3], NULL) / 100.0;
+ break;
+ }
+ g_strfreev (decimals);
+
+ /* Padding values in TTML files are relative to the region width & height;
+ * make them relative to the overall display width & height like all other
+ * dimensions. */
+ style_set->padding_before *= style_set->extent_h;
+ style_set->padding_after *= style_set->extent_h;
+ style_set->padding_end *= style_set->extent_w;
+ style_set->padding_start *= style_set->extent_w;
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "writingMode"))) {
+ if (g_str_has_prefix (attr, "rl"))
+ style_set->writing_mode = GST_SUBTITLE_WRITING_MODE_RLTB;
+ else if ((g_strcmp0 (attr, "tbrl") == 0)
+ || (g_strcmp0 (attr, "tb") == 0))
+ style_set->writing_mode = GST_SUBTITLE_WRITING_MODE_TBRL;
+ else if (g_strcmp0 (attr, "tblr") == 0)
+ style_set->writing_mode = GST_SUBTITLE_WRITING_MODE_TBLR;
+ else
+ style_set->writing_mode = GST_SUBTITLE_WRITING_MODE_LRTB;
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "showBackground"))) {
+ if (g_strcmp0 (attr, "whenActive") == 0)
+ style_set->show_background = GST_SUBTITLE_BACKGROUND_MODE_WHEN_ACTIVE;
+ else
+ style_set->show_background = GST_SUBTITLE_BACKGROUND_MODE_ALWAYS;
+ }
+
+ if ((attr = ttml_style_set_get_attr (tss, "overflow"))) {
+ if (g_strcmp0 (attr, "visible") == 0)
+ style_set->overflow = GST_SUBTITLE_OVERFLOW_MODE_VISIBLE;
+ else
+ style_set->overflow = GST_SUBTITLE_OVERFLOW_MODE_HIDDEN;
+ }
+}
+
+
+static TtmlStyleSet *
+ttml_style_set_copy (TtmlStyleSet * style_set)
+{
+ GHashTableIter iter;
+ gpointer attr_name, attr_value;
+ TtmlStyleSet *ret = ttml_style_set_new ();
+
+ g_hash_table_iter_init (&iter, style_set->table);
+ while (g_hash_table_iter_next (&iter, &attr_name, &attr_value)) {
+ ttml_style_set_add_attr (ret, (const gchar *) attr_name,
+ (const gchar *) attr_value);
+ }
+
+ return ret;
+}
+
+
+/* set2 overrides set1. Unlike style inheritance, merging will result in all
+ * values from set1 being merged into set2. */
+static TtmlStyleSet *
+ttml_style_set_merge (TtmlStyleSet * set1, TtmlStyleSet * set2)
+{
+ TtmlStyleSet *ret = NULL;
+
+ if (set1) {
+ ret = ttml_style_set_copy (set1);
+
+ if (set2) {
+ GHashTableIter iter;
+ gpointer attr_name, attr_value;
+
+ g_hash_table_iter_init (&iter, set2->table);
+ while (g_hash_table_iter_next (&iter, &attr_name, &attr_value)) {
+ ttml_style_set_add_attr (ret, (const gchar *) attr_name,
+ (const gchar *) attr_value);
+ }
+ }
+ } else if (set2) {
+ ret = ttml_style_set_copy (set2);
+ }
+
+ return ret;
+}
+
+
+static gchar *
+ttml_get_relative_font_size (const gchar * parent_size,
+ const gchar * child_size)
+{
+ guint psize = (guint) g_ascii_strtoull (parent_size, NULL, 10U);
+ guint csize = (guint) g_ascii_strtoull (child_size, NULL, 10U);
+ csize = (csize * psize) / 100U;
+ return g_strdup_printf ("%u%%", csize);
+}
+
+
+static TtmlStyleSet *
+ttml_style_set_inherit (TtmlStyleSet * parent, TtmlStyleSet * child)
+{
+ TtmlStyleSet *ret = NULL;
+ GHashTableIter iter;
+ gpointer attr_name, attr_value;
+
+ if (child) {
+ ret = ttml_style_set_copy (child);
+ } else {
+ ret = ttml_style_set_new ();
+ }
+
+ if (!parent)
+ return ret;
+
+ g_hash_table_iter_init (&iter, parent->table);
+ while (g_hash_table_iter_next (&iter, &attr_name, &attr_value)) {
+ /* In TTML, if an element which has a defined fontSize is the child of an
+ * element that also has a defined fontSize, the child's font size is
+ * relative to that of its parent. If its parent doesn't have a defined
+ * fontSize, then the child's fontSize is relative to the document's cell
+ * size. Therefore, if the former is true, we calculate the value of
+ * fontSize based on the parent's fontSize; otherwise, we simply keep
+ * the value defined in the child's style set. */
+ if (g_strcmp0 ((const gchar *) attr_name, "fontSize") == 0
+ && ttml_style_set_contains_attr (ret, (const gchar *) attr_name)) {
+ const gchar *original_child_font_size =
+ ttml_style_set_get_attr (child, "fontSize");
+ gchar *scaled_child_font_size =
+ ttml_get_relative_font_size ((const gchar *) attr_value,
+ original_child_font_size);
+ GST_CAT_LOG (ttmlparse_debug, "Calculated font size: %s",
+ scaled_child_font_size);
+ ttml_style_set_add_attr (ret, (const gchar *) attr_name,
+ scaled_child_font_size);
+ g_free (scaled_child_font_size);
+ }
+
+ /* Not all styling attributes are inherited in TTML. */
+ if (g_strcmp0 ((const gchar *) attr_name, "backgroundColor") != 0
+ && g_strcmp0 ((const gchar *) attr_name, "origin") != 0
+ && g_strcmp0 ((const gchar *) attr_name, "extent") != 0
+ && g_strcmp0 ((const gchar *) attr_name, "displayAlign") != 0
+ && g_strcmp0 ((const gchar *) attr_name, "overflow") != 0
+ && g_strcmp0 ((const gchar *) attr_name, "padding") != 0
+ && g_strcmp0 ((const gchar *) attr_name, "writingMode") != 0
+ && g_strcmp0 ((const gchar *) attr_name, "showBackground") != 0
+ && g_strcmp0 ((const gchar *) attr_name, "unicodeBidi") != 0) {
+ if (!ttml_style_set_contains_attr (ret, (const gchar *) attr_name)) {
+ ttml_style_set_add_attr (ret, (const gchar *) attr_name,
+ (const gchar *) attr_value);
+ }
+ }
+ }
+
+ return ret;
+}
+
+
+static gchar *
+ttml_get_element_type_string (TtmlElement * element)
+{
+ switch (element->type) {
+ case TTML_ELEMENT_TYPE_STYLE:
+ return g_strdup ("<style>");
+ break;
+ case TTML_ELEMENT_TYPE_REGION:
+ return g_strdup ("<region>");
+ break;
+ case TTML_ELEMENT_TYPE_BODY:
+ return g_strdup ("<body>");
+ break;
+ case TTML_ELEMENT_TYPE_DIV:
+ return g_strdup ("<div>");
+ break;
+ case TTML_ELEMENT_TYPE_P:
+ return g_strdup ("<p>");
+ break;
+ case TTML_ELEMENT_TYPE_SPAN:
+ return g_strdup ("<span>");
+ break;
+ case TTML_ELEMENT_TYPE_ANON_SPAN:
+ return g_strdup ("<anon-span>");
+ break;
+ case TTML_ELEMENT_TYPE_BR:
+ return g_strdup ("<br>");
+ break;
+ default:
+ return g_strdup ("Unknown");
+ break;
+ }
+}
+
+
+/* Merge styles referenced by an element. */
+static gboolean
+ttml_resolve_styles (GNode * node, gpointer data)
+{
+ TtmlStyleSet *tmp = NULL;
+ TtmlElement *element, *style;
+ GHashTable *styles_table;
+ gchar *type_string;
+ guint i;
+
+ styles_table = (GHashTable *) data;
+ element = node->data;
+
+ type_string = ttml_get_element_type_string (element);
+ GST_CAT_LOG (ttmlparse_debug, "Element type: %s", type_string);
+ g_free (type_string);
+
+ if (!element->styles)
+ return FALSE;
+
+ for (i = 0; i < g_strv_length (element->styles); ++i) {
+ tmp = element->style_set;
+ style = g_hash_table_lookup (styles_table, element->styles[i]);
+ if (style) {
+ GST_CAT_LOG (ttmlparse_debug, "Merging style %s...", element->styles[i]);
+ element->style_set = ttml_style_set_merge (element->style_set,
+ style->style_set);
+ ttml_style_set_delete (tmp);
+ } else {
+ GST_CAT_WARNING (ttmlparse_debug,
+ "Element references an unknown style (%s)", element->styles[i]);
+ }
+ }
+
+ GST_CAT_LOG (ttmlparse_debug, "Style set after merging:");
+ ttml_style_set_print (element->style_set);
+
+ return FALSE;
+}
+
+
+static void
+ttml_resolve_referenced_styles (GList * trees, GHashTable * styles_table)
+{
+ GList *tree;
+
+ for (tree = g_list_first (trees); tree; tree = tree->next) {
+ GNode *root = (GNode *) tree->data;
+ g_node_traverse (root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, ttml_resolve_styles,
+ styles_table);
+ }
+}
+
+
+/* Inherit styling attributes from parent. */
+static gboolean
+ttml_inherit_styles (GNode * node, gpointer data)
+{
+ TtmlStyleSet *tmp = NULL;
+ TtmlElement *element, *parent;
+ gchar *type_string;
+
+ element = node->data;
+
+ type_string = ttml_get_element_type_string (element);
+ GST_CAT_LOG (ttmlparse_debug, "Element type: %s", type_string);
+ g_free (type_string);
+
+ if (node->parent) {
+ parent = node->parent->data;
+ if (parent->style_set) {
+ tmp = element->style_set;
+ if (element->type == TTML_ELEMENT_TYPE_ANON_SPAN) {
+ /* Anon spans should merge all style attributes from their parent. */
+ element->style_set = ttml_style_set_merge (parent->style_set,
+ element->style_set);
+ } else {
+ element->style_set = ttml_style_set_inherit (parent->style_set,
+ element->style_set);
+ }
+ ttml_style_set_delete (tmp);
+ }
+ }
+
+ GST_CAT_LOG (ttmlparse_debug, "Style set after inheriting:");
+ ttml_style_set_print (element->style_set);
+
+ return FALSE;
+}
+
+
+static void
+ttml_inherit_element_styles (GList * trees)
+{
+ GList *tree;
+
+ for (tree = g_list_first (trees); tree; tree = tree->next) {
+ GNode *root = (GNode *) tree->data;
+ g_node_traverse (root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, ttml_inherit_styles,
+ NULL);
+ }
+}
+
+
+/* If whitespace_mode isn't explicitly set for this element, inherit from its
+ * parent. If this element is the root of the tree, set whitespace_mode to
+ * that of the overall document. */
+static gboolean
+ttml_inherit_element_whitespace_mode (GNode * node, gpointer data)
+{
+ TtmlWhitespaceMode *doc_mode = (TtmlWhitespaceMode *) data;
+ TtmlElement *element = node->data;
+ TtmlElement *parent;
+
+ if (element->whitespace_mode != TTML_WHITESPACE_MODE_NONE)
+ return FALSE;
+
+ if (G_NODE_IS_ROOT (node)) {
+ element->whitespace_mode = *doc_mode;
+ return FALSE;
+ }
+
+ parent = node->parent->data;
+ element->whitespace_mode = parent->whitespace_mode;
+ return FALSE;
+}
+
+
+static void
+ttml_inherit_whitespace_mode (GNode * tree, TtmlWhitespaceMode doc_mode)
+{
+ g_node_traverse (tree, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
+ ttml_inherit_element_whitespace_mode, &doc_mode);
+}
+
+
+static gboolean
+ttml_free_node_data (GNode * node, gpointer data)
+{
+ TtmlElement *element = node->data;
+ ttml_delete_element (element);
+ return FALSE;
+}
+
+
+static void
+ttml_delete_tree (GNode * tree)
+{
+ g_node_traverse (tree, G_PRE_ORDER, G_TRAVERSE_ALL, -1, ttml_free_node_data,
+ NULL);
+ g_node_destroy (tree);
+}
+
+
+typedef struct
+{
+ GstClockTime begin;
+ GstClockTime end;
+} ClipWindow;
+
+static gboolean
+ttml_clip_element_period (GNode * node, gpointer data)
+{
+ TtmlElement *element = node->data;
+ ClipWindow *window = data;
+
+ if (!GST_CLOCK_TIME_IS_VALID (element->begin)) {
+ return FALSE;
+ }
+
+ if (element->begin > window->end || element->end < window->begin) {
+ ttml_delete_tree (node);
+ node = NULL;
+ return FALSE;
+ }
+
+ element->begin = MAX (element->begin, window->begin);
+ element->end = MIN (element->end, window->end);
+ return FALSE;
+}
+
+
+static void
+ttml_apply_time_window (GNode * tree, GstClockTime window_begin,
+ GstClockTime window_end)
+{
+ ClipWindow window;
+ window.begin = window_begin;
+ window.end = window_end;
+
+ g_node_traverse (tree, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
+ ttml_clip_element_period, &window);
+}
+
+
+static gboolean
+ttml_resolve_element_timings (GNode * node, gpointer data)
+{
+ TtmlElement *element, *leaf;
+
+ leaf = element = node->data;
+
+ if (GST_CLOCK_TIME_IS_VALID (leaf->begin)
+ && GST_CLOCK_TIME_IS_VALID (leaf->end)) {
+ GST_CAT_LOG (ttmlparse_debug, "Leaf node already has timing.");
+ return FALSE;
+ }
+
+ /* Inherit timings from ancestor. */
+ while (node->parent && !GST_CLOCK_TIME_IS_VALID (element->begin)) {
+ node = node->parent;
+ element = node->data;
+ }
+
+ if (!GST_CLOCK_TIME_IS_VALID (element->begin)) {
+ GST_CAT_WARNING (ttmlparse_debug,
+ "No timing found for element. Removing from tree...");
+ g_node_unlink (node);
+ } else {
+ leaf->begin = element->begin;
+ leaf->end = element->end;
+ GST_CAT_LOG (ttmlparse_debug, "Leaf begin: %" GST_TIME_FORMAT,
+ GST_TIME_ARGS (leaf->begin));
+ GST_CAT_LOG (ttmlparse_debug, "Leaf end: %" GST_TIME_FORMAT,
+ GST_TIME_ARGS (leaf->end));
+ }
+
+ return FALSE;
+}
+
+
+static void
+ttml_resolve_timings (GNode * tree)
+{
+ g_node_traverse (tree, G_PRE_ORDER, G_TRAVERSE_LEAVES, -1,
+ ttml_resolve_element_timings, NULL);
+}
+
+
+static gboolean
+ttml_resolve_leaf_region (GNode * node, gpointer data)
+{
+ TtmlElement *element, *leaf;
+ leaf = element = node->data;
+
+ while (node->parent && !element->region) {
+ node = node->parent;
+ element = node->data;
+ }
+
+ if (element->region) {
+ leaf->region = g_strdup (element->region);
+ GST_CAT_LOG (ttmlparse_debug, "Leaf region: %s", leaf->region);
+ } else {
+ GST_CAT_WARNING (ttmlparse_debug, "No region found above leaf element.");
+ }
+
+ return FALSE;
+}
+
+
+static void
+ttml_resolve_regions (GNode * tree)
+{
+ g_node_traverse (tree, G_PRE_ORDER, G_TRAVERSE_LEAVES, -1,
+ ttml_resolve_leaf_region, NULL);
+}
+
+
+typedef struct
+{
+ GstClockTime start_time;
+ GstClockTime next_transition_time;
+} TrState;
+
+
+static gboolean
+ttml_update_transition_time (GNode * node, gpointer data)
+{
+ TtmlElement *element = node->data;
+ TrState *state = (TrState *) data;
+
+ if ((element->begin < state->next_transition_time)
+ && (!GST_CLOCK_TIME_IS_VALID (state->start_time)
+ || (element->begin > state->start_time))) {
+ state->next_transition_time = element->begin;
+ GST_CAT_LOG (ttmlparse_debug,
+ "Updating next transition time to element begin time (%"
+ GST_TIME_FORMAT ")", GST_TIME_ARGS (state->next_transition_time));
+ return FALSE;
+ }
+
+ if ((element->end < state->next_transition_time)
+ && (element->end > state->start_time)) {
+ state->next_transition_time = element->end;
+ GST_CAT_LOG (ttmlparse_debug,
+ "Updating next transition time to element end time (%"
+ GST_TIME_FORMAT ")", GST_TIME_ARGS (state->next_transition_time));
+ }
+
+ return FALSE;
+}
+
+
+/* Return details about the next transition after @time. */
+static GstClockTime
+ttml_find_next_transition (GList * trees, GstClockTime time)
+{
+ TrState state;
+ state.start_time = time;
+ state.next_transition_time = GST_CLOCK_TIME_NONE;
+
+ for (trees = g_list_first (trees); trees; trees = trees->next) {
+ GNode *tree = (GNode *) trees->data;
+ g_node_traverse (tree, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
+ ttml_update_transition_time, &state);
+ }
+
+ GST_CAT_LOG (ttmlparse_debug, "Next transition is at %" GST_TIME_FORMAT,
+ GST_TIME_ARGS (state.next_transition_time));
+
+ return state.next_transition_time;
+}
+
+
+/* Remove nodes from tree that are not visible at @time. */
+static GNode *
+ttml_remove_nodes_by_time (GNode * node, GstClockTime time)
+{
+ GNode *child, *next_child;
+ TtmlElement *element;
+ element = node->data;
+
+ child = node->children;
+ next_child = child ? child->next : NULL;
+ while (child) {
+ ttml_remove_nodes_by_time (child, time);
+ child = next_child;
+ next_child = child ? child->next : NULL;
+ }
+
+ if (!node->children && ((element->begin > time) || (element->end <= time))) {
+ g_node_destroy (node);
+ node = NULL;
+ }
+
+ return node;
+}
+
+
+/* Return a list of trees containing the elements and their ancestors that are
+ * visible at @time. */
+static GList *
+ttml_get_active_trees (GList * element_trees, GstClockTime time)
+{
+ GList *tree;
+ GList *ret = NULL;
+
+ for (tree = g_list_first (element_trees); tree; tree = tree->next) {
+ GNode *root = g_node_copy ((GNode *) tree->data);
+ GST_CAT_LOG (ttmlparse_debug, "There are %u nodes in tree.",
+ g_node_n_nodes (root, G_TRAVERSE_ALL));
+ root = ttml_remove_nodes_by_time (root, time);
+ if (root) {
+ GST_CAT_LOG (ttmlparse_debug,
+ "After filtering there are %u nodes in tree.", g_node_n_nodes (root,
+ G_TRAVERSE_ALL));
+
+ ret = g_list_append (ret, root);
+ } else {
+ GST_CAT_LOG (ttmlparse_debug,
+ "All elements have been filtered from tree.");
+ }
+ }
+
+ GST_CAT_DEBUG (ttmlparse_debug, "There are %u trees in returned list.",
+ g_list_length (ret));
+ return ret;
+}
+
+
+static GList *
+ttml_create_scenes (GList * region_trees)
+{
+ TtmlScene *cur_scene = NULL;
+ GList *output_scenes = NULL;
+ GList *active_trees = NULL;
+ GstClockTime timestamp = GST_CLOCK_TIME_NONE;
+
+ while ((timestamp = ttml_find_next_transition (region_trees, timestamp))
+ != GST_CLOCK_TIME_NONE) {
+ GST_CAT_LOG (ttmlparse_debug,
+ "Next transition found at time %" GST_TIME_FORMAT,
+ GST_TIME_ARGS (timestamp));
+ if (cur_scene) {
+ cur_scene->end = timestamp;
+ output_scenes = g_list_append (output_scenes, cur_scene);
+ }
+
+ active_trees = ttml_get_active_trees (region_trees, timestamp);
+ GST_CAT_LOG (ttmlparse_debug, "There will be %u active regions after "
+ "transition", g_list_length (active_trees));
+
+ if (active_trees) {
+ cur_scene = g_slice_new0 (TtmlScene);
+ cur_scene->begin = timestamp;
+ cur_scene->trees = active_trees;
+ } else {
+ cur_scene = NULL;
+ }
+ }
+
+ return output_scenes;
+}
+
+
+/* Handle element whitespace in accordance with section 7.2.3 of the TTML
+ * specification. Specifically, this function implements the
+ * white-space-collapse="true" and linefeed-treatment="treat-as-space"
+ * behaviours. Note that stripping of whitespace at the start and end of line
+ * areas (suppress-at-line-break="auto" and
+ * white-space-treatment="ignore-if-surrounding-linefeed" behaviours) can only
+ * be done by the renderer once the text from multiple elements has been laid
+ * out in line areas. */
+static gboolean
+ttml_handle_element_whitespace (GNode * node, gpointer data)
+{
+ TtmlElement *element = node->data;
+ guint space_count = 0;
+ guint textlen;
+ gchar *c;
+
+ if (!element->text
+ || (element->whitespace_mode == TTML_WHITESPACE_MODE_PRESERVE)) {
+ return FALSE;
+ }
+
+ textlen = strlen (element->text);
+ for (c = element->text; TRUE; c = g_utf8_next_char (c)) {
+ gchar buf[6] = { 0 };
+ gunichar u = g_utf8_get_char (c);
+ gint nbytes = g_unichar_to_utf8 (u, buf);
+
+ if (nbytes == 1 && buf[0] == 0xA) {
+ *c = ' ';
+ buf[0] = 0x20;
+ }
+
+ if (nbytes == 1 && (buf[0] == 0x20 || buf[0] == 0x9 || buf[0] == 0xD)) {
+ ++space_count;
+ } else {
+ if (space_count > 1) {
+ gchar *new_head = c - space_count + 1;
+ g_strlcpy (new_head, c, textlen);
+ c = new_head;
+ }
+ space_count = 0;
+ if (nbytes == 1 && buf[0] == 0x0) /* Reached end of string. */
+ break;
+ }
+ }
+
+ return FALSE;
+}
+
+
+static void
+ttml_handle_whitespace (GNode * tree)
+{
+ g_node_traverse (tree, G_PRE_ORDER, G_TRAVERSE_LEAVES, -1,
+ ttml_handle_element_whitespace, NULL);
+}
+
+
+/* Store child elements of @node with name @element_name in @table, as long as
+ * @table doesn't already contain an element with the same ID. */
+static void
+ttml_store_unique_children (xmlNodePtr node, const gchar * element_name,
+ GHashTable * table)
+{
+ xmlNodePtr ptr;
+
+ for (ptr = node->children; ptr; ptr = ptr->next) {
+ if (xmlStrcmp (ptr->name, (const xmlChar *) element_name) == 0) {
+ TtmlElement *element = ttml_parse_element (ptr);
+
+ if (element)
+ if (!g_hash_table_contains (table, element->id))
+ g_hash_table_insert (table, (gpointer) (element->id),
+ (gpointer) element);
+ }
+ }
+}
+
+
+/* Parse style and region elements from @head and store in their respective
+ * hash tables for future reference. */
+static void
+ttml_parse_head (xmlNodePtr head, GHashTable * styles_table,
+ GHashTable * regions_table)
+{
+ xmlNodePtr node;
+
+ for (node = head->children; node; node = node->next) {
+ if (xmlStrcmp (node->name, (const xmlChar *) "styling") == 0)
+ ttml_store_unique_children (node, "style", styles_table);
+ if (xmlStrcmp (node->name, (const xmlChar *) "layout") == 0)
+ ttml_store_unique_children (node, "region", regions_table);
+ }
+}
+
+
+/* Remove nodes that do not belong to @region, or are not an ancestor of a node
+ * belonging to @region. */
+static GNode *
+ttml_remove_nodes_by_region (GNode * node, const gchar * region)
+{
+ GNode *child, *next_child;
+ TtmlElement *element;
+ element = node->data;
+
+ child = node->children;
+ next_child = child ? child->next : NULL;
+ while (child) {
+ ttml_remove_nodes_by_region (child, region);
+ child = next_child;
+ next_child = child ? child->next : NULL;
+ }
+
+ if ((element->type == TTML_ELEMENT_TYPE_ANON_SPAN
+ || element->type != TTML_ELEMENT_TYPE_BR)
+ && element->region && (g_strcmp0 (element->region, region) != 0)) {
+ ttml_delete_element (element);
+ g_node_destroy (node);
+ return NULL;
+ }
+ if (element->type != TTML_ELEMENT_TYPE_ANON_SPAN
+ && element->type != TTML_ELEMENT_TYPE_BR && !node->children) {
+ ttml_delete_element (element);
+ g_node_destroy (node);
+ return NULL;
+ }
+
+ return node;
+}
+
+
+static TtmlElement *
+ttml_copy_element (const TtmlElement * element)
+{
+ TtmlElement *ret = g_slice_new0 (TtmlElement);
+
+ ret->type = element->type;
+ if (element->id)
+ ret->id = g_strdup (element->id);
+ if (element->styles)
+ ret->styles = g_strdupv (element->styles);
+ if (element->region)
+ ret->region = g_strdup (element->region);
+ ret->begin = element->begin;
+ ret->end = element->end;
+ if (element->style_set)
+ ret->style_set = ttml_style_set_copy (element->style_set);
+ if (element->text)
+ ret->text = g_strdup (element->text);
+ ret->text_index = element->text_index;
+
+ return ret;
+}
+
+
+static gpointer
+ttml_copy_tree_element (gconstpointer src, gpointer data)
+{
+ return ttml_copy_element ((TtmlElement *) src);
+}
+
+
+/* Split the body tree into a set of trees, each containing only the elements
+ * belonging to a single region. Returns a list of trees, one per region, each
+ * with the corresponding region element at its root. */
+static GList *
+ttml_split_body_by_region (GNode * body, GHashTable * regions)
+{
+ GHashTableIter iter;
+ gpointer key, value;
+ GList *ret = NULL;
+
+ g_hash_table_iter_init (&iter, regions);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ gchar *region_name = (gchar *) key;
+ TtmlElement *region = (TtmlElement *) value;
+ GNode *region_node = g_node_new (ttml_copy_element (region));
+ GNode *body_copy = g_node_copy_deep (body, ttml_copy_tree_element, NULL);
+
+ GST_CAT_DEBUG (ttmlparse_debug, "Creating tree for region %s", region_name);
+ GST_CAT_LOG (ttmlparse_debug, "Copy of body has %u nodes.",
+ g_node_n_nodes (body_copy, G_TRAVERSE_ALL));
+
+ body_copy = ttml_remove_nodes_by_region (body_copy, region_name);
+ if (body_copy) {
+ GST_CAT_LOG (ttmlparse_debug, "Copy of body now has %u nodes.",
+ g_node_n_nodes (body_copy, G_TRAVERSE_ALL));
+
+ /* Reparent tree to region node. */
+ g_node_prepend (region_node, body_copy);
+ }
+ GST_CAT_LOG (ttmlparse_debug, "Final tree has %u nodes.",
+ g_node_n_nodes (region_node, G_TRAVERSE_ALL));
+ ret = g_list_append (ret, region_node);
+ }
+
+ GST_CAT_DEBUG (ttmlparse_debug, "Returning %u trees.", g_list_length (ret));
+ return ret;
+}
+
+
+static guint
+ttml_add_text_to_buffer (GstBuffer * buf, const gchar * text)
+{
+ GstMemory *mem;
+ GstMapInfo map;
+ guint ret;
+
+ mem = gst_allocator_alloc (NULL, strlen (text) + 1, NULL);
+ if (!gst_memory_map (mem, &map, GST_MAP_WRITE))
+ GST_CAT_ERROR (ttmlparse_debug, "Failed to map memory.");
+
+ g_strlcpy ((gchar *) map.data, text, map.size);
+ GST_CAT_DEBUG (ttmlparse_debug, "Inserted following text into buffer: %s",
+ (gchar *) map.data);
+ gst_memory_unmap (mem, &map);
+
+ ret = gst_buffer_n_memory (buf);
+ gst_buffer_insert_memory (buf, -1, mem);
+ return ret;
+}
+
+
+/* Create a GstSubtitleElement from @element, add it to @block, and insert its
+ * associated text in @buf. */
+static void
+ttml_add_element (GstSubtitleBlock * block, TtmlElement * element,
+ GstBuffer * buf, guint cellres_x, guint cellres_y)
+{
+ GstSubtitleStyleSet *element_style = NULL;
+ guint buffer_index;
+ GstSubtitleElement *sub_element = NULL;
+
+ element_style = gst_subtitle_style_set_new ();
+ ttml_update_style_set (element_style, element->style_set,
+ cellres_x, cellres_y);
+ GST_CAT_DEBUG (ttmlparse_debug, "Creating element with text index %u",
+ element->text_index);
+
+ if (element->type != TTML_ELEMENT_TYPE_BR)
+ buffer_index = ttml_add_text_to_buffer (buf, element->text);
+ else
+ buffer_index = ttml_add_text_to_buffer (buf, "\n");
+
+ GST_CAT_DEBUG (ttmlparse_debug, "Inserted text at index %u in GstBuffer.",
+ buffer_index);
+ sub_element = gst_subtitle_element_new (element_style, buffer_index,
+ (element->whitespace_mode != TTML_WHITESPACE_MODE_PRESERVE));
+
+ gst_subtitle_block_add_element (block, sub_element);
+ GST_CAT_DEBUG (ttmlparse_debug, "Added element to block; there are now %u"
+ " elements in the block.", gst_subtitle_block_get_element_count (block));
+}
+
+
+/* Return TRUE if @color is totally transparent. */
+static gboolean
+ttml_color_is_transparent (const GstSubtitleColor * color)
+{
+ if (!color)
+ return FALSE;
+ else
+ return (color->a == 0);
+}
+
+
+/* Blend @color2 over @color1 and return the resulting color. This is currently
+ * a dummy implementation that simply returns color2 as long as it's
+ * not fully transparent. */
+/* TODO: Implement actual blending of colors. */
+static GstSubtitleColor
+ttml_blend_colors (GstSubtitleColor color1, GstSubtitleColor color2)
+{
+ if (ttml_color_is_transparent (&color2))
+ return color1;
+ else
+ return color2;
+}
+
+
+/* Create the subtitle region and its child blocks and elements for @tree,
+ * inserting element text in @buf. Ownership of created region is transferred
+ * to caller. */
+static GstSubtitleRegion *
+ttml_create_subtitle_region (GNode * tree, GstBuffer * buf, guint cellres_x,
+ guint cellres_y)
+{
+ GstSubtitleRegion *region = NULL;
+ GstSubtitleStyleSet *region_style;
+ GstSubtitleColor block_color;
+ TtmlElement *element;
+ GNode *node;
+
+ element = tree->data;
+ g_assert (element->type == TTML_ELEMENT_TYPE_REGION);
+
+ region_style = gst_subtitle_style_set_new ();
+ ttml_update_style_set (region_style, element->style_set, cellres_x,
+ cellres_y);
+ region = gst_subtitle_region_new (region_style);
+
+ node = tree->children;
+ if (!node)
+ return region;
+
+ g_assert (node->next == NULL);
+ element = node->data;
+ g_assert (element->type == TTML_ELEMENT_TYPE_BODY);
+ block_color =
+ ttml_parse_colorstring (ttml_style_set_get_attr (element->style_set,
+ "backgroundColor"));
+
+ for (node = node->children; node; node = node->next) {
+ GNode *p_node;
+ GstSubtitleColor div_color;
+
+ element = node->data;
+ g_assert (element->type == TTML_ELEMENT_TYPE_DIV);
+ div_color =
+ ttml_parse_colorstring (ttml_style_set_get_attr (element->style_set,
+ "backgroundColor"));
+ block_color = ttml_blend_colors (block_color, div_color);
+
+ for (p_node = node->children; p_node; p_node = p_node->next) {
+ GstSubtitleBlock *block = NULL;
+ GstSubtitleStyleSet *block_style;
+ GNode *content_node;
+ GstSubtitleColor p_color;
+
+ element = p_node->data;
+ g_assert (element->type == TTML_ELEMENT_TYPE_P);
+ p_color =
+ ttml_parse_colorstring (ttml_style_set_get_attr (element->style_set,
+ "backgroundColor"));
+ block_color = ttml_blend_colors (block_color, p_color);
+
+ block_style = gst_subtitle_style_set_new ();
+ ttml_update_style_set (block_style, element->style_set, cellres_x,
+ cellres_y);
+ block_style->background_color = block_color;
+ block = gst_subtitle_block_new (block_style);
+ g_assert (block != NULL);
+
+ for (content_node = p_node->children; content_node;
+ content_node = content_node->next) {
+ GNode *anon_node;
+ element = content_node->data;
+
+ if (element->type == TTML_ELEMENT_TYPE_BR
+ || element->type == TTML_ELEMENT_TYPE_ANON_SPAN) {
+ ttml_add_element (block, element, buf, cellres_x, cellres_y);
+ } else if (element->type == TTML_ELEMENT_TYPE_SPAN) {
+ /* Loop through anon-span children of this span. */
+ for (anon_node = content_node->children; anon_node;
+ anon_node = anon_node->next) {
+ element = anon_node->data;
+
+ if (element->type == TTML_ELEMENT_TYPE_BR
+ || element->type == TTML_ELEMENT_TYPE_ANON_SPAN) {
+ ttml_add_element (block, element, buf, cellres_x, cellres_y);
+ } else {
+ GST_CAT_ERROR (ttmlparse_debug,
+ "Element type not allowed at this level of document.");
+ }
+ }
+ } else {
+ GST_CAT_ERROR (ttmlparse_debug,
+ "Element type not allowed at this level of document.");
+ }
+ }
+
+ gst_subtitle_region_add_block (region, block);
+ GST_CAT_DEBUG (ttmlparse_debug,
+ "Added block to region; there are now %u blocks" " in the region.",
+ gst_subtitle_region_get_block_count (region));
+ }
+ }
+
+ return region;
+}
+
+
+/* For each scene, create data objects to describe the layout and styling of
+ * that scene and attach it as metadata to the GstBuffer that will be used to
+ * carry that scene's text. */
+static void
+ttml_attach_scene_metadata (GList * scenes, guint cellres_x, guint cellres_y)
+{
+ GList *scene_entry;
+
+ for (scene_entry = g_list_first (scenes); scene_entry;
+ scene_entry = scene_entry->next) {
+ TtmlScene *scene = scene_entry->data;
+ GList *region_tree;
+ GPtrArray *regions = g_ptr_array_new_with_free_func (
+ (GDestroyNotify) gst_subtitle_region_unref);
+
+ scene->buf = gst_buffer_new ();
+ GST_BUFFER_PTS (scene->buf) = scene->begin;
+ GST_BUFFER_DURATION (scene->buf) = (scene->end - scene->begin);
+
+ for (region_tree = g_list_first (scene->trees); region_tree;
+ region_tree = region_tree->next) {
+ GNode *tree = (GNode *) region_tree->data;
+ GstSubtitleRegion *region;
+
+ region = ttml_create_subtitle_region (tree, scene->buf, cellres_x,
+ cellres_y);
+ g_ptr_array_add (regions, region);
+ }
+
+ gst_buffer_add_subtitle_meta (scene->buf, regions);
+ }
+}
+
+
+static GList *
+create_buffer_list (GList * scenes)
+{
+ GList *ret = NULL;
+
+ while (scenes) {
+ TtmlScene *scene = scenes->data;
+ ret = g_list_prepend (ret, gst_buffer_ref (scene->buf));
+ scenes = scenes->next;
+ }
+ return g_list_reverse (ret);
+}
+
+
+static void
+ttml_delete_scene (TtmlScene * scene)
+{
+ if (scene->trees)
+ g_list_free_full (scene->trees, (GDestroyNotify) g_node_destroy);
+ if (scene->buf)
+ gst_buffer_unref (scene->buf);
+ g_slice_free (TtmlScene, scene);
+}
+
+
+static void
+ttml_assign_region_times (GList * region_trees, GstClockTime doc_begin,
+ GstClockTime doc_duration)
+{
+ GList *tree;
+
+ for (tree = g_list_first (region_trees); tree; tree = tree->next) {
+ GNode *region_node = (GNode *) tree->data;
+ TtmlElement *region = (TtmlElement *) region_node->data;
+ const gchar *show_background_value =
+ ttml_style_set_get_attr (region->style_set, "showBackground");
+ gboolean always_visible =
+ (g_strcmp0 (show_background_value, "always") == 0);
+
+ GstSubtitleColor region_color = { 0, 0, 0, 0 };
+ if (ttml_style_set_contains_attr (region->style_set, "backgroundColor"))
+ region_color =
+ ttml_parse_colorstring (ttml_style_set_get_attr (region->style_set,
+ "backgroundColor"));
+
+ if (always_visible && !ttml_color_is_transparent (&region_color)) {
+ GST_CAT_DEBUG (ttmlparse_debug, "Assigning times to region.");
+ /* If the input XML document was not encapsulated in a container that
+ * provides timing information for the document as a whole (i.e., its
+ * PTS and duration) and the region background should be always visible,
+ * set region start time to 0 and end time to 24 hours. This ensures that
+ * regions with showBackground="always" are visible for the entirety of
+ * any real-world stream. */
+ region->begin = (doc_begin != GST_CLOCK_TIME_NONE) ? doc_begin : 0;
+ region->end = (doc_duration != GST_CLOCK_TIME_NONE) ?
+ region->begin + doc_duration : 24 * 3600 * GST_SECOND;
+ }
+ }
+}
+
+
+static xmlNodePtr
+ttml_find_child (xmlNodePtr parent, const gchar * name)
+{
+ xmlNodePtr child = parent->children;
+ while (child && xmlStrcmp (child->name, (const xmlChar *) name) != 0)
+ child = child->next;
+ return child;
+}
+
+
+GList *
+ttml_parse (const gchar * input, GstClockTime begin, GstClockTime duration)
+{
+ xmlDocPtr doc;
+ xmlNodePtr root_node, head_node, body_node;
+
+ GHashTable *styles_table, *regions_table;
+ GList *output_buffers = NULL;
+ gchar *value;
+ guint cellres_x, cellres_y;
+ TtmlWhitespaceMode doc_whitespace_mode = TTML_WHITESPACE_MODE_DEFAULT;
+
+ if (!g_utf8_validate (input, -1, NULL)) {
+ GST_CAT_ERROR (ttmlparse_debug, "Input isn't valid UTF-8.");
+ return NULL;
+ }
+ GST_CAT_LOG (ttmlparse_debug, "Input:\n%s", input);
+
+ styles_table = g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
+ (GDestroyNotify) ttml_delete_element);
+ regions_table = g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
+ (GDestroyNotify) ttml_delete_element);
+
+ /* Parse input. */
+ doc = xmlReadMemory (input, strlen (input), "any_doc_name", NULL,
+ XML_PARSE_NOBLANKS);
+ if (!doc) {
+ GST_CAT_ERROR (ttmlparse_debug, "Failed to parse document.");
+ return NULL;
+ }
+ root_node = xmlDocGetRootElement (doc);
+
+ if (xmlStrcmp (root_node->name, (const xmlChar *) "tt") != 0) {
+ GST_CAT_ERROR (ttmlparse_debug, "Root element of document is not tt:tt.");
+ xmlFreeDoc (doc);
+ return NULL;
+ }
+
+ if ((value = ttml_get_xml_property (root_node, "cellResolution"))) {
+ gchar *ptr = value;
+ cellres_x = (guint) g_ascii_strtoull (ptr, &ptr, 10U);
+ cellres_y = (guint) g_ascii_strtoull (ptr, NULL, 10U);
+ g_free (value);
+ } else {
+ cellres_x = DEFAULT_CELLRES_X;
+ cellres_y = DEFAULT_CELLRES_Y;
+ }
+
+ GST_CAT_DEBUG (ttmlparse_debug, "cellres_x: %u cellres_y: %u", cellres_x,
+ cellres_y);
+
+ if ((value = ttml_get_xml_property (root_node, "space"))) {
+ if (g_strcmp0 (value, "preserve") == 0) {
+ GST_CAT_DEBUG (ttmlparse_debug, "Preserving whitespace...");
+ doc_whitespace_mode = TTML_WHITESPACE_MODE_PRESERVE;
+ }
+ g_free (value);
+ }
+
+ if (!(head_node = ttml_find_child (root_node, "head"))) {
+ GST_CAT_ERROR (ttmlparse_debug, "No <head> element found.");
+ xmlFreeDoc (doc);
+ return NULL;
+ }
+ ttml_parse_head (head_node, styles_table, regions_table);
+
+ if ((body_node = ttml_find_child (root_node, "body"))) {
+ GNode *body_tree;
+ GList *region_trees = NULL;
+ GList *scenes = NULL;
+
+ body_tree = ttml_parse_body (body_node);
+ GST_CAT_LOG (ttmlparse_debug, "body_tree tree contains %u nodes.",
+ g_node_n_nodes (body_tree, G_TRAVERSE_ALL));
+ GST_CAT_LOG (ttmlparse_debug, "body_tree tree height is %u",
+ g_node_max_height (body_tree));
+
+ ttml_inherit_whitespace_mode (body_tree, doc_whitespace_mode);
+ ttml_handle_whitespace (body_tree);
+ if (GST_CLOCK_TIME_IS_VALID (begin) && GST_CLOCK_TIME_IS_VALID (duration))
+ ttml_apply_time_window (body_tree, begin, begin + duration);
+ ttml_resolve_timings (body_tree);
+ ttml_resolve_regions (body_tree);
+ region_trees = ttml_split_body_by_region (body_tree, regions_table);
+ ttml_resolve_referenced_styles (region_trees, styles_table);
+ ttml_inherit_element_styles (region_trees);
+ ttml_assign_region_times (region_trees, begin, duration);
+ scenes = ttml_create_scenes (region_trees);
+ GST_CAT_LOG (ttmlparse_debug, "There are %u scenes in all.",
+ g_list_length (scenes));
+ ttml_attach_scene_metadata (scenes, cellres_x, cellres_y);
+ output_buffers = create_buffer_list (scenes);
+
+ g_list_free_full (scenes, (GDestroyNotify) ttml_delete_scene);
+ g_list_free_full (region_trees, (GDestroyNotify) ttml_delete_tree);
+ ttml_delete_tree (body_tree);
+ }
+
+ xmlFreeDoc (doc);
+ g_hash_table_destroy (styles_table);
+ g_hash_table_destroy (regions_table);
+
+ return output_buffers;
+}
diff --git a/ext/ttml/ttmlparse.h b/ext/ttml/ttmlparse.h
new file mode 100644
index 000000000..b5f21bf66
--- /dev/null
+++ b/ext/ttml/ttmlparse.h
@@ -0,0 +1,34 @@
+/* GStreamer TTML subtitle parser
+ * Copyright (C) <2015> British Broadcasting Corporation
+ * Authors:
+ * Chris Bass <dash@rd.bbc.co.uk>
+ * Peter Taylour <dash@rd.bbc.co.uk>
+ *
+ * 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., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifndef _TTML_PARSE_H_
+#define _TTML_PARSE_H_
+
+#include <gst/gst.h>
+
+G_BEGIN_DECLS
+
+GList *ttml_parse (const gchar * file, GstClockTime begin,
+ GstClockTime duration);
+
+G_END_DECLS
+#endif /* _TTML_PARSE_H_ */