gstreamer/ext/ogg/gstoggparse.c
Vincent Penquerc'h b7bb1e5633 ogg: do not use 32 bit modifiers to print serial numbers
If ints are 64 bits, 32 bits should get promoted in varargs anyway,
and we don't care about 16 bit ints.
This makes the code a lot more readable, and still gets us nice
hexadecimal 32 bit serialnos.

https://bugzilla.gnome.org/show_bug.cgi?id=656775
2011-08-18 20:18:53 +01:00

758 lines
23 KiB
C

/* GStreamer
* Copyright (C) 2005 Michael Smith <msmith@fluendo.com>
*
* gstoggparse.c: ogg stream parser
*
* 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.
*/
/* This ogg parser is essentially a subset of the ogg demuxer - rather than
* fully demuxing into packets, we only parse out the pages, create one
* GstBuffer per page, set all the appropriate flags on those pages, set caps
* appropriately (particularly the 'streamheader' which gives all the header
* pages required for initialing decode).
*
* It's dramatically simpler than the full demuxer as it does not support
* seeking.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <gst/gst.h>
#include <ogg/ogg.h>
#include <string.h>
#include "gstogg.h"
#include "gstoggstream.h"
GST_DEBUG_CATEGORY_STATIC (gst_ogg_parse_debug);
#define GST_CAT_DEFAULT gst_ogg_parse_debug
#define GST_TYPE_OGG_PARSE (gst_ogg_parse_get_type())
#define GST_OGG_PARSE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_OGG_PARSE, GstOggParse))
#define GST_OGG_PARSE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_OGG_PARSE, GstOggParse))
#define GST_IS_OGG_PARSE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_OGG_PARSE))
#define GST_IS_OGG_PARSE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_OGG_PARSE))
static GType gst_ogg_parse_get_type (void);
typedef struct _GstOggParse GstOggParse;
typedef struct _GstOggParseClass GstOggParseClass;
struct _GstOggParse
{
GstElement element;
GstPad *sinkpad; /* Sink pad we're reading data from */
GstPad *srcpad; /* Source pad we're writing to */
GSList *oggstreams; /* list of GstOggStreams for known streams */
gint64 offset; /* Current stream offset */
gboolean in_headers; /* Set if we're reading headers for streams */
gboolean last_page_not_bos; /* Set if we've seen a non-BOS page */
ogg_sync_state sync; /* Ogg page synchronisation */
GstCaps *caps; /* Our src caps */
GstOggStream *video_stream; /* Stream used to construct delta_unit flags */
};
struct _GstOggParseClass
{
GstElementClass parent_class;
};
static void gst_ogg_parse_base_init (gpointer g_class);
static void gst_ogg_parse_class_init (GstOggParseClass * klass);
static void gst_ogg_parse_init (GstOggParse * ogg);
static GstElementClass *parent_class = NULL;
static GType
gst_ogg_parse_get_type (void)
{
static GType ogg_parse_type = 0;
if (!ogg_parse_type) {
static const GTypeInfo ogg_parse_info = {
sizeof (GstOggParseClass),
gst_ogg_parse_base_init,
NULL,
(GClassInitFunc) gst_ogg_parse_class_init,
NULL,
NULL,
sizeof (GstOggParse),
0,
(GInstanceInitFunc) gst_ogg_parse_init,
};
ogg_parse_type = g_type_register_static (GST_TYPE_ELEMENT, "GstOggParse",
&ogg_parse_info, 0);
}
return ogg_parse_type;
}
static void
free_stream (GstOggStream * stream)
{
g_list_foreach (stream->headers, (GFunc) gst_mini_object_unref, NULL);
g_list_foreach (stream->unknown_pages, (GFunc) gst_mini_object_unref, NULL);
g_list_foreach (stream->stored_buffers, (GFunc) gst_mini_object_unref, NULL);
g_free (stream);
}
static void
gst_ogg_parse_delete_all_streams (GstOggParse * ogg)
{
g_slist_foreach (ogg->oggstreams, (GFunc) free_stream, NULL);
g_slist_free (ogg->oggstreams);
ogg->oggstreams = NULL;
}
static GstOggStream *
gst_ogg_parse_new_stream (GstOggParse * parser, ogg_page * page)
{
GstOggStream *stream;
ogg_packet packet;
int ret;
guint32 serialno;
serialno = ogg_page_serialno (page);
GST_DEBUG_OBJECT (parser, "creating new stream %08x", serialno);
stream = g_new0 (GstOggStream, 1);
stream->serialno = serialno;
stream->in_headers = 1;
if (ogg_stream_init (&stream->stream, serialno) != 0) {
GST_ERROR ("Could not initialize ogg_stream struct for serial %08x.",
serialno);
return NULL;
}
/* FIXME check return */
ogg_stream_pagein (&stream->stream, page);
/* FIXME check return */
ret = ogg_stream_packetout (&stream->stream, &packet);
if (ret == 1) {
gst_ogg_stream_setup_map (stream, &packet);
if (stream->is_video) {
parser->video_stream = stream;
}
}
parser->oggstreams = g_slist_append (parser->oggstreams, stream);
return stream;
}
static GstOggStream *
gst_ogg_parse_find_stream (GstOggParse * parser, guint32 serialno)
{
GSList *l;
for (l = parser->oggstreams; l != NULL; l = l->next) {
GstOggStream *stream = (GstOggStream *) l->data;
if (stream->serialno == serialno)
return stream;
}
return NULL;
}
/* signals and args */
enum
{
/* FILL ME */
LAST_SIGNAL
};
enum
{
ARG_0
/* FILL ME */
};
static GstStaticPadTemplate ogg_parse_src_template_factory =
GST_STATIC_PAD_TEMPLATE ("src",
GST_PAD_SRC,
GST_PAD_ALWAYS,
GST_STATIC_CAPS ("application/ogg")
);
static GstStaticPadTemplate ogg_parse_sink_template_factory =
GST_STATIC_PAD_TEMPLATE ("sink",
GST_PAD_SINK,
GST_PAD_ALWAYS,
GST_STATIC_CAPS ("application/ogg")
);
static void gst_ogg_parse_dispose (GObject * object);
static GstStateChangeReturn gst_ogg_parse_change_state (GstElement * element,
GstStateChange transition);
static GstFlowReturn gst_ogg_parse_chain (GstPad * pad, GstBuffer * buffer);
static void
gst_ogg_parse_base_init (gpointer g_class)
{
GstElementClass *element_class = GST_ELEMENT_CLASS (g_class);
gst_element_class_set_details_simple (element_class,
"Ogg parser", "Codec/Parser",
"parse ogg streams into pages (info about ogg: http://xiph.org)",
"Michael Smith <msmith@fluendo.com>");
gst_element_class_add_pad_template (element_class,
gst_static_pad_template_get (&ogg_parse_sink_template_factory));
gst_element_class_add_pad_template (element_class,
gst_static_pad_template_get (&ogg_parse_src_template_factory));
}
static void
gst_ogg_parse_class_init (GstOggParseClass * klass)
{
GstElementClass *gstelement_class = GST_ELEMENT_CLASS (klass);
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
parent_class = g_type_class_peek_parent (klass);
gstelement_class->change_state = gst_ogg_parse_change_state;
gobject_class->dispose = gst_ogg_parse_dispose;
}
static void
gst_ogg_parse_init (GstOggParse * ogg)
{
/* create the sink and source pads */
ogg->sinkpad =
gst_pad_new_from_static_template (&ogg_parse_sink_template_factory,
"sink");
ogg->srcpad =
gst_pad_new_from_static_template (&ogg_parse_src_template_factory, "src");
/* TODO: Are there any events we must handle? */
/* gst_pad_set_event_function (ogg->sinkpad, gst_ogg_parse_handle_event); */
gst_pad_set_chain_function (ogg->sinkpad, gst_ogg_parse_chain);
gst_element_add_pad (GST_ELEMENT (ogg), ogg->sinkpad);
gst_element_add_pad (GST_ELEMENT (ogg), ogg->srcpad);
ogg->oggstreams = NULL;
}
static void
gst_ogg_parse_dispose (GObject * object)
{
GstOggParse *ogg = GST_OGG_PARSE (object);
GST_LOG_OBJECT (ogg, "Disposing of object %p", ogg);
ogg_sync_clear (&ogg->sync);
gst_ogg_parse_delete_all_streams (ogg);
if (ogg->caps) {
gst_caps_unref (ogg->caps);
ogg->caps = NULL;
}
if (G_OBJECT_CLASS (parent_class)->dispose)
G_OBJECT_CLASS (parent_class)->dispose (object);
}
/* submit the given buffer to the ogg sync */
static GstFlowReturn
gst_ogg_parse_submit_buffer (GstOggParse * ogg, GstBuffer * buffer)
{
guint size;
guint8 *data;
gchar *oggbuffer;
GstFlowReturn ret = GST_FLOW_OK;
size = GST_BUFFER_SIZE (buffer);
data = GST_BUFFER_DATA (buffer);
GST_DEBUG_OBJECT (ogg, "submitting %u bytes", size);
if (G_UNLIKELY (size == 0))
goto done;
oggbuffer = ogg_sync_buffer (&ogg->sync, size);
if (G_UNLIKELY (oggbuffer == NULL)) {
GST_ELEMENT_ERROR (ogg, STREAM, DECODE,
(NULL), ("failed to get ogg sync buffer"));
ret = GST_FLOW_ERROR;
goto done;
}
memcpy (oggbuffer, data, size);
if (G_UNLIKELY (ogg_sync_wrote (&ogg->sync, size) < 0)) {
GST_ELEMENT_ERROR (ogg, STREAM, DECODE,
(NULL), ("failed to write %d bytes to the sync buffer", size));
ret = GST_FLOW_ERROR;
}
done:
gst_buffer_unref (buffer);
return ret;
}
static void
gst_ogg_parse_append_header (GValue * array, GstBuffer * buf)
{
GValue value = { 0 };
/* We require a copy to avoid circular refcounts */
GstBuffer *buffer = gst_buffer_copy (buf);
GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_IN_CAPS);
g_value_init (&value, GST_TYPE_BUFFER);
gst_value_set_buffer (&value, buffer);
gst_value_array_append_value (array, &value);
g_value_unset (&value);
}
typedef enum
{
PAGE_HEADER, /* Header page */
PAGE_DATA, /* Data page */
PAGE_PENDING, /* We don't know yet, we'll have to see some future pages */
} page_type;
static page_type
gst_ogg_parse_is_header (GstOggParse * ogg, GstOggStream * stream,
ogg_page * page)
{
ogg_int64_t gpos = ogg_page_granulepos (page);
if (gpos < 0)
return PAGE_PENDING;
/* This is good enough for now, but technically requires codec-specific
* behaviour to be perfect. This is where we need the mooted library for
* this stuff, which nobody has written.
*/
if (gpos > 0)
return PAGE_DATA;
else
return PAGE_HEADER;
}
static GstBuffer *
gst_ogg_parse_buffer_from_page (ogg_page * page,
guint64 offset, GstClockTime timestamp)
{
int size = page->header_len + page->body_len;
GstBuffer *buf = gst_buffer_new_and_alloc (size);
memcpy (GST_BUFFER_DATA (buf), page->header, page->header_len);
memcpy (GST_BUFFER_DATA (buf) + page->header_len, page->body, page->body_len);
GST_BUFFER_TIMESTAMP (buf) = timestamp;
GST_BUFFER_OFFSET (buf) = offset;
GST_BUFFER_OFFSET_END (buf) = offset + size;
return buf;
}
/* Reads in buffers, parses them, reframes into one-buffer-per-ogg-page, submits
* pages to output pad.
*/
static GstFlowReturn
gst_ogg_parse_chain (GstPad * pad, GstBuffer * buffer)
{
GstOggParse *ogg;
GstFlowReturn result = GST_FLOW_OK;
gint ret = -1;
guint32 serialno;
GstBuffer *pagebuffer;
GstClockTime buffertimestamp = GST_BUFFER_TIMESTAMP (buffer);
ogg = GST_OGG_PARSE (GST_OBJECT_PARENT (pad));
GST_LOG_OBJECT (ogg, "Chain function received buffer of size %d",
GST_BUFFER_SIZE (buffer));
gst_ogg_parse_submit_buffer (ogg, buffer);
while (ret != 0 && result == GST_FLOW_OK) {
ogg_page page;
/* We use ogg_sync_pageseek() rather than ogg_sync_pageout() so that we can
* track how many bytes the ogg layer discarded (in the case of sync errors,
* etc.); this allows us to accurately track the current stream offset
*/
ret = ogg_sync_pageseek (&ogg->sync, &page);
if (ret == 0) {
/* need more data, that's fine... */
break;
} else if (ret < 0) {
/* discontinuity; track how many bytes we skipped (-ret) */
ogg->offset -= ret;
} else {
gint64 granule = ogg_page_granulepos (&page);
#ifndef GST_DISABLE_GST_DEBUG
int bos = ogg_page_bos (&page);
#endif
guint64 startoffset = ogg->offset;
GstOggStream *stream;
gboolean keyframe;
serialno = ogg_page_serialno (&page);
stream = gst_ogg_parse_find_stream (ogg, serialno);
GST_LOG_OBJECT (ogg, "Timestamping outgoing buffer as %" GST_TIME_FORMAT,
GST_TIME_ARGS (buffertimestamp));
if (stream) {
buffertimestamp = gst_ogg_stream_get_end_time_for_granulepos (stream,
granule);
if (ogg->video_stream) {
if (stream == ogg->video_stream) {
keyframe = gst_ogg_stream_granulepos_is_key_frame (stream, granule);
} else {
keyframe = FALSE;
}
} else {
keyframe = TRUE;
}
} else {
buffertimestamp = GST_CLOCK_TIME_NONE;
keyframe = TRUE;
}
pagebuffer = gst_ogg_parse_buffer_from_page (&page, startoffset,
buffertimestamp);
/* We read out 'ret' bytes, so we set the next offset appropriately */
ogg->offset += ret;
GST_LOG_OBJECT (ogg,
"processing ogg page (serial %08x, pageno %ld, "
"granule pos %" G_GUINT64_FORMAT ", bos %d, offset %"
G_GUINT64_FORMAT "-%" G_GUINT64_FORMAT ") keyframe=%d",
serialno, ogg_page_pageno (&page),
granule, bos, startoffset, ogg->offset, keyframe);
if (ogg_page_bos (&page)) {
/* If we've seen this serialno before, this is technically an error,
* we log this case but accept it - this one replaces the previous
* stream with this serialno. We can do this since we're streaming, and
* not supporting seeking...
*/
GstOggStream *stream = gst_ogg_parse_find_stream (ogg, serialno);
if (stream != NULL) {
GST_LOG_OBJECT (ogg, "Incorrect stream; repeats serial number %08x "
"at offset %" G_GINT64_FORMAT, serialno, ogg->offset);
}
if (ogg->last_page_not_bos) {
GST_LOG_OBJECT (ogg, "Deleting all referenced streams, found a new "
"chain starting with serial %u", serialno);
gst_ogg_parse_delete_all_streams (ogg);
}
stream = gst_ogg_parse_new_stream (ogg, &page);
ogg->last_page_not_bos = FALSE;
gst_buffer_ref (pagebuffer);
stream->headers = g_list_append (stream->headers, pagebuffer);
if (!ogg->in_headers) {
GST_LOG_OBJECT (ogg,
"Found start of new chain at offset %" G_GUINT64_FORMAT,
startoffset);
ogg->in_headers = 1;
}
/* For now, we just keep the header buffer in the stream->headers list;
* it actually gets output once we've collected the entire set
*/
} else {
/* Non-BOS page. Either: we're outside headers, and this isn't a
* header (normal data), outside headers and this is (error!), inside
* headers, this is (append header), or inside headers and this isn't
* (we've found the end of headers; flush the lot!)
*
* Before that, we flag that the last page seen (this one) was not a
* BOS page; that way we know that when we next see a BOS page it's a
* new chain, and we can flush all existing streams.
*/
page_type type;
GstOggStream *stream = gst_ogg_parse_find_stream (ogg, serialno);
if (!stream) {
GST_LOG_OBJECT (ogg,
"Non-BOS page unexpectedly found at %" G_GINT64_FORMAT,
ogg->offset);
goto failure;
}
ogg->last_page_not_bos = TRUE;
type = gst_ogg_parse_is_header (ogg, stream, &page);
if (type == PAGE_PENDING && ogg->in_headers) {
gst_buffer_ref (pagebuffer);
stream->unknown_pages = g_list_append (stream->unknown_pages,
pagebuffer);
} else if (type == PAGE_HEADER) {
if (!ogg->in_headers) {
GST_LOG_OBJECT (ogg, "Header page unexpectedly found outside "
"headers at offset %" G_GINT64_FORMAT, ogg->offset);
goto failure;
} else {
/* Append the header to the buffer list, after any unknown previous
* pages
*/
stream->headers = g_list_concat (stream->headers,
stream->unknown_pages);
g_list_free (stream->unknown_pages);
gst_buffer_ref (pagebuffer);
stream->headers = g_list_append (stream->headers, pagebuffer);
}
} else { /* PAGE_DATA, or PAGE_PENDING but outside headers */
if (ogg->in_headers) {
/* First non-header page... set caps, flush headers.
*
* First up, we build a single GValue list of all the pagebuffers
* we're using for the headers, in order.
* Then we set this on the caps structure. Then we can start pushing
* buffers for the headers, and finally we send this non-header
* page.
*/
GstCaps *caps;
GstStructure *structure;
GValue array = { 0 };
gint count = 0;
gboolean found_pending_headers = FALSE;
GSList *l;
g_value_init (&array, GST_TYPE_ARRAY);
for (l = ogg->oggstreams; l != NULL; l = l->next) {
GstOggStream *stream = (GstOggStream *) l->data;
if (g_list_length (stream->headers) == 0) {
GST_LOG_OBJECT (ogg, "No primary header found for stream %08x",
stream->serialno);
goto failure;
}
gst_ogg_parse_append_header (&array,
GST_BUFFER (stream->headers->data));
count++;
}
for (l = ogg->oggstreams; l != NULL; l = l->next) {
GstOggStream *stream = (GstOggStream *) l->data;
GList *j;
/* already appended the first header, now do headers 2-N */
for (j = stream->headers->next; j != NULL; j = j->next) {
gst_ogg_parse_append_header (&array, GST_BUFFER (j->data));
count++;
}
}
caps = gst_pad_get_caps (ogg->srcpad);
caps = gst_caps_make_writable (caps);
structure = gst_caps_get_structure (caps, 0);
gst_structure_set_value (structure, "streamheader", &array);
gst_pad_set_caps (ogg->srcpad, caps);
g_value_unset (&array);
if (ogg->caps)
gst_caps_unref (ogg->caps);
ogg->caps = caps;
GST_LOG_OBJECT (ogg, "Set \"streamheader\" caps with %d buffers "
"(one per page)", count);
/* Now, we do the same thing, but push buffers... */
for (l = ogg->oggstreams; l != NULL; l = l->next) {
GstOggStream *stream = (GstOggStream *) l->data;
GstBuffer *buf = GST_BUFFER (stream->headers->data);
buf = gst_buffer_make_metadata_writable (buf);
gst_buffer_set_caps (buf, caps);
result = gst_pad_push (ogg->srcpad, buf);
if (result != GST_FLOW_OK)
return result;
}
for (l = ogg->oggstreams; l != NULL; l = l->next) {
GstOggStream *stream = (GstOggStream *) l->data;
GList *j;
/* pushed the first one for each stream already, now do 2-N */
for (j = stream->headers->next; j != NULL; j = j->next) {
GstBuffer *buf = GST_BUFFER (j->data);
buf = gst_buffer_make_metadata_writable (buf);
gst_buffer_set_caps (buf, caps);
result = gst_pad_push (ogg->srcpad, buf);
if (result != GST_FLOW_OK)
return result;
}
}
ogg->in_headers = 0;
/* And finally the pending data pages */
for (l = ogg->oggstreams; l != NULL; l = l->next) {
GstOggStream *stream = (GstOggStream *) l->data;
GList *k;
if (stream->unknown_pages == NULL)
continue;
if (found_pending_headers) {
GST_WARNING_OBJECT (ogg, "Incorrectly muxed headers found at "
"approximate offset %" G_GINT64_FORMAT, ogg->offset);
}
found_pending_headers = TRUE;
GST_LOG_OBJECT (ogg, "Pushing %d pending pages after headers",
g_list_length (stream->unknown_pages) + 1);
for (k = stream->unknown_pages; k != NULL; k = k->next) {
GstBuffer *buf;
buf = gst_buffer_make_metadata_writable (GST_BUFFER (k->data));
gst_buffer_set_caps (buf, caps);
result = gst_pad_push (ogg->srcpad, buf);
if (result != GST_FLOW_OK)
return result;
}
g_list_foreach (stream->unknown_pages,
(GFunc) gst_mini_object_unref, NULL);
g_list_free (stream->unknown_pages);
stream->unknown_pages = NULL;
}
}
if (granule == -1) {
stream->stored_buffers = g_list_append (stream->stored_buffers,
pagebuffer);
} else {
while (stream->stored_buffers) {
GstBuffer *buf = stream->stored_buffers->data;
buf = gst_buffer_make_metadata_writable (buf);
gst_buffer_set_caps (buf, ogg->caps);
GST_BUFFER_TIMESTAMP (buf) = buffertimestamp;
if (!keyframe) {
GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_DELTA_UNIT);
} else {
keyframe = FALSE;
}
result = gst_pad_push (ogg->srcpad, buf);
if (result != GST_FLOW_OK)
return result;
stream->stored_buffers =
g_list_delete_link (stream->stored_buffers,
stream->stored_buffers);
}
pagebuffer = gst_buffer_make_metadata_writable (pagebuffer);
gst_buffer_set_caps (pagebuffer, ogg->caps);
if (!keyframe) {
GST_BUFFER_FLAG_SET (pagebuffer, GST_BUFFER_FLAG_DELTA_UNIT);
} else {
keyframe = FALSE;
}
result = gst_pad_push (ogg->srcpad, pagebuffer);
if (result != GST_FLOW_OK)
return result;
}
}
}
}
}
return result;
failure:
gst_pad_push_event (GST_PAD (ogg->srcpad), gst_event_new_eos ());
return GST_FLOW_ERROR;
}
static GstStateChangeReturn
gst_ogg_parse_change_state (GstElement * element, GstStateChange transition)
{
GstOggParse *ogg;
GstStateChangeReturn result = GST_STATE_CHANGE_FAILURE;
ogg = GST_OGG_PARSE (element);
switch (transition) {
case GST_STATE_CHANGE_NULL_TO_READY:
ogg_sync_init (&ogg->sync);
break;
case GST_STATE_CHANGE_READY_TO_PAUSED:
ogg_sync_reset (&ogg->sync);
break;
case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
break;
default:
break;
}
result = parent_class->change_state (element, transition);
switch (transition) {
case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
break;
case GST_STATE_CHANGE_PAUSED_TO_READY:
break;
case GST_STATE_CHANGE_READY_TO_NULL:
ogg_sync_clear (&ogg->sync);
break;
default:
break;
}
return result;
}
gboolean
gst_ogg_parse_plugin_init (GstPlugin * plugin)
{
GST_DEBUG_CATEGORY_INIT (gst_ogg_parse_debug, "oggparse", 0, "ogg parser");
return gst_element_register (plugin, "oggparse", GST_RANK_NONE,
GST_TYPE_OGG_PARSE);
}