gstreamer/ext/ogg/gstoggparse.c

747 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., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, 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_slice_free (GstOggStream, 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_slice_new0 (GstOggStream);
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, GstObject * parent,
GstBuffer * buffer);
static void
gst_ogg_parse_base_init (gpointer g_class)
{
GstElementClass *element_class = GST_ELEMENT_CLASS (g_class);
gst_element_class_set_static_metadata (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_static_pad_template (element_class,
&ogg_parse_sink_template_factory);
gst_element_class_add_static_pad_template (element_class,
&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)
{
gsize size;
gchar *oggbuffer;
GstFlowReturn ret = GST_FLOW_OK;
size = gst_buffer_get_size (buffer);
GST_DEBUG_OBJECT (ogg, "submitting %" G_GSIZE_FORMAT " 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;
}
size = gst_buffer_extract (buffer, 0, oggbuffer, size);
if (G_UNLIKELY (ogg_sync_wrote (&ogg->sync, size) < 0)) {
GST_ELEMENT_ERROR (ogg, STREAM, DECODE, (NULL),
("failed to write %" G_GSIZE_FORMAT " 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_HEADER);
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);
gst_buffer_fill (buf, 0, page->header, page->header_len);
gst_buffer_fill (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, GstObject * parent, 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 (parent);
GST_LOG_OBJECT (ogg,
"Chain function received buffer of size %" G_GSIZE_FORMAT,
gst_buffer_get_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_query_caps (ogg->srcpad, NULL);
caps = gst_caps_make_writable (caps);
structure = gst_caps_get_structure (caps, 0);
gst_structure_take_value (structure, "streamheader", &array);
gst_pad_set_caps (ogg->srcpad, caps);
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);
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);
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 = GST_BUFFER (k->data);
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_writable (buf);
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_writable (pagebuffer);
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);
}