blob: d402c655aa8d3ad8429803ddd578a1c7fe6720e0 [file] [log] [blame]
/*
* gstcmmlenc.c - GStreamer CMML encoder
* 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.
*/
/**
* SECTION:element-cmmlenc
* @see_also: cmmldec, oggmux
*
* Cmmlenc encodes a CMML document into a CMML stream. <ulink
* url="http://www.annodex.net/TR/draft-pfeiffer-cmml-02.html">CMML</ulink> is
* an XML markup language for time-continuous data maintained by the <ulink
* url="http:/www.annodex.org/">Annodex Foundation</ulink>.
*
* <refsect2>
* <title>Example pipeline</title>
* |[
* gst-launch -v filesrc location=annotations.cmml ! cmmlenc ! oggmux name=mux ! filesink location=annotated.ogg
* ]|
* </refsect2>
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <string.h>
#include "gstcmmlenc.h"
#include "gstannodex.h"
GST_DEBUG_CATEGORY_STATIC (cmmlenc);
#define GST_CAT_DEFAULT cmmlenc
#define CMML_IDENT_HEADER_SIZE 29
enum
{
ARG_0,
GST_CMML_ENC_GRANULERATE_N,
GST_CMML_ENC_GRANULERATE_D,
GST_CMML_ENC_GRANULESHIFT
};
enum
{
LAST_SIGNAL
};
static GstStaticPadTemplate gst_cmml_enc_src_factory =
GST_STATIC_PAD_TEMPLATE ("src",
GST_PAD_SRC,
GST_PAD_ALWAYS,
GST_STATIC_CAPS ("text/x-cmml, encoded = (boolean) true")
);
static GstStaticPadTemplate gst_cmml_enc_sink_factory =
GST_STATIC_PAD_TEMPLATE ("sink",
GST_PAD_SINK,
GST_PAD_ALWAYS,
GST_STATIC_CAPS ("text/x-cmml, encoded = (boolean) false")
);
#define gst_cmml_enc_parent_class parent_class
G_DEFINE_TYPE (GstCmmlEnc, gst_cmml_enc, GST_TYPE_ELEMENT);
static void gst_cmml_enc_get_property (GObject * object, guint property_id,
GValue * value, GParamSpec * pspec);
static void gst_cmml_enc_set_property (GObject * object, guint property_id,
const GValue * value, GParamSpec * pspec);
static gboolean gst_cmml_enc_sink_event (GstPad * pad, GstObject * parent,
GstEvent * event);
static GstStateChangeReturn gst_cmml_enc_change_state (GstElement * element,
GstStateChange transition);
static GstFlowReturn gst_cmml_enc_chain (GstPad * pad, GstObject * parent,
GstBuffer * buffer);
static void gst_cmml_enc_parse_preamble (GstCmmlEnc * enc, guchar * preamble,
guchar * processing_instruction);
static void gst_cmml_enc_parse_end_tag (GstCmmlEnc * enc);
static void gst_cmml_enc_parse_tag_head (GstCmmlEnc * enc,
GstCmmlTagHead * head);
static void gst_cmml_enc_parse_tag_clip (GstCmmlEnc * enc,
GstCmmlTagClip * tag);
static GstFlowReturn gst_cmml_enc_new_buffer (GstCmmlEnc * enc,
guchar * data, gint size, GstBuffer ** buffer);
static GstFlowReturn gst_cmml_enc_push_clip (GstCmmlEnc * enc,
GstCmmlTagClip * clip, GstClockTime prev_clip_time);
static GstFlowReturn gst_cmml_enc_push (GstCmmlEnc * enc, GstBuffer * buffer);
static void gst_cmml_enc_finalize (GObject * object);
static void
gst_cmml_enc_class_init (GstCmmlEncClass * enc_class)
{
GObjectClass *klass = G_OBJECT_CLASS (enc_class);
GstElementClass *element_class = GST_ELEMENT_CLASS (enc_class);
klass->get_property = gst_cmml_enc_get_property;
klass->set_property = gst_cmml_enc_set_property;
klass->finalize = gst_cmml_enc_finalize;
g_object_class_install_property (klass, GST_CMML_ENC_GRANULERATE_N,
g_param_spec_int64 ("granule-rate-numerator",
"Granulerate numerator",
"Granulerate numerator",
0, G_MAXINT64, 1000,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (klass, GST_CMML_ENC_GRANULERATE_D,
g_param_spec_int64 ("granule-rate-denominator",
"Granulerate denominator",
"Granulerate denominator",
0, G_MAXINT64, 1,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (klass, GST_CMML_ENC_GRANULESHIFT,
g_param_spec_uchar ("granule-shift",
"Granuleshift",
"The number of lower bits to use for partitioning a granule position",
0, 64, 32,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
element_class->change_state = gst_cmml_enc_change_state;
gst_element_class_add_pad_template (element_class,
gst_static_pad_template_get (&gst_cmml_enc_sink_factory));
gst_element_class_add_pad_template (element_class,
gst_static_pad_template_get (&gst_cmml_enc_src_factory));
gst_element_class_set_static_metadata (element_class, "CMML streams encoder",
"Codec/Encoder",
"Encodes CMML streams", "Alessandro Decina <alessandro@nnva.org>");
}
static void
gst_cmml_enc_init (GstCmmlEnc * enc)
{
enc->sinkpad =
gst_pad_new_from_static_template (&gst_cmml_enc_sink_factory, "sink");
gst_pad_set_chain_function (enc->sinkpad, gst_cmml_enc_chain);
gst_pad_set_event_function (enc->sinkpad, gst_cmml_enc_sink_event);
gst_element_add_pad (GST_ELEMENT (enc), enc->sinkpad);
enc->srcpad =
gst_pad_new_from_static_template (&gst_cmml_enc_src_factory, "src");
gst_element_add_pad (GST_ELEMENT (enc), enc->srcpad);
enc->major = 3;
enc->minor = 0;
}
static void
gst_cmml_enc_set_property (GObject * object, guint property_id,
const GValue * value, GParamSpec * pspec)
{
GstCmmlEnc *enc = GST_CMML_ENC (object);
switch (property_id) {
case GST_CMML_ENC_GRANULERATE_N:
/* XXX: may need to flush clips */
enc->granulerate_n = g_value_get_int64 (value);
break;
case GST_CMML_ENC_GRANULERATE_D:
enc->granulerate_d = g_value_get_int64 (value);
break;
case GST_CMML_ENC_GRANULESHIFT:
enc->granuleshift = g_value_get_uchar (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
}
static void
gst_cmml_enc_get_property (GObject * object, guint property_id,
GValue * value, GParamSpec * pspec)
{
GstCmmlEnc *enc = GST_CMML_ENC (object);
switch (property_id) {
case GST_CMML_ENC_GRANULERATE_N:
g_value_set_int64 (value, enc->granulerate_n);
break;
case GST_CMML_ENC_GRANULERATE_D:
g_value_set_int64 (value, enc->granulerate_d);
break;
case GST_CMML_ENC_GRANULESHIFT:
g_value_set_uchar (value, enc->granuleshift);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
}
static void
gst_cmml_enc_finalize (GObject * object)
{
GstCmmlEnc *enc = GST_CMML_ENC (object);
if (enc->tracks) {
gst_cmml_track_list_destroy (enc->tracks);
enc->tracks = NULL;
}
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static GstStateChangeReturn
gst_cmml_enc_change_state (GstElement * element, GstStateChange transition)
{
GstCmmlEnc *enc = GST_CMML_ENC (element);
GstStateChangeReturn res;
switch (transition) {
case GST_STATE_CHANGE_READY_TO_PAUSED:
enc->parser = gst_cmml_parser_new (GST_CMML_PARSER_ENCODE);
enc->parser->user_data = enc;
enc->parser->preamble_callback =
(GstCmmlParserPreambleCallback) gst_cmml_enc_parse_preamble;
enc->parser->head_callback =
(GstCmmlParserHeadCallback) gst_cmml_enc_parse_tag_head;
enc->parser->clip_callback =
(GstCmmlParserClipCallback) gst_cmml_enc_parse_tag_clip;
enc->parser->cmml_end_callback =
(GstCmmlParserCmmlEndCallback) gst_cmml_enc_parse_end_tag;
enc->tracks = gst_cmml_track_list_new ();
enc->sent_headers = FALSE;
enc->sent_eos = FALSE;
enc->flow_return = GST_FLOW_OK;
break;
default:
break;
}
res = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
switch (transition) {
case GST_STATE_CHANGE_PAUSED_TO_READY:
{
gst_cmml_track_list_destroy (enc->tracks);
enc->tracks = NULL;
g_free (enc->preamble);
enc->preamble = NULL;
gst_cmml_parser_free (enc->parser);
break;
}
default:
break;
}
return res;
}
static gboolean
gst_cmml_enc_sink_event (GstPad * pad, GstObject * parent, GstEvent * event)
{
GstCmmlEnc *enc = GST_CMML_ENC (parent);
switch (GST_EVENT_TYPE (event)) {
case GST_EVENT_EOS:
{
if (!enc->sent_eos)
gst_cmml_enc_parse_end_tag (enc);
break;
}
default:
break;
}
return gst_pad_event_default (pad, parent, event);
}
static GstFlowReturn
gst_cmml_enc_new_buffer (GstCmmlEnc * enc,
guchar * data, gint size, GstBuffer ** buffer)
{
GstFlowReturn res;
*buffer = gst_buffer_new_allocate (NULL, size, NULL);
if (*buffer != NULL) {
if (data)
gst_buffer_fill (*buffer, 0, data, size);
res = GST_FLOW_OK;
} else {
GST_WARNING_OBJECT (enc, "could not allocate buffer");
res = GST_FLOW_ERROR;
}
return res;
}
static GstCaps *
gst_cmml_enc_set_header_on_caps (GstCmmlEnc * enc, GstCaps * caps,
GstBuffer * ident, GstBuffer * preamble, GstBuffer * head)
{
GValue array = { 0 };
GValue value = { 0 };
GstStructure *structure;
GstBuffer *buffer;
caps = gst_caps_make_writable (caps);
structure = gst_caps_get_structure (caps, 0);
g_value_init (&array, GST_TYPE_ARRAY);
g_value_init (&value, GST_TYPE_BUFFER);
/* Make copies of header buffers to avoid circular references */
buffer = gst_buffer_copy (ident);
gst_value_set_buffer (&value, buffer);
gst_value_array_append_value (&array, &value);
gst_buffer_unref (buffer);
buffer = gst_buffer_copy (preamble);
gst_value_set_buffer (&value, buffer);
gst_value_array_append_value (&array, &value);
gst_buffer_unref (buffer);
buffer = gst_buffer_copy (head);
gst_value_set_buffer (&value, buffer);
gst_value_array_append_value (&array, &value);
gst_buffer_unref (buffer);
GST_BUFFER_FLAG_SET (ident, GST_BUFFER_FLAG_HEADER);
GST_BUFFER_FLAG_SET (preamble, GST_BUFFER_FLAG_HEADER);
GST_BUFFER_FLAG_SET (head, GST_BUFFER_FLAG_HEADER);
gst_structure_set_value (structure, "streamheader", &array);
g_value_unset (&value);
g_value_unset (&array);
return caps;
}
/* create a CMML ident header buffer
*/
static GstFlowReturn
gst_cmml_enc_new_ident_header (GstCmmlEnc * enc, GstBuffer ** buffer)
{
guint8 ident_header[CMML_IDENT_HEADER_SIZE];
guint8 *wptr = ident_header;
memcpy (wptr, "CMML\0\0\0\0", 8);
wptr += 8;
GST_WRITE_UINT16_LE (wptr, enc->major);
wptr += 2;
GST_WRITE_UINT16_LE (wptr, enc->minor);
wptr += 2;
GST_WRITE_UINT64_LE (wptr, enc->granulerate_n);
wptr += 8;
GST_WRITE_UINT64_LE (wptr, enc->granulerate_d);
wptr += 8;
*wptr = enc->granuleshift;
return gst_cmml_enc_new_buffer (enc,
(guchar *) & ident_header, CMML_IDENT_HEADER_SIZE, buffer);
}
/* parse the CMML preamble */
static void
gst_cmml_enc_parse_preamble (GstCmmlEnc * enc,
guchar * preamble, guchar * processing_instruction)
{
GST_INFO_OBJECT (enc, "parsing preamble");
/* save the preamble: it will be pushed when the head tag is found */
enc->preamble = (guchar *) g_strconcat ((gchar *) preamble,
(gchar *) processing_instruction, NULL);
}
/* parse the CMML end tag */
static void
gst_cmml_enc_parse_end_tag (GstCmmlEnc * enc)
{
GstBuffer *buffer;
GST_INFO_OBJECT (enc, "parsing end tag");
/* push an empty buffer to signal EOS */
enc->flow_return = gst_cmml_enc_new_buffer (enc, NULL, 0, &buffer);
if (enc->flow_return == GST_FLOW_OK) {
/* set granulepos 0 on EOS */
GST_BUFFER_OFFSET_END (buffer) = 0;
enc->flow_return = gst_cmml_enc_push (enc, buffer);
enc->sent_eos = TRUE;
}
return;
}
/* encode the CMML head tag and push the CMML headers
*/
static void
gst_cmml_enc_parse_tag_head (GstCmmlEnc * enc, GstCmmlTagHead * head)
{
GList *headers = NULL;
GList *walk;
guchar *head_string;
GstCaps *caps;
GstBuffer *ident_buf, *preamble_buf, *head_buf;
GstBuffer *buffer;
if (enc->preamble == NULL)
goto flow_unexpected;
GST_INFO_OBJECT (enc, "parsing head tag");
enc->flow_return = gst_cmml_enc_new_ident_header (enc, &ident_buf);
if (enc->flow_return != GST_FLOW_OK)
goto alloc_error;
headers = g_list_append (headers, ident_buf);
enc->flow_return = gst_cmml_enc_new_buffer (enc,
enc->preamble, strlen ((gchar *) enc->preamble), &preamble_buf);
if (enc->flow_return != GST_FLOW_OK)
goto alloc_error;
headers = g_list_append (headers, preamble_buf);
head_string = gst_cmml_parser_tag_head_to_string (enc->parser, head);
enc->flow_return = gst_cmml_enc_new_buffer (enc,
head_string, strlen ((gchar *) head_string), &head_buf);
g_free (head_string);
if (enc->flow_return != GST_FLOW_OK)
goto alloc_error;
headers = g_list_append (headers, head_buf);
caps = gst_pad_query_caps (enc->srcpad, NULL);
caps = gst_cmml_enc_set_header_on_caps (enc, caps,
ident_buf, preamble_buf, head_buf);
gst_pad_set_caps (enc->srcpad, caps);
gst_caps_unref (caps);
while (headers) {
buffer = GST_BUFFER (headers->data);
/* set granulepos 0 on headers */
GST_BUFFER_OFFSET_END (buffer) = 0;
enc->flow_return = gst_cmml_enc_push (enc, buffer);
headers = g_list_delete_link (headers, headers);
if (enc->flow_return != GST_FLOW_OK)
goto push_error;
}
enc->sent_headers = TRUE;
return;
flow_unexpected:
GST_ELEMENT_ERROR (enc, STREAM, ENCODE,
(NULL), ("got head tag before preamble"));
enc->flow_return = GST_FLOW_ERROR;
return;
push_error:
/* fallthrough */
alloc_error:
for (walk = headers; walk; walk = walk->next)
gst_buffer_unref (GST_BUFFER (walk->data));
g_list_free (headers);
return;
}
/* encode a CMML clip tag
* remove the start and end attributes (GstCmmlParser does this itself) and
* push the tag with the timestamp of its start attribute. If the tag has the
* end attribute, create a new empty clip and encode it.
*/
static void
gst_cmml_enc_parse_tag_clip (GstCmmlEnc * enc, GstCmmlTagClip * clip)
{
GstCmmlTagClip *prev_clip;
GstClockTime prev_clip_time = GST_CLOCK_TIME_NONE;
/* this can happen if there's a programming error (eg user forgets to set
* the start-time property) or if one of the gst_cmml_clock_time_from_*
* overflows in GstCmmlParser */
if (clip->start_time == GST_CLOCK_TIME_NONE) {
GST_ELEMENT_ERROR (enc, STREAM, ENCODE,
(NULL), ("invalid start time for clip (%s)", clip->id));
enc->flow_return = GST_FLOW_ERROR;
return;
}
/* get the previous clip's start time to encode the current granulepos */
prev_clip = gst_cmml_track_list_get_track_last_clip (enc->tracks,
(gchar *) clip->track);
if (prev_clip) {
prev_clip_time = prev_clip->start_time;
if (prev_clip_time > clip->start_time) {
GST_ELEMENT_ERROR (enc, STREAM, ENCODE,
(NULL), ("previous clip start time > current clip (%s) start time",
clip->id));
enc->flow_return = GST_FLOW_ERROR;
return;
}
/* we don't need the prev clip anymore */
gst_cmml_track_list_del_clip (enc->tracks, prev_clip);
}
/* add the current clip to the tracklist */
gst_cmml_track_list_add_clip (enc->tracks, clip);
enc->flow_return = gst_cmml_enc_push_clip (enc, clip, prev_clip_time);
}
static GstFlowReturn
gst_cmml_enc_push_clip (GstCmmlEnc * enc, GstCmmlTagClip * clip,
GstClockTime prev_clip_time)
{
GstFlowReturn res;
GstBuffer *buffer;
gchar *clip_string;
gint64 granulepos;
/* encode the clip */
clip_string =
(gchar *) gst_cmml_parser_tag_clip_to_string (enc->parser, clip);
res = gst_cmml_enc_new_buffer (enc,
(guchar *) clip_string, strlen (clip_string), &buffer);
g_free (clip_string);
if (res != GST_FLOW_OK)
goto done;
GST_INFO_OBJECT (enc, "encoding clip"
"(start-time: %" GST_TIME_FORMAT " end-time: %" GST_TIME_FORMAT,
GST_TIME_ARGS (clip->start_time), GST_TIME_ARGS (clip->end_time));
/* set the granulepos */
granulepos = gst_cmml_clock_time_to_granule (prev_clip_time, clip->start_time,
enc->granulerate_n, enc->granulerate_d, enc->granuleshift);
if (granulepos == -1) {
gst_buffer_unref (buffer);
goto granule_overflow;
}
GST_BUFFER_OFFSET (buffer) = clip->start_time;
GST_BUFFER_OFFSET_END (buffer) = granulepos;
GST_BUFFER_TIMESTAMP (buffer) = clip->start_time;
res = gst_cmml_enc_push (enc, buffer);
if (res != GST_FLOW_OK)
goto done;
if (clip->end_time != GST_CLOCK_TIME_NONE) {
/* create a new empty clip for the same cmml track starting at end_time
*/
GObject *end_clip = g_object_new (GST_TYPE_CMML_TAG_CLIP,
"start-time", clip->end_time, "track", clip->track, NULL);
/* encode the empty end clip */
gst_cmml_enc_push_clip (enc, GST_CMML_TAG_CLIP (end_clip),
clip->start_time);
g_object_unref (end_clip);
}
done:
return res;
granule_overflow:
GST_ELEMENT_ERROR (enc, STREAM, ENCODE, (NULL), ("granulepos overflow"));
return GST_FLOW_ERROR;
}
static GstFlowReturn
gst_cmml_enc_push (GstCmmlEnc * enc, GstBuffer * buffer)
{
GstFlowReturn res;
res = gst_pad_push (enc->srcpad, buffer);
if (res != GST_FLOW_OK)
GST_WARNING_OBJECT (enc, "push returned: %s", gst_flow_get_name (res));
return res;
}
static GstFlowReturn
gst_cmml_enc_chain (GstPad * pad, GstObject * parent, GstBuffer * buffer)
{
GError *err = NULL;
GstCmmlEnc *enc = GST_CMML_ENC (parent);
GstMapInfo map;
/* the CMML handlers registered with enc->parser will override this when
* encoding/pushing the buffers downstream
*/
enc->flow_return = GST_FLOW_OK;
gst_buffer_map (buffer, &map, GST_MAP_READ);
if (!gst_cmml_parser_parse_chunk (enc->parser, (gchar *) map.data, map.size,
&err)) {
GST_ELEMENT_ERROR (enc, STREAM, ENCODE, (NULL), ("%s", err->message));
g_error_free (err);
enc->flow_return = GST_FLOW_ERROR;
}
gst_buffer_unmap (buffer, &map);
gst_buffer_unref (buffer);
return enc->flow_return;
}
gboolean
gst_cmml_enc_plugin_init (GstPlugin * plugin)
{
if (!gst_element_register (plugin, "cmmlenc", GST_RANK_NONE,
GST_TYPE_CMML_ENC))
return FALSE;
GST_DEBUG_CATEGORY_INIT (cmmlenc, "cmmlenc", 0,
"annodex cmml decoding element");
return TRUE;
}