blob: b76cf4a0bd9161a59b953fdabab2f08bea28985e [file] [log] [blame]
/*
* gstcmmlparser.c - GStreamer CMML document parser
* Copyright (C) 2005 Alessandro Decina
*
* Authors:
* Alessandro Decina <alessandro@nnva.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
/* FIXME 0.11: suppress warnings for deprecated API such as GValueArray
* with newer GLib versions (>= 2.31.0) */
#define GLIB_DISABLE_DEPRECATION_WARNINGS
#include <string.h>
#include <stdarg.h>
#include <gst/gst.h>
#include "gstcmmlparser.h"
#include "gstannodex.h"
#include "gstcmmlutils.h"
GST_DEBUG_CATEGORY_STATIC (cmmlparser);
#define GST_CAT_DEFAULT cmmlparser
static void gst_cmml_parser_generic_error (void *ctx, const char *msg, ...);
static xmlNodePtr gst_cmml_parser_new_node (GstCmmlParser * parser,
const gchar * name, ...);
static void
gst_cmml_parser_parse_start_element_ns (xmlParserCtxt * ctxt,
const xmlChar * name, const xmlChar * prefix, const xmlChar * URI,
int nb_preferences, const xmlChar ** namespaces,
int nb_attributes, int nb_defaulted, const xmlChar ** attributes);
static void gst_cmml_parser_parse_end_element_ns (xmlParserCtxt * ctxt,
const xmlChar * name, const xmlChar * prefix, const xmlChar * URI);
static void gst_cmml_parser_parse_processing_instruction (xmlParserCtxtPtr ctxt,
const xmlChar * target, const xmlChar * data);
static void gst_cmml_parser_meta_to_string (GstCmmlParser * parser,
xmlNodePtr parent, GValueArray * meta);
/* initialize the parser */
void
gst_cmml_parser_init (void)
{
GST_DEBUG_CATEGORY_INIT (cmmlparser, "cmmlparser", 0, "annodex CMML parser");
xmlGenericError = gst_cmml_parser_generic_error;
}
/* create a new CMML parser
*/
GstCmmlParser *
gst_cmml_parser_new (GstCmmlParserMode mode)
{
GstCmmlParser *parser = g_malloc (sizeof (GstCmmlParser));
parser->mode = mode;
parser->context = xmlCreatePushParserCtxt (NULL, NULL,
NULL, 0, "cmml-bitstream");
xmlCtxtUseOptions (parser->context, XML_PARSE_NONET | XML_PARSE_NOERROR);
parser->context->_private = parser;
parser->context->sax->startElementNs =
(startElementNsSAX2Func) gst_cmml_parser_parse_start_element_ns;
parser->context->sax->endElementNs =
(endElementNsSAX2Func) gst_cmml_parser_parse_end_element_ns;
parser->context->sax->processingInstruction = (processingInstructionSAXFunc)
gst_cmml_parser_parse_processing_instruction;
parser->preamble_callback = NULL;
parser->cmml_end_callback = NULL;
parser->stream_callback = NULL;
parser->head_callback = NULL;
parser->clip_callback = NULL;
parser->user_data = NULL;
return parser;
}
/* free a CMML parser instance
*/
void
gst_cmml_parser_free (GstCmmlParser * parser)
{
if (parser) {
xmlFreeDoc (parser->context->myDoc);
xmlFreeParserCtxt (parser->context);
g_free (parser);
}
}
/* parse an xml chunk
*
* returns false if the xml is invalid
*/
gboolean
gst_cmml_parser_parse_chunk (GstCmmlParser * parser,
const gchar * data, guint size, GError ** err)
{
gint xmlres;
xmlres = xmlParseChunk (parser->context, data, size, 0);
if (xmlres != XML_ERR_OK) {
xmlErrorPtr xml_error = xmlCtxtGetLastError (parser->context);
GST_DEBUG ("Error occurred decoding chunk %s", data);
g_set_error (err,
GST_LIBRARY_ERROR, GST_LIBRARY_ERROR_FAILED, "%s", xml_error->message);
return FALSE;
}
return TRUE;
}
/* convert an xmlNodePtr to a string
*/
static guchar *
gst_cmml_parser_node_to_string (GstCmmlParser * parser, xmlNodePtr node)
{
xmlBufferPtr xml_buffer;
xmlDocPtr doc;
guchar *str;
if (parser)
doc = parser->context->myDoc;
else
doc = NULL;
xml_buffer = xmlBufferCreate ();
xmlNodeDump (xml_buffer, doc, node, 0, 0);
str = xmlStrndup (xml_buffer->content, xml_buffer->use);
xmlBufferFree (xml_buffer);
return str;
}
guchar *
gst_cmml_parser_tag_stream_to_string (GstCmmlParser * parser,
GstCmmlTagStream * stream)
{
xmlNodePtr node;
xmlNodePtr import;
guchar *ret;
node = gst_cmml_parser_new_node (parser, "stream", NULL);
if (stream->timebase)
xmlSetProp (node, (xmlChar *) "timebase", stream->timebase);
if (stream->utc)
xmlSetProp (node, (xmlChar *) "utc", stream->utc);
if (stream->imports) {
gint i;
GValue *val;
for (i = 0; i < stream->imports->n_values; ++i) {
val = g_value_array_get_nth (stream->imports, i);
import = gst_cmml_parser_new_node (parser, "import",
"src", g_value_get_string (val), NULL);
xmlAddChild (node, import);
}
}
ret = gst_cmml_parser_node_to_string (parser, node);
xmlUnlinkNode (node);
xmlFreeNode (node);
return ret;
}
/* convert a GstCmmlTagHead to its string representation
*/
guchar *
gst_cmml_parser_tag_head_to_string (GstCmmlParser * parser,
GstCmmlTagHead * head)
{
xmlNodePtr node;
xmlNodePtr tmp;
guchar *ret;
node = gst_cmml_parser_new_node (parser, "head", NULL);
if (head->title) {
tmp = gst_cmml_parser_new_node (parser, "title", NULL);
xmlNodeSetContent (tmp, head->title);
xmlAddChild (node, tmp);
}
if (head->base) {
tmp = gst_cmml_parser_new_node (parser, "base", "uri", head->base, NULL);
xmlAddChild (node, tmp);
}
if (head->meta)
gst_cmml_parser_meta_to_string (parser, node, head->meta);
ret = gst_cmml_parser_node_to_string (parser, node);
xmlUnlinkNode (node);
xmlFreeNode (node);
return ret;
}
/* convert a GstCmmlTagClip to its string representation
*/
guchar *
gst_cmml_parser_tag_clip_to_string (GstCmmlParser * parser,
GstCmmlTagClip * clip)
{
xmlNodePtr node;
xmlNodePtr tmp;
guchar *ret;
node = gst_cmml_parser_new_node (parser, "clip",
"id", clip->id, "track", clip->track, NULL);
/* add the anchor element */
if (clip->anchor_href) {
tmp = gst_cmml_parser_new_node (parser, "a",
"href", clip->anchor_href, NULL);
if (clip->anchor_text)
xmlNodeSetContent (tmp, clip->anchor_text);
xmlAddChild (node, tmp);
}
/* add the img element */
if (clip->img_src) {
tmp = gst_cmml_parser_new_node (parser, "img",
"src", clip->img_src, "alt", clip->img_alt, NULL);
xmlAddChild (node, tmp);
}
/* add the desc element */
if (clip->desc_text) {
tmp = gst_cmml_parser_new_node (parser, "desc", NULL);
xmlNodeSetContent (tmp, clip->desc_text);
xmlAddChild (node, tmp);
}
/* add the meta elements */
if (clip->meta)
gst_cmml_parser_meta_to_string (parser, node, clip->meta);
if (parser->mode == GST_CMML_PARSER_DECODE) {
gchar *time_str;
time_str = gst_cmml_clock_time_to_npt (clip->start_time);
if (time_str == NULL)
goto fail;
xmlSetProp (node, (xmlChar *) "start", (xmlChar *) time_str);
g_free (time_str);
if (clip->end_time != GST_CLOCK_TIME_NONE) {
time_str = gst_cmml_clock_time_to_npt (clip->end_time);
if (time_str == NULL)
goto fail;
xmlSetProp (node, (xmlChar *) "end", (xmlChar *) time_str);
g_free (time_str);
}
}
ret = gst_cmml_parser_node_to_string (parser, node);
xmlUnlinkNode (node);
xmlFreeNode (node);
return ret;
fail:
xmlUnlinkNode (node);
xmlFreeNode (node);
return NULL;
}
guchar *
gst_cmml_parser_tag_object_to_string (GstCmmlParser * parser, GObject * tag)
{
guchar *tag_string = NULL;
GType tag_type = G_OBJECT_TYPE (tag);
if (tag_type == GST_TYPE_CMML_TAG_STREAM)
tag_string = gst_cmml_parser_tag_stream_to_string (parser,
GST_CMML_TAG_STREAM (tag));
else if (tag_type == GST_TYPE_CMML_TAG_HEAD)
tag_string = gst_cmml_parser_tag_head_to_string (parser,
GST_CMML_TAG_HEAD (tag));
else if (tag_type == GST_TYPE_CMML_TAG_CLIP)
tag_string = gst_cmml_parser_tag_clip_to_string (parser,
GST_CMML_TAG_CLIP (tag));
else
g_warning ("could not convert object to cmml");
return tag_string;
}
/*** private section ***/
/* create a new node
*
* helper to create a node and set its attributes
*/
static xmlNodePtr
gst_cmml_parser_new_node (GstCmmlParser * parser, const gchar * name, ...)
{
va_list args;
xmlNodePtr node;
xmlChar *prop_name, *prop_value;
node = xmlNewNode (NULL, (xmlChar *) name);
va_start (args, name);
prop_name = va_arg (args, xmlChar *);
while (prop_name != NULL) {
prop_value = va_arg (args, xmlChar *);
if (prop_value != NULL)
xmlSetProp (node, prop_name, prop_value);
prop_name = va_arg (args, xmlChar *);
}
va_end (args);
return node;
}
/* get the last node of the stream
*
* returns the last node at depth 1 (if any) or the root node
*/
static xmlNodePtr
gst_cmml_parser_get_last_element (GstCmmlParser * parser)
{
xmlNodePtr node;
node = xmlDocGetRootElement (parser->context->myDoc);
if (!node) {
g_warning ("no last cmml element");
return NULL;
}
if (node->children)
node = xmlGetLastChild (node);
return node;
}
static void
gst_cmml_parser_parse_preamble (GstCmmlParser * parser,
const guchar * attributes)
{
gchar *preamble;
gchar *element;
const gchar *version;
const gchar *encoding;
const gchar *standalone;
xmlDocPtr doc;
doc = parser->context->myDoc;
version = doc->version ? (gchar *) doc->version : "1.0";
encoding = doc->encoding ? (gchar *) doc->encoding : "UTF-8";
standalone = doc->standalone ? "yes" : "no";
preamble = g_strdup_printf ("<?xml version=\"%s\""
" encoding=\"%s\" standalone=\"%s\"?>\n"
"<!DOCTYPE cmml SYSTEM \"cmml.dtd\">\n", version, encoding, standalone);
if (attributes == NULL)
attributes = (guchar *) "";
if (parser->mode == GST_CMML_PARSER_ENCODE)
element = g_strdup_printf ("<?cmml %s?>", attributes);
else
element = g_strdup_printf ("<cmml %s>", attributes);
parser->preamble_callback (parser->user_data,
(guchar *) preamble, (guchar *) element);
g_free (preamble);
g_free (element);
}
/* parse the cmml stream tag */
static void
gst_cmml_parser_parse_stream (GstCmmlParser * parser, xmlNodePtr stream)
{
GstCmmlTagStream *stream_tag;
GValue str_val = { 0 };
xmlNodePtr walk;
guchar *timebase;
g_value_init (&str_val, G_TYPE_STRING);
/* read the timebase and utc attributes */
timebase = xmlGetProp (stream, (xmlChar *) "timebase");
if (timebase == NULL)
timebase = (guchar *) g_strdup ("0");
stream_tag = g_object_new (GST_TYPE_CMML_TAG_STREAM,
"timebase", timebase, NULL);
g_free (timebase);
stream_tag->utc = xmlGetProp (stream, (xmlChar *) "utc");
/* walk the children nodes */
for (walk = stream->children; walk; walk = walk->next) {
/* for every import tag add its src attribute to stream_tag->imports */
if (!xmlStrcmp (walk->name, (xmlChar *) "import")) {
g_value_take_string (&str_val,
(gchar *) xmlGetProp (walk, (xmlChar *) "src"));
if (stream_tag->imports == NULL)
stream_tag->imports = g_value_array_new (0);
g_value_array_append (stream_tag->imports, &str_val);
}
}
g_value_unset (&str_val);
parser->stream_callback (parser->user_data, stream_tag);
g_object_unref (stream_tag);
}
/* parse the cmml head tag */
static void
gst_cmml_parser_parse_head (GstCmmlParser * parser, xmlNodePtr head)
{
GstCmmlTagHead *head_tag;
xmlNodePtr walk;
GValue str_val = { 0 };
head_tag = g_object_new (GST_TYPE_CMML_TAG_HEAD, NULL);
g_value_init (&str_val, G_TYPE_STRING);
/* Parse the content of the node and setup the GST_TAG_CMML_HEAD tag.
* Create a GST_TAG_TITLE when we find the title element.
*/
for (walk = head->children; walk; walk = walk->next) {
if (!xmlStrcmp (walk->name, (xmlChar *) "title")) {
head_tag->title = xmlNodeGetContent (walk);
} else if (!xmlStrcmp (walk->name, (xmlChar *) "base")) {
head_tag->base = xmlGetProp (walk, (xmlChar *) "uri");
} else if (!xmlStrcmp (walk->name, (xmlChar *) "meta")) {
if (head_tag->meta == NULL)
head_tag->meta = g_value_array_new (0);
/* add a pair name, content to the meta value array */
g_value_take_string (&str_val,
(gchar *) xmlGetProp (walk, (xmlChar *) "name"));
g_value_array_append (head_tag->meta, &str_val);
g_value_take_string (&str_val,
(gchar *) xmlGetProp (walk, (xmlChar *) "content"));
g_value_array_append (head_tag->meta, &str_val);
}
}
g_value_unset (&str_val);
parser->head_callback (parser->user_data, head_tag);
g_object_unref (head_tag);
}
/* parse a cmml clip tag */
static void
gst_cmml_parser_parse_clip (GstCmmlParser * parser, xmlNodePtr clip)
{
GstCmmlTagClip *clip_tag;
GValue str_val = { 0 };
guchar *id, *track, *start, *end;
xmlNodePtr walk;
GstClockTime start_time = GST_CLOCK_TIME_NONE;
GstClockTime end_time = GST_CLOCK_TIME_NONE;
start = xmlGetProp (clip, (xmlChar *) "start");
if (parser->mode == GST_CMML_PARSER_ENCODE && start == NULL)
/* XXX: validate the document */
return;
id = xmlGetProp (clip, (xmlChar *) "id");
track = xmlGetProp (clip, (xmlChar *) "track");
end = xmlGetProp (clip, (xmlChar *) "end");
if (track == NULL)
track = (guchar *) g_strdup ("default");
if (start) {
if (!strncmp ((gchar *) start, "smpte", 5))
start_time = gst_cmml_clock_time_from_smpte ((gchar *) start);
else
start_time = gst_cmml_clock_time_from_npt ((gchar *) start);
}
if (end) {
if (!strncmp ((gchar *) end, "smpte", 5))
start_time = gst_cmml_clock_time_from_smpte ((gchar *) end);
else
end_time = gst_cmml_clock_time_from_npt ((gchar *) end);
}
clip_tag = g_object_new (GST_TYPE_CMML_TAG_CLIP, "id", id,
"track", track, "start-time", start_time, "end-time", end_time, NULL);
g_free (id);
g_free (track);
g_free (start);
g_free (end);
g_value_init (&str_val, G_TYPE_STRING);
/* parse the children */
for (walk = clip->children; walk; walk = walk->next) {
/* the clip is not empty */
clip_tag->empty = FALSE;
if (!xmlStrcmp (walk->name, (xmlChar *) "a")) {
clip_tag->anchor_href = xmlGetProp (walk, (xmlChar *) "href");
clip_tag->anchor_text = xmlNodeGetContent (walk);
} else if (!xmlStrcmp (walk->name, (xmlChar *) "img")) {
clip_tag->img_src = xmlGetProp (walk, (xmlChar *) "src");
clip_tag->img_alt = xmlGetProp (walk, (xmlChar *) "alt");
} else if (!xmlStrcmp (walk->name, (xmlChar *) "desc")) {
clip_tag->desc_text = xmlNodeGetContent (walk);
} else if (!xmlStrcmp (walk->name, (xmlChar *) "meta")) {
if (clip_tag->meta == NULL)
clip_tag->meta = g_value_array_new (0);
/* add a pair name, content to the meta value array */
g_value_take_string (&str_val,
(char *) xmlGetProp (walk, (xmlChar *) "name"));
g_value_array_append (clip_tag->meta, &str_val);
g_value_take_string (&str_val,
(char *) xmlGetProp (walk, (xmlChar *) "content"));
g_value_array_append (clip_tag->meta, &str_val);
}
}
g_value_unset (&str_val);
parser->clip_callback (parser->user_data, clip_tag);
g_object_unref (clip_tag);
}
void
gst_cmml_parser_meta_to_string (GstCmmlParser * parser,
xmlNodePtr parent, GValueArray * array)
{
gint i;
xmlNodePtr node;
GValue *name, *content;
for (i = 0; i < array->n_values - 1; i += 2) {
name = g_value_array_get_nth (array, i);
content = g_value_array_get_nth (array, i + 1);
node = gst_cmml_parser_new_node (parser, "meta",
"name", g_value_get_string (name),
"content", g_value_get_string (content), NULL);
xmlAddChild (parent, node);
}
}
static void
gst_cmml_parser_generic_error (void *ctx, const char *msg, ...)
{
#ifndef GST_DISABLE_GST_DEBUG
va_list varargs;
va_start (varargs, msg);
gst_debug_log_valist (GST_CAT_DEFAULT, GST_LEVEL_WARNING,
"", "", 0, NULL, msg, varargs);
va_end (varargs);
#endif /* GST_DISABLE_GST_DEBUG */
}
/* sax handler called when an element start tag is found
* this is used to parse the cmml start tag
*/
static void
gst_cmml_parser_parse_start_element_ns (xmlParserCtxt * ctxt,
const xmlChar * name, const xmlChar * prefix, const xmlChar * URI,
int nb_preferences, const xmlChar ** namespaces,
int nb_attributes, int nb_defaulted, const xmlChar ** attributes)
{
GstCmmlParser *parser = (GstCmmlParser *) ctxt->_private;
xmlSAX2StartElementNs (ctxt, name, prefix, URI, nb_preferences, namespaces,
nb_attributes, nb_defaulted, attributes);
if (parser->mode == GST_CMML_PARSER_ENCODE)
if (!xmlStrcmp (name, (xmlChar *) "cmml"))
if (parser->preamble_callback)
/* FIXME: parse attributes */
gst_cmml_parser_parse_preamble (parser, NULL);
}
/* sax processing instruction handler
* used to parse the cmml processing instruction
*/
static void
gst_cmml_parser_parse_processing_instruction (xmlParserCtxtPtr ctxt,
const xmlChar * target, const xmlChar * data)
{
GstCmmlParser *parser = (GstCmmlParser *) ctxt->_private;
xmlSAX2ProcessingInstruction (ctxt, target, data);
if (parser->mode == GST_CMML_PARSER_DECODE)
if (!xmlStrcmp (target, (xmlChar *) "cmml"))
if (parser->preamble_callback)
gst_cmml_parser_parse_preamble (parser, data);
}
/* sax handler called when an xml end tag is found
* used to parse the stream, head and clip nodes
*/
static void
gst_cmml_parser_parse_end_element_ns (xmlParserCtxt * ctxt,
const xmlChar * name, const xmlChar * prefix, const xmlChar * URI)
{
xmlNodePtr node;
GstCmmlParser *parser = (GstCmmlParser *) ctxt->_private;
xmlSAX2EndElementNs (ctxt, name, prefix, URI);
if (!xmlStrcmp (name, (xmlChar *) "clip")) {
if (parser->clip_callback) {
node = gst_cmml_parser_get_last_element (parser);
gst_cmml_parser_parse_clip (parser, node);
}
} else if (!xmlStrcmp (name, (xmlChar *) "cmml")) {
if (parser->cmml_end_callback)
parser->cmml_end_callback (parser->user_data);
} else if (!xmlStrcmp (name, (xmlChar *) "stream")) {
if (parser->stream_callback) {
node = gst_cmml_parser_get_last_element (parser);
gst_cmml_parser_parse_stream (parser, node);
}
} else if (!xmlStrcmp (name, (xmlChar *) "head")) {
if (parser->head_callback) {
node = gst_cmml_parser_get_last_element (parser);
gst_cmml_parser_parse_head (parser, node);
}
}
}