/* GStreamer * Copyright (C) 2005 Michael Smith * * 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 #include #include #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_details_simple (element_class, "Ogg parser", "Codec/Parser", "parse ogg streams into pages (info about ogg: http://xiph.org)", "Michael Smith "); 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) { gsize size; guint8 *data; gchar *oggbuffer; GstFlowReturn ret = GST_FLOW_OK; data = gst_buffer_map (buffer, &size, NULL, GST_MAP_READ); 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; } memcpy (oggbuffer, data, 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_unmap (buffer, data, size); 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); 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_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); 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); }