| /* |
| * 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. |
| */ |
| |
| #include <string.h> |
| #include <stdarg.h> |
| #include <gst/gst.h> |
| |
| #include "gstcmmlparser.h" |
| #include "gstannodex.h" |
| #include "gstcmmlutils.h" |
| |
| GST_DEBUG_CATEGORY (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, xml_error->message); |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| /* convert an xmlNodePtr to a string |
| */ |
| 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, ...) |
| { |
| va_list varargs; |
| |
| va_start (varargs, msg); |
| gst_debug_log_valist (GST_CAT_DEFAULT, GST_LEVEL_WARNING, |
| "", "", 0, NULL, msg, varargs); |
| va_end (varargs); |
| } |
| |
| /* 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); |
| } |
| } |
| } |