/* GStreamer * Copyright (C) 2004 Wim Taymans * * gstoggdemux.c: ogg stream demuxer * * 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. */ /** * SECTION:element-oggdemux * @see_also: oggmux * * This element demuxes ogg files into their encoded audio and video components. * * * Example pipelines * |[ * gst-launch -v filesrc location=test.ogg ! oggdemux ! vorbisdec ! audioconvert ! alsasink * ]| Decodes the vorbis audio stored inside an ogg container. * */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include "gstoggdemux.h" #define CHUNKSIZE (8500) /* this is out of vorbisfile */ /* we hope we get a granpos within this many bytes off the end */ #define DURATION_CHUNK_OFFSET (64*1024) /* stop duration checks within this much of EOS */ #define EOS_AVOIDANCE_THRESHOLD 8192 /* An Ogg page can not be larger than 255 segments of 255 bytes, plus 26 bytes of header */ #define MAX_OGG_PAGE_SIZE (255 * 255 + 26) #define GST_FLOW_LIMIT GST_FLOW_CUSTOM_ERROR #define GST_FLOW_SKIP_PUSH GST_FLOW_CUSTOM_SUCCESS_1 #define GST_CHAIN_LOCK(ogg) g_mutex_lock(&(ogg)->chain_lock) #define GST_CHAIN_UNLOCK(ogg) g_mutex_unlock(&(ogg)->chain_lock) #define GST_PUSH_LOCK(ogg) \ do { \ GST_TRACE_OBJECT(ogg, "Push lock"); \ g_mutex_lock(&(ogg)->push_lock); \ } while(0) #define GST_PUSH_UNLOCK(ogg) \ do { \ GST_TRACE_OBJECT(ogg, "Push unlock"); \ g_mutex_unlock(&(ogg)->push_lock); \ } while(0) GST_DEBUG_CATEGORY (gst_ogg_demux_debug); GST_DEBUG_CATEGORY (gst_ogg_demux_setup_debug); #define GST_CAT_DEFAULT gst_ogg_demux_debug static ogg_packet * _ogg_packet_copy (const ogg_packet * packet) { ogg_packet *ret = g_slice_new (ogg_packet); *ret = *packet; ret->packet = g_memdup (packet->packet, packet->bytes); return ret; } static void _ogg_packet_free (ogg_packet * packet) { g_free (packet->packet); g_slice_free (ogg_packet, packet); } static ogg_page * gst_ogg_page_copy (ogg_page * page) { ogg_page *p = g_slice_new (ogg_page); /* make a copy of the page */ p->header = g_memdup (page->header, page->header_len); p->header_len = page->header_len; p->body = g_memdup (page->body, page->body_len); p->body_len = page->body_len; return p; } static void gst_ogg_page_free (ogg_page * page) { g_free (page->header); g_free (page->body); g_slice_free (ogg_page, page); } static gboolean gst_ogg_demux_collect_chain_info (GstOggDemux * ogg, GstOggChain * chain); static gboolean gst_ogg_demux_activate_chain (GstOggDemux * ogg, GstOggChain * chain, GstEvent * event); static void gst_ogg_pad_mark_discont (GstOggPad * pad); static void gst_ogg_chain_mark_discont (GstOggChain * chain); static gboolean gst_ogg_demux_perform_seek (GstOggDemux * ogg, GstEvent * event); static gboolean gst_ogg_demux_receive_event (GstElement * element, GstEvent * event); static void gst_ogg_pad_dispose (GObject * object); static void gst_ogg_pad_finalize (GObject * object); static gboolean gst_ogg_pad_src_query (GstPad * pad, GstObject * parent, GstQuery * query); static gboolean gst_ogg_pad_event (GstPad * pad, GstObject * parent, GstEvent * event); static GstOggPad *gst_ogg_chain_get_stream (GstOggChain * chain, guint32 serialno); static GstFlowReturn gst_ogg_demux_combine_flows (GstOggDemux * ogg, GstOggPad * pad, GstFlowReturn ret); static void gst_ogg_demux_sync_streams (GstOggDemux * ogg); static GstCaps *gst_ogg_demux_set_header_on_caps (GstOggDemux * ogg, GstCaps * caps, GList * headers); static gboolean gst_ogg_demux_send_event (GstOggDemux * ogg, GstEvent * event); static gboolean gst_ogg_demux_perform_seek_push (GstOggDemux * ogg, GstEvent * event); static gboolean gst_ogg_demux_check_duration_push (GstOggDemux * ogg, GstSeekFlags flags, GstEvent * event); GType gst_ogg_pad_get_type (void); G_DEFINE_TYPE (GstOggPad, gst_ogg_pad, GST_TYPE_PAD); static void gst_ogg_pad_class_init (GstOggPadClass * klass) { GObjectClass *gobject_class; gobject_class = (GObjectClass *) klass; gobject_class->dispose = gst_ogg_pad_dispose; gobject_class->finalize = gst_ogg_pad_finalize; } static void gst_ogg_pad_init (GstOggPad * pad) { gst_pad_set_event_function (GST_PAD (pad), GST_DEBUG_FUNCPTR (gst_ogg_pad_event)); gst_pad_set_query_function (GST_PAD (pad), GST_DEBUG_FUNCPTR (gst_ogg_pad_src_query)); gst_pad_use_fixed_caps (GST_PAD (pad)); pad->current_granule = -1; pad->keyframe_granule = -1; pad->start_time = GST_CLOCK_TIME_NONE; pad->position = GST_CLOCK_TIME_NONE; pad->have_type = FALSE; pad->continued = NULL; pad->map.headers = NULL; pad->map.queued = NULL; pad->map.granulerate_n = 0; pad->map.granulerate_d = 0; pad->map.granuleshift = -1; } static void gst_ogg_pad_dispose (GObject * object) { GstOggPad *pad = GST_OGG_PAD (object); pad->chain = NULL; pad->ogg = NULL; g_list_foreach (pad->map.headers, (GFunc) _ogg_packet_free, NULL); g_list_free (pad->map.headers); pad->map.headers = NULL; g_list_foreach (pad->map.queued, (GFunc) _ogg_packet_free, NULL); g_list_free (pad->map.queued); pad->map.queued = NULL; g_free (pad->map.index); pad->map.index = NULL; /* clear continued pages */ g_list_foreach (pad->continued, (GFunc) gst_ogg_page_free, NULL); g_list_free (pad->continued); pad->continued = NULL; if (pad->map.caps) { gst_caps_unref (pad->map.caps); pad->map.caps = NULL; } if (pad->map.taglist) { gst_tag_list_unref (pad->map.taglist); pad->map.taglist = NULL; } ogg_stream_reset (&pad->map.stream); G_OBJECT_CLASS (gst_ogg_pad_parent_class)->dispose (object); } static void gst_ogg_pad_finalize (GObject * object) { GstOggPad *pad = GST_OGG_PAD (object); ogg_stream_clear (&pad->map.stream); G_OBJECT_CLASS (gst_ogg_pad_parent_class)->finalize (object); } static gboolean gst_ogg_pad_src_query (GstPad * pad, GstObject * parent, GstQuery * query) { gboolean res = TRUE; GstOggDemux *ogg; ogg = GST_OGG_DEMUX (parent); switch (GST_QUERY_TYPE (query)) { case GST_QUERY_DURATION: { GstFormat format; gint64 total_time = -1; gst_query_parse_duration (query, &format, NULL); /* can only get duration in time */ if (format != GST_FORMAT_TIME) goto wrong_format; if (ogg->total_time != -1) { /* we can return the total length */ total_time = ogg->total_time; } else { gint bitrate = ogg->bitrate; /* try with length and bitrate */ if (bitrate > 0) { GstQuery *uquery; /* ask upstream for total length in bytes */ uquery = gst_query_new_duration (GST_FORMAT_BYTES); if (gst_pad_peer_query (ogg->sinkpad, uquery)) { gint64 length; gst_query_parse_duration (uquery, NULL, &length); /* estimate using the bitrate */ total_time = gst_util_uint64_scale (length, 8 * GST_SECOND, bitrate); GST_LOG_OBJECT (ogg, "length: %" G_GINT64_FORMAT ", bitrate %d, total_time %" GST_TIME_FORMAT, length, bitrate, GST_TIME_ARGS (total_time)); } gst_query_unref (uquery); } } gst_query_set_duration (query, GST_FORMAT_TIME, total_time); break; } case GST_QUERY_SEEKING: { GstFormat format; gst_query_parse_seeking (query, &format, NULL, NULL, NULL); if (format == GST_FORMAT_TIME) { gboolean seekable = FALSE; gint64 stop = -1; GST_CHAIN_LOCK (ogg); if (ogg->pullmode) { seekable = TRUE; stop = ogg->total_time; } else if (ogg->push_disable_seeking) { seekable = FALSE; } else if (ogg->current_chain == NULL) { GstQuery *squery; /* assume we can seek if upstream is seekable in BYTES format */ GST_LOG_OBJECT (ogg, "no current chain, check upstream seekability"); squery = gst_query_new_seeking (GST_FORMAT_BYTES); if (gst_pad_peer_query (ogg->sinkpad, squery)) gst_query_parse_seeking (squery, NULL, &seekable, NULL, NULL); else seekable = FALSE; gst_query_unref (squery); } else if (ogg->current_chain->streams->len) { gint i; seekable = FALSE; for (i = 0; i < ogg->current_chain->streams->len; i++) { GstOggPad *pad = g_array_index (ogg->current_chain->streams, GstOggPad *, i); seekable = TRUE; if (pad->map.index != NULL && pad->map.n_index != 0) { GstOggIndex *idx; GstClockTime idx_time; idx = &pad->map.index[pad->map.n_index - 1]; idx_time = gst_util_uint64_scale (idx->timestamp, GST_SECOND, pad->map.kp_denom); if (stop == -1) stop = idx_time; else stop = MAX (idx_time, stop); } else { stop = -1; /* we've no clue, sadly, without seeking */ } } } gst_query_set_seeking (query, GST_FORMAT_TIME, seekable, 0, stop); GST_CHAIN_UNLOCK (ogg); } else { res = FALSE; } break; } case GST_QUERY_SEGMENT:{ GstFormat format; gint64 start, stop; format = ogg->segment.format; start = gst_segment_to_stream_time (&ogg->segment, format, ogg->segment.start); if ((stop = ogg->segment.stop) == -1) stop = ogg->segment.duration; else stop = gst_segment_to_stream_time (&ogg->segment, format, stop); gst_query_set_segment (query, ogg->segment.rate, format, start, stop); res = TRUE; break; } default: res = gst_pad_query_default (pad, parent, query); break; } done: return res; /* ERRORS */ wrong_format: { GST_DEBUG_OBJECT (ogg, "only query duration on TIME is supported"); res = FALSE; goto done; } } static gboolean gst_ogg_demux_receive_event (GstElement * element, GstEvent * event) { gboolean res; GstOggDemux *ogg; ogg = GST_OGG_DEMUX (element); switch (GST_EVENT_TYPE (event)) { case GST_EVENT_SEEK: /* now do the seek */ res = gst_ogg_demux_perform_seek (ogg, event); gst_event_unref (event); break; default: GST_DEBUG_OBJECT (ogg, "We only handle seek events here"); goto error; } return res; /* ERRORS */ error: { GST_DEBUG_OBJECT (ogg, "error handling event"); gst_event_unref (event); return FALSE; } } static gboolean gst_ogg_pad_event (GstPad * pad, GstObject * parent, GstEvent * event) { gboolean res; GstOggDemux *ogg; ogg = GST_OGG_DEMUX (parent); switch (GST_EVENT_TYPE (event)) { case GST_EVENT_SEEK: /* now do the seek */ res = gst_ogg_demux_perform_seek (ogg, event); gst_event_unref (event); break; case GST_EVENT_RECONFIGURE: GST_OGG_PAD (pad)->last_ret = GST_FLOW_OK; res = gst_pad_event_default (pad, parent, event); break; default: res = gst_pad_event_default (pad, parent, event); break; } return res; } static void gst_ogg_pad_reset (GstOggPad * pad) { ogg_stream_reset (&pad->map.stream); GST_DEBUG_OBJECT (pad, "doing reset"); /* clear continued pages */ g_list_foreach (pad->continued, (GFunc) gst_ogg_page_free, NULL); g_list_free (pad->continued); pad->continued = NULL; pad->last_ret = GST_FLOW_OK; pad->position = GST_CLOCK_TIME_NONE; pad->current_granule = -1; pad->keyframe_granule = -1; pad->is_eos = FALSE; } /* queue data, basically takes the packet, puts it in a buffer and store the * buffer in the queued list. */ static GstFlowReturn gst_ogg_demux_queue_data (GstOggPad * pad, ogg_packet * packet) { #ifndef GST_DISABLE_GST_DEBUG GstOggDemux *ogg = pad->ogg; #endif GST_DEBUG_OBJECT (ogg, "%p queueing data serial %08x", pad, pad->map.serialno); pad->map.queued = g_list_append (pad->map.queued, _ogg_packet_copy (packet)); /* we are ok now */ return GST_FLOW_OK; } static GstFlowReturn gst_ogg_demux_chain_peer (GstOggPad * pad, ogg_packet * packet, gboolean push_headers) { GstBuffer *buf = NULL; GstFlowReturn ret, cret; GstOggDemux *ogg = pad->ogg; gint64 current_time; GstOggChain *chain; gint64 duration; gint offset; gint trim; GstClockTime out_timestamp, out_duration; guint64 out_offset, out_offset_end; gboolean delta_unit = FALSE; gboolean is_header; ret = cret = GST_FLOW_OK; GST_DEBUG_OBJECT (pad, "Chaining %d %d %" GST_TIME_FORMAT " %d %p", ogg->pullmode, ogg->push_state, GST_TIME_ARGS (ogg->push_time_length), ogg->push_disable_seeking, ogg->building_chain); if (G_UNLIKELY (pad->is_eos)) { GST_DEBUG_OBJECT (pad, "Skipping packet on pad that is eos"); ret = GST_FLOW_EOS; goto combine; } GST_PUSH_LOCK (ogg); if (!ogg->pullmode && ogg->push_state == PUSH_PLAYING && ogg->push_time_length == GST_CLOCK_TIME_NONE && !ogg->push_disable_seeking) { if (!ogg->building_chain) { /* we got all headers, now try to get duration */ if (!gst_ogg_demux_check_duration_push (ogg, GST_SEEK_FLAG_FLUSH, NULL)) { GST_PUSH_UNLOCK (ogg); return GST_FLOW_OK; } } GST_PUSH_UNLOCK (ogg); return GST_FLOW_OK; } GST_PUSH_UNLOCK (ogg); GST_DEBUG_OBJECT (ogg, "%p streaming to peer serial %08x", pad, pad->map.serialno); gst_ogg_stream_update_stats (&pad->map, packet); if (pad->map.is_ogm) { const guint8 *data; long bytes; data = packet->packet; bytes = packet->bytes; if (bytes < 1) goto empty_packet; if ((data[0] & 1) || (data[0] & 3 && pad->map.is_ogm_text)) { /* We don't push header packets for OGM */ goto done; } offset = 1 + (((data[0] & 0xc0) >> 6) | ((data[0] & 0x02) << 1)); delta_unit = (((data[0] & 0x08) >> 3) == 0); trim = 0; /* Strip trailing \0 for subtitles */ if (pad->map.is_ogm_text) { while (bytes && data[bytes - 1] == 0) { trim++; bytes--; } } } else if (pad->map.is_vp8) { if ((packet->bytes >= 7 && memcmp (packet->packet, "OVP80\2 ", 7) == 0) || packet->b_o_s || (packet->bytes >= 5 && memcmp (packet->packet, "OVP80", 5) == 0)) { /* We don't push header packets for VP8 */ goto done; } offset = 0; trim = 0; delta_unit = !gst_ogg_stream_packet_is_key_frame (&pad->map, packet); } else { offset = 0; trim = 0; delta_unit = !gst_ogg_stream_packet_is_key_frame (&pad->map, packet); } /* get timing info for the packet */ is_header = gst_ogg_stream_packet_is_header (&pad->map, packet); if (is_header) { duration = 0; GST_DEBUG_OBJECT (ogg, "packet is header"); } else { duration = gst_ogg_stream_get_packet_duration (&pad->map, packet); GST_DEBUG_OBJECT (ogg, "packet duration %" G_GUINT64_FORMAT, duration); } if (packet->b_o_s) { out_timestamp = GST_CLOCK_TIME_NONE; out_duration = GST_CLOCK_TIME_NONE; out_offset = 0; out_offset_end = -1; } else { if (packet->granulepos != -1) { gint64 granule = gst_ogg_stream_granulepos_to_granule (&pad->map, packet->granulepos); if (granule < 0) { GST_ERROR_OBJECT (ogg, "granulepos %" G_GINT64_FORMAT " yielded granule %" G_GINT64_FORMAT, (gint64) packet->granulepos, (gint64) granule); return GST_FLOW_ERROR; } pad->current_granule = granule; pad->keyframe_granule = gst_ogg_stream_granulepos_to_key_granule (&pad->map, packet->granulepos); GST_DEBUG_OBJECT (ogg, "new granule %" G_GUINT64_FORMAT, pad->current_granule); } else if (ogg->segment.rate > 0.0 && pad->current_granule != -1) { pad->current_granule += duration; if (!delta_unit) { pad->keyframe_granule = pad->current_granule; } GST_DEBUG_OBJECT (ogg, "interpolating granule %" G_GUINT64_FORMAT, pad->current_granule); } if (ogg->segment.rate < 0.0 && packet->granulepos == -1) { /* negative rates, only set timestamp on the packets with a granulepos */ out_timestamp = -1; out_duration = -1; out_offset = -1; out_offset_end = -1; } else { /* we only push buffers after we have a valid granule. This is done so that * we nicely skip packets without a timestamp after a seek. This is ok * because we base or seek on the packet after the page with the smaller * timestamp. */ if (pad->current_granule == -1) goto no_timestamp; if (pad->map.is_ogm) { out_timestamp = gst_ogg_stream_granule_to_time (&pad->map, pad->current_granule); out_duration = gst_util_uint64_scale (duration, GST_SECOND * pad->map.granulerate_d, pad->map.granulerate_n); } else if (pad->map.is_sparse) { out_timestamp = gst_ogg_stream_granule_to_time (&pad->map, pad->current_granule); if (duration == GST_CLOCK_TIME_NONE) { out_duration = GST_CLOCK_TIME_NONE; } else { out_duration = gst_util_uint64_scale (duration, GST_SECOND * pad->map.granulerate_d, pad->map.granulerate_n); } } else { out_timestamp = gst_ogg_stream_granule_to_time (&pad->map, pad->current_granule - duration); out_duration = gst_ogg_stream_granule_to_time (&pad->map, pad->current_granule) - out_timestamp; } out_offset_end = gst_ogg_stream_granule_to_granulepos (&pad->map, pad->current_granule, pad->keyframe_granule); out_offset = gst_ogg_stream_granule_to_time (&pad->map, pad->current_granule); } } if (pad->map.is_ogm_text) { /* check for invalid buffer sizes */ if (G_UNLIKELY (offset + trim >= packet->bytes)) goto empty_packet; } if (!pad->added) goto not_added; buf = gst_buffer_new_and_alloc (packet->bytes - offset - trim); /* set delta flag for OGM content */ if (delta_unit) GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_DELTA_UNIT); /* set header flag for buffers that are also in the streamheaders */ if (is_header) GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_HEADER); if (packet->packet != NULL) { /* copy packet in buffer */ gst_buffer_fill (buf, 0, packet->packet + offset, packet->bytes - offset - trim); } GST_BUFFER_TIMESTAMP (buf) = out_timestamp; GST_BUFFER_DURATION (buf) = out_duration; GST_BUFFER_OFFSET (buf) = out_offset; GST_BUFFER_OFFSET_END (buf) = out_offset_end; /* Mark discont on the buffer */ if (pad->discont) { GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_DISCONT); pad->discont = FALSE; } pad->position = ogg->segment.position; /* don't push the header packets when we are asked to skip them */ if (!packet->b_o_s || push_headers) { if (pad->last_ret == GST_FLOW_OK) { ret = gst_pad_push (GST_PAD_CAST (pad), buf); } else { GST_DEBUG_OBJECT (ogg, "not pushing buffer on error pad"); ret = pad->last_ret; gst_buffer_unref (buf); } buf = NULL; } /* we're done with skeleton stuff */ if (pad->map.is_skeleton) goto combine; /* check if valid granulepos, then we can calculate the current * position. We know the granule for each packet but we only want to update * the position when we have a valid granulepos on the packet because else * our time jumps around for the different streams. */ if (packet->granulepos < 0) goto combine; /* convert to time */ current_time = gst_ogg_stream_get_end_time_for_granulepos (&pad->map, packet->granulepos); /* convert to stream time */ if ((chain = pad->chain)) { gint64 chain_start = 0; if (chain->segment_start != GST_CLOCK_TIME_NONE) chain_start = chain->segment_start; current_time = current_time - chain_start + chain->begin_time; } /* and store as the current position */ ogg->segment.position = current_time; GST_DEBUG_OBJECT (ogg, "ogg current time %" GST_TIME_FORMAT, GST_TIME_ARGS (current_time)); /* check stream eos */ if (!pad->is_eos && !delta_unit && ((ogg->segment.rate > 0.0 && ogg->segment.stop != GST_CLOCK_TIME_NONE && current_time >= ogg->segment.stop) || (ogg->segment.rate < 0.0 && current_time <= ogg->segment.start))) { GST_DEBUG_OBJECT (ogg, "marking pad %p EOS", pad); pad->is_eos = TRUE; if (ret == GST_FLOW_OK) { ret = GST_FLOW_EOS; } } combine: /* combine flows */ cret = gst_ogg_demux_combine_flows (ogg, pad, ret); done: if (buf) gst_buffer_unref (buf); /* return combined flow result */ return cret; /* special cases */ empty_packet: { GST_DEBUG_OBJECT (ogg, "Skipping empty packet"); goto done; } no_timestamp: { GST_DEBUG_OBJECT (ogg, "skipping packet: no valid granule found yet"); goto done; } not_added: { GST_DEBUG_OBJECT (ogg, "pad not added yet"); goto done; } } static guint64 gst_ogg_demux_collect_start_time (GstOggDemux * ogg, GstOggChain * chain) { gint i; guint64 start_time = G_MAXUINT64; for (i = 0; i < chain->streams->len; i++) { GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i); if (pad->map.is_skeleton) continue; /* can do this if the pad start time is not defined */ GST_DEBUG_OBJECT (ogg, "Pad %08x (%s) start time is %" GST_TIME_FORMAT, pad->map.serialno, gst_ogg_stream_get_media_type (&pad->map), GST_TIME_ARGS (pad->start_time)); if (pad->start_time == GST_CLOCK_TIME_NONE) { if (!pad->map.is_sparse) { start_time = G_MAXUINT64; break; } } else { start_time = MIN (start_time, pad->start_time); } } return start_time; } static GstClockTime gst_ogg_demux_collect_sync_time (GstOggDemux * ogg, GstOggChain * chain) { gint i; GstClockTime sync_time = GST_CLOCK_TIME_NONE; if (!chain) { GST_WARNING_OBJECT (ogg, "No chain!"); return GST_CLOCK_TIME_NONE; } for (i = 0; i < chain->streams->len; i++) { GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i); if (pad->map.is_sparse) continue; if (pad->push_sync_time == GST_CLOCK_TIME_NONE) { sync_time = GST_CLOCK_TIME_NONE; break; } else { if (sync_time == GST_CLOCK_TIME_NONE) sync_time = pad->push_sync_time; else sync_time = MAX (sync_time, pad->push_sync_time); } } return sync_time; } /* submit a packet to the oggpad, this function will run the type detection * code for the pad if this is the first packet for this stream */ static GstFlowReturn gst_ogg_pad_submit_packet (GstOggPad * pad, ogg_packet * packet) { gint64 granule; GstFlowReturn ret = GST_FLOW_OK; GstOggDemux *ogg = pad->ogg; GST_DEBUG_OBJECT (ogg, "%p submit packet serial %08x", pad, pad->map.serialno); if (!pad->have_type) { pad->have_type = gst_ogg_stream_setup_map (&pad->map, packet); if (!pad->have_type && !pad->map.caps) { pad->map.caps = gst_caps_new_empty_simple ("application/x-unknown"); } if (pad->map.is_skeleton) { GST_DEBUG_OBJECT (ogg, "we have a fishead"); /* copy values over to global ogg level */ ogg->basetime = pad->map.basetime; ogg->prestime = pad->map.prestime; /* use total time to update the total ogg time */ if (ogg->total_time == -1) { ogg->total_time = pad->map.total_time; } else if (pad->map.total_time > 0) { ogg->total_time = MAX (ogg->total_time, pad->map.total_time); } } if (!pad->map.caps) { GST_WARNING_OBJECT (ogg, "stream parser didn't create src pad caps"); } } if (pad->map.is_skeleton) { guint32 serialno; GstOggPad *skel_pad; GstOggSkeleton type; /* try to parse the serialno first */ if (gst_ogg_map_parse_fisbone (&pad->map, packet->packet, packet->bytes, &serialno, &type)) { GST_DEBUG_OBJECT (pad->ogg, "got skeleton packet for stream 0x%08x", serialno); skel_pad = gst_ogg_chain_get_stream (pad->chain, serialno); if (skel_pad) { switch (type) { case GST_OGG_SKELETON_FISBONE: /* parse the remainder of the fisbone in the pad with the serialno, * note that we ignore the start_time as this is usually wrong for * live streams */ gst_ogg_map_add_fisbone (&skel_pad->map, &pad->map, packet->packet, packet->bytes, NULL); break; case GST_OGG_SKELETON_INDEX: gst_ogg_map_add_index (&skel_pad->map, &pad->map, packet->packet, packet->bytes); /* use total time to update the total ogg time */ if (ogg->total_time == -1) { ogg->total_time = skel_pad->map.total_time; } else if (skel_pad->map.total_time > 0) { ogg->total_time = MAX (ogg->total_time, skel_pad->map.total_time); } break; default: break; } } else { GST_WARNING_OBJECT (pad->ogg, "found skeleton fisbone for an unknown stream 0x%08x", serialno); } } } granule = gst_ogg_stream_granulepos_to_granule (&pad->map, packet->granulepos); if (granule >= 0) { GST_DEBUG_OBJECT (ogg, "%p has granulepos %" G_GINT64_FORMAT, pad, granule); pad->current_granule = granule; } else if (granule != -1) { GST_ERROR_OBJECT (ogg, "granulepos %" G_GINT64_FORMAT " yielded granule %" G_GINT64_FORMAT, (gint64) packet->granulepos, (gint64) granule); return GST_FLOW_ERROR; } /* restart header packet count when seeing a b_o_s page; * particularly useful following a seek or even following chain finding */ if (packet->b_o_s) { GST_DEBUG_OBJECT (ogg, "b_o_s packet, resetting header packet count"); pad->map.n_header_packets_seen = 0; if (!pad->map.have_headers) { GST_DEBUG_OBJECT (ogg, "clearing header packets"); g_list_foreach (pad->map.headers, (GFunc) _ogg_packet_free, NULL); g_list_free (pad->map.headers); pad->map.headers = NULL; } } /* Overload the value of b_o_s in ogg_packet with a flag whether or * not this is a header packet. Maybe some day this could be cleaned * up. */ packet->b_o_s = gst_ogg_stream_packet_is_header (&pad->map, packet); if (!packet->b_o_s) { GST_DEBUG ("found non-header packet"); pad->map.have_headers = TRUE; if (pad->start_time == GST_CLOCK_TIME_NONE) { gint64 duration = gst_ogg_stream_get_packet_duration (&pad->map, packet); GST_DEBUG ("duration %" G_GINT64_FORMAT, duration); if (duration != -1) { pad->map.accumulated_granule += duration; GST_DEBUG ("accumulated granule %" G_GINT64_FORMAT, pad->map.accumulated_granule); } if (packet->granulepos != -1) { ogg_int64_t start_granule; gint64 granule; granule = gst_ogg_stream_granulepos_to_granule (&pad->map, packet->granulepos); if (granule < 0) { GST_ERROR_OBJECT (ogg, "granulepos %" G_GINT64_FORMAT " yielded granule %" G_GINT64_FORMAT, (gint64) packet->granulepos, (gint64) granule); return GST_FLOW_ERROR; } if (granule >= pad->map.accumulated_granule) start_granule = granule - pad->map.accumulated_granule; else { if (pad->map.forbid_start_clamping) { GST_ERROR_OBJECT (ogg, "Start of stream maps to negative time"); return GST_FLOW_ERROR; } else { start_granule = 0; } } pad->start_time = gst_ogg_stream_granule_to_time (&pad->map, start_granule); GST_DEBUG_OBJECT (ogg, "start time %" GST_TIME_FORMAT " (%" GST_TIME_FORMAT ") for %s " "from granpos %" G_GINT64_FORMAT " (granule %" G_GINT64_FORMAT ", " "accumulated granule %" G_GINT64_FORMAT, GST_TIME_ARGS (pad->start_time), GST_TIME_ARGS (pad->start_time), gst_ogg_stream_get_media_type (&pad->map), (gint64) packet->granulepos, granule, pad->map.accumulated_granule); } else { packet->granulepos = gst_ogg_stream_granule_to_granulepos (&pad->map, pad->map.accumulated_granule, pad->keyframe_granule); } } } else { /* look for tags in header packet (before inc header count) */ gst_ogg_stream_extract_tags (&pad->map, packet); pad->map.n_header_packets_seen++; if (!pad->map.have_headers) { pad->map.headers = g_list_append (pad->map.headers, _ogg_packet_copy (packet)); GST_DEBUG ("keeping header packet %d", pad->map.n_header_packets_seen); } } /* we know the start_time of the pad data, see if we * can activate the complete chain if this is a dynamic * chain. We need all the headers too for this. */ if (pad->start_time != GST_CLOCK_TIME_NONE && pad->map.have_headers) { GstOggChain *chain = pad->chain; /* check if complete chain has start time */ if (chain == ogg->building_chain) { GstEvent *event = NULL; if (ogg->resync) { guint64 start_time; GST_DEBUG_OBJECT (ogg, "need to resync"); /* when we need to resync after a seek, we wait until we have received * timestamps on all streams */ start_time = gst_ogg_demux_collect_start_time (ogg, chain); if (start_time != G_MAXUINT64) { gint64 segment_time; GstSegment segment; GST_DEBUG_OBJECT (ogg, "start_time: %" GST_TIME_FORMAT, GST_TIME_ARGS (start_time)); if (chain->segment_start < start_time) segment_time = (start_time - chain->segment_start) + chain->begin_time; else segment_time = chain->begin_time; /* create the newsegment event we are going to send out */ gst_segment_init (&segment, GST_FORMAT_TIME); GST_PUSH_LOCK (ogg); if (!ogg->pullmode && ogg->push_state == PUSH_LINEAR2) { /* if we are fast forwarding to the actual seek target, ensure previous frames are clipped */ GST_DEBUG_OBJECT (ogg, "Resynced, starting segment at %" GST_TIME_FORMAT ", start_time %" GST_TIME_FORMAT, GST_TIME_ARGS (ogg->push_seek_time_original_target), GST_TIME_ARGS (start_time)); segment.rate = ogg->push_seek_rate; segment.start = ogg->push_seek_time_original_target; segment.stop = ogg->push_seek_time_original_stop; segment.time = ogg->push_seek_time_original_target; event = gst_event_new_segment (&segment); gst_event_set_seqnum (event, ogg->push_seek_seqnum); ogg->push_state = PUSH_PLAYING; } else { segment.rate = ogg->segment.rate; segment.applied_rate = ogg->segment.applied_rate; segment.start = start_time; segment.stop = chain->segment_stop; segment.time = segment_time; event = gst_event_new_segment (&segment); } GST_PUSH_UNLOCK (ogg); ogg->resync = FALSE; } } else { /* see if we have enough info to activate the chain, we have enough info * when all streams have a valid start time. */ if (gst_ogg_demux_collect_chain_info (ogg, chain)) { GstSegment segment; GST_DEBUG_OBJECT (ogg, "segment_start: %" GST_TIME_FORMAT, GST_TIME_ARGS (chain->segment_start)); GST_DEBUG_OBJECT (ogg, "segment_stop: %" GST_TIME_FORMAT, GST_TIME_ARGS (chain->segment_stop)); GST_DEBUG_OBJECT (ogg, "segment_time: %" GST_TIME_FORMAT, GST_TIME_ARGS (chain->begin_time)); /* create the newsegment event we are going to send out */ gst_segment_init (&segment, GST_FORMAT_TIME); segment.rate = ogg->segment.rate; segment.applied_rate = ogg->segment.applied_rate; segment.start = chain->segment_start; segment.stop = chain->segment_stop; segment.time = chain->begin_time; event = gst_event_new_segment (&segment); } } if (event) { gst_event_set_seqnum (event, ogg->seqnum); gst_ogg_demux_activate_chain (ogg, chain, event); ogg->building_chain = NULL; } } } /* if we are building a chain, store buffer for when we activate * it. This path is taken if we operate in streaming mode. */ if (ogg->building_chain) { /* bos packets where stored in the header list so we can discard * them here*/ if (!packet->b_o_s) ret = gst_ogg_demux_queue_data (pad, packet); } /* else we are completely streaming to the peer */ else { ret = gst_ogg_demux_chain_peer (pad, packet, !ogg->pullmode); } return ret; } /* flush at most @npackets from the stream layer. All packets if * @npackets is 0; */ static GstFlowReturn gst_ogg_pad_stream_out (GstOggPad * pad, gint npackets) { GstFlowReturn result = GST_FLOW_OK; gboolean done = FALSE; GstOggDemux *ogg; ogg = pad->ogg; while (!done) { int ret; ogg_packet packet; ret = ogg_stream_packetout (&pad->map.stream, &packet); switch (ret) { case 0: GST_LOG_OBJECT (ogg, "packetout done"); done = TRUE; break; case -1: GST_LOG_OBJECT (ogg, "packetout discont"); if (!pad->map.is_sparse) { gst_ogg_chain_mark_discont (pad->chain); } else { gst_ogg_pad_mark_discont (pad); } break; case 1: GST_LOG_OBJECT (ogg, "packetout gave packet of size %ld", packet.bytes); if (packet.bytes > ogg->max_packet_size) ogg->max_packet_size = packet.bytes; result = gst_ogg_pad_submit_packet (pad, &packet); /* not linked is not a problem, it's possible that we are still * collecting headers and that we don't have exposed the pads yet */ if (result == GST_FLOW_NOT_LINKED) break; else if (result <= GST_FLOW_EOS) goto could_not_submit; break; default: GST_WARNING_OBJECT (ogg, "invalid return value %d for ogg_stream_packetout, resetting stream", ret); gst_ogg_pad_reset (pad); break; } if (npackets > 0) { npackets--; done = (npackets == 0); } } return result; /* ERRORS */ could_not_submit: { GST_WARNING_OBJECT (ogg, "could not submit packet for stream %08x, " "error: %d", pad->map.serialno, result); gst_ogg_pad_reset (pad); return result; } } static void gst_ogg_demux_setup_bisection_bounds (GstOggDemux * ogg) { if (ogg->push_last_seek_time >= ogg->push_seek_time_target) { GST_DEBUG_OBJECT (ogg, "We overshot by %" GST_TIME_FORMAT, GST_TIME_ARGS (ogg->push_last_seek_time - ogg->push_seek_time_target)); ogg->push_offset1 = ogg->push_last_seek_offset; ogg->push_time1 = ogg->push_last_seek_time; ogg->seek_undershot = FALSE; } else { GST_DEBUG_OBJECT (ogg, "We undershot by %" GST_TIME_FORMAT, GST_TIME_ARGS (ogg->push_seek_time_target - ogg->push_last_seek_time)); ogg->push_offset0 = ogg->push_last_seek_offset; ogg->push_time0 = ogg->push_last_seek_time; ogg->seek_undershot = TRUE; } } static gint64 gst_ogg_demux_estimate_bisection_target (GstOggDemux * ogg, float seek_quality) { gint64 best; gint64 segment_bitrate; gint64 skew; /* we might not know the length of the stream in time, so push_time1 might not be set */ GST_DEBUG_OBJECT (ogg, "push time 1: %" GST_TIME_FORMAT ", dbytes %" G_GINT64_FORMAT, GST_TIME_ARGS (ogg->push_time1), ogg->push_offset1 - ogg->push_offset0); if (ogg->push_time1 == GST_CLOCK_TIME_NONE) { GST_DEBUG_OBJECT (ogg, "New segment to consider: bytes %" G_GINT64_FORMAT " %" G_GINT64_FORMAT ", time %" GST_TIME_FORMAT " (open ended)", ogg->push_offset0, ogg->push_offset1, GST_TIME_ARGS (ogg->push_time0)); if (ogg->push_last_seek_time == ogg->push_start_time) { /* if we're at start and don't know the end time, we can't estimate bitrate, so get the nominal declared bitrate as a failsafe, or some random constant which will be discarded after we made a (probably dire) first guess */ segment_bitrate = (ogg->bitrate > 0 ? ogg->bitrate : 1000); } else { segment_bitrate = gst_util_uint64_scale (ogg->push_last_seek_offset - 0, 8 * GST_SECOND, ogg->push_last_seek_time - ogg->push_start_time); } best = ogg->push_offset0 + gst_util_uint64_scale (ogg->push_seek_time_target - ogg->push_time0, segment_bitrate, 8 * GST_SECOND); ogg->seek_secant = TRUE; } else { GST_DEBUG_OBJECT (ogg, "New segment to consider: bytes %" G_GINT64_FORMAT " %" G_GINT64_FORMAT ", time %" GST_TIME_FORMAT " %" GST_TIME_FORMAT, ogg->push_offset0, ogg->push_offset1, GST_TIME_ARGS (ogg->push_time0), GST_TIME_ARGS (ogg->push_time1)); if (ogg->push_time0 == ogg->push_time1) { best = ogg->push_offset0; } else { segment_bitrate = gst_util_uint64_scale (ogg->push_offset1 - ogg->push_offset0, 8 * GST_SECOND, ogg->push_time1 - ogg->push_time0); GST_DEBUG_OBJECT (ogg, "Local bitrate on the %" GST_TIME_FORMAT " - %" GST_TIME_FORMAT " segment: %" G_GINT64_FORMAT, GST_TIME_ARGS (ogg->push_time0), GST_TIME_ARGS (ogg->push_time1), segment_bitrate); best = ogg->push_offset0 + gst_util_uint64_scale (ogg->push_seek_time_target - ogg->push_time0, segment_bitrate, 8 * GST_SECOND); if (seek_quality < 0.5f && ogg->seek_secant) { gint64 new_best, best2 = (ogg->push_offset0 + ogg->push_offset1) / 2; /* if dire result, give as much as 25% weight to a dumb bisection guess */ float secant_weight = 1.0f - ((0.5 - seek_quality) / 0.5f) * 0.25; new_best = (best * secant_weight + best2 * (1.0f - secant_weight)); GST_DEBUG_OBJECT (ogg, "Secant says %" G_GINT64_FORMAT ", straight is %" G_GINT64_FORMAT ", new best %" G_GINT64_FORMAT " with secant_weight %f", best, best2, new_best, secant_weight); best = new_best; ogg->seek_secant = FALSE; } else { ogg->seek_secant = TRUE; } } } GST_DEBUG_OBJECT (ogg, "Raw best guess: %" G_GINT64_FORMAT, best); /* offset the guess down as we need to capture the start of the page we are targetting - but only do so if we did not undershoot last time, as we're likely to still do this time */ if (!ogg->seek_undershot) { /* very small packets are packed on pages, so offset by at least a value which is likely to get us at least one page where the packet starts */ skew = ogg->max_packet_size > ogg->max_page_size ? ogg->max_packet_size : ogg->max_page_size; GST_DEBUG_OBJECT (ogg, "Offsetting by %" G_GINT64_FORMAT, skew); best -= skew; } /* do not seek too close to the bounds, as we stop seeking when we get to within max_packet_size before the target */ if (best > ogg->push_offset1 - ogg->max_packet_size) { best = ogg->push_offset1 - ogg->max_packet_size; GST_DEBUG_OBJECT (ogg, "Too close to high bound, pushing back to %" G_GINT64_FORMAT, best); } else if (best < ogg->push_offset0 + ogg->max_packet_size) { best = ogg->push_offset0 + ogg->max_packet_size; GST_DEBUG_OBJECT (ogg, "Too close to low bound, pushing forth to %" G_GINT64_FORMAT, best); } /* keep within bounds */ if (best > ogg->push_offset1) best = ogg->push_offset1; if (best < ogg->push_offset0) best = ogg->push_offset0; GST_DEBUG_OBJECT (ogg, "Choosing target %" G_GINT64_FORMAT, best); return best; } static void gst_ogg_demux_record_keyframe_time (GstOggDemux * ogg, GstOggPad * pad, ogg_int64_t granpos) { gint64 kf_granule; GstClockTime kf_time; kf_granule = gst_ogg_stream_granulepos_to_key_granule (&pad->map, granpos); kf_time = gst_ogg_stream_granule_to_time (&pad->map, kf_granule); pad->push_kf_time = kf_time; } /* returns the earliest keyframe time for all non sparse pads in the chain, * if known, and GST_CLOCK_TIME_NONE if not */ static GstClockTime gst_ogg_demux_get_earliest_keyframe_time (GstOggDemux * ogg) { GstClockTime t = GST_CLOCK_TIME_NONE; GstOggChain *chain = ogg->building_chain; int i; if (!chain) { GST_WARNING_OBJECT (ogg, "No chain!"); return GST_CLOCK_TIME_NONE; } for (i = 0; i < chain->streams->len; i++) { GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i); if (pad->map.is_sparse) continue; if (pad->push_kf_time == GST_CLOCK_TIME_NONE) return GST_CLOCK_TIME_NONE; if (t == GST_CLOCK_TIME_NONE || pad->push_kf_time < t) t = pad->push_kf_time; } return t; } /* MUST be called with the push lock locked, and will unlock it regardless of return value. */ static GstFlowReturn gst_ogg_demux_seek_back_after_push_duration_check_unlock (GstOggDemux * ogg) { GstEvent *event; /* Get the delayed event, if any */ event = ogg->push_mode_seek_delayed_event; ogg->push_mode_seek_delayed_event = NULL; ogg->push_state = PUSH_PLAYING; GST_PUSH_UNLOCK (ogg); if (event) { /* If there is one, perform it */ gst_ogg_demux_perform_seek_push (ogg, event); } else { /* If there wasn't, seek back at start to start normal playback */ GST_INFO_OBJECT (ogg, "Seeking back to 0 after duration check"); event = gst_event_new_seek (1.0, GST_FORMAT_BYTES, GST_SEEK_FLAG_ACCURATE | GST_SEEK_FLAG_FLUSH, GST_SEEK_TYPE_SET, 1, GST_SEEK_TYPE_SET, GST_CLOCK_TIME_NONE); if (!gst_pad_push_event (ogg->sinkpad, event)) { GST_WARNING_OBJECT (ogg, "Failed seeking back to start"); return GST_FLOW_ERROR; } } return GST_FLOW_OK; } static float gst_ogg_demux_estimate_seek_quality (GstOggDemux * ogg) { gint64 diff; /* how far from the goal we ended up */ gint64 dist; /* how far we moved this iteration */ float seek_quality; if (ogg->push_prev_seek_time == GST_CLOCK_TIME_NONE) { /* for the first seek, we pretend we got a good seek, as we don't have a previous seek yet */ return 1.0f; } /* We take a guess at how good the last seek was at guessing the byte target by comparing the amplitude of the last seek to the error */ diff = ogg->push_seek_time_target - ogg->push_last_seek_time; if (diff < 0) diff = -diff; dist = ogg->push_last_seek_time - ogg->push_prev_seek_time; if (dist < 0) dist = -dist; seek_quality = (dist == 0) ? 0.0f : 1.0f / (1.0f + diff / (float) dist); GST_DEBUG_OBJECT (ogg, "We moved %" GST_TIME_FORMAT ", we're off by %" GST_TIME_FORMAT ", seek quality %f", GST_TIME_ARGS (dist), GST_TIME_ARGS (diff), seek_quality); return seek_quality; } static void gst_ogg_demux_update_bisection_stats (GstOggDemux * ogg) { int n; GST_INFO_OBJECT (ogg, "Bisection needed %d + %d steps", ogg->push_bisection_steps[0], ogg->push_bisection_steps[1]); for (n = 0; n < 2; ++n) { ogg->stats_bisection_steps[n] += ogg->push_bisection_steps[n]; if (ogg->stats_bisection_max_steps[n] < ogg->push_bisection_steps[n]) ogg->stats_bisection_max_steps[n] = ogg->push_bisection_steps[n]; } ogg->stats_nbisections++; GST_INFO_OBJECT (ogg, "So far, %.2f + %.2f bisections needed per seek (max %d + %d)", ogg->stats_bisection_steps[0] / (float) ogg->stats_nbisections, ogg->stats_bisection_steps[1] / (float) ogg->stats_nbisections, ogg->stats_bisection_max_steps[0], ogg->stats_bisection_max_steps[1]); } static gboolean gst_ogg_pad_handle_push_mode_state (GstOggPad * pad, ogg_page * page) { GstOggDemux *ogg = pad->ogg; ogg_int64_t granpos = ogg_page_granulepos (page); GST_PUSH_LOCK (ogg); if (granpos >= 0) { if (ogg->push_start_time == GST_CLOCK_TIME_NONE) { ogg->push_start_time = gst_ogg_stream_get_start_time_for_granulepos (&pad->map, granpos); GST_DEBUG_OBJECT (ogg, "Stream start time: %" GST_TIME_FORMAT, GST_TIME_ARGS (ogg->push_start_time)); } ogg->push_time_offset = gst_ogg_stream_get_end_time_for_granulepos (&pad->map, granpos); if (ogg->push_time_offset > 0) { GST_DEBUG_OBJECT (ogg, "Bitrate since start: %" G_GUINT64_FORMAT, gst_util_uint64_scale (ogg->push_byte_offset, 8 * GST_SECOND, ogg->push_time_offset)); } if (ogg->push_state == PUSH_DURATION) { GstClockTime t = gst_ogg_stream_get_end_time_for_granulepos (&pad->map, granpos); if (ogg->total_time == GST_CLOCK_TIME_NONE || t > ogg->total_time) { GST_DEBUG_OBJECT (ogg, "New total time: %" GST_TIME_FORMAT, GST_TIME_ARGS (t)); ogg->total_time = t; ogg->push_time_length = t; } /* If we were determining the duration of the stream, we're now done, and can get back to sending the original event we delayed. We stop a bit before the end of the stream, as if we get a EOS event and there is a queue2 upstream (such as when using playbin), it will pause the task *after* we come back from the EOS handler, so we cannot prevent the pausing by issuing a seek. */ if (ogg->push_byte_offset + EOS_AVOIDANCE_THRESHOLD >= ogg->push_byte_length) { GstMessage *message; GstFlowReturn res; /* tell the pipeline we've just found out the duration */ ogg->push_time_length = ogg->total_time; GST_INFO_OBJECT (ogg, "New duration found: %" GST_TIME_FORMAT, GST_TIME_ARGS (ogg->total_time)); message = gst_message_new_duration_changed (GST_OBJECT (ogg)); gst_element_post_message (GST_ELEMENT (ogg), message); GST_DEBUG_OBJECT (ogg, "We're close enough to the end, and we're scared " "to get too close, seeking back to start"); res = gst_ogg_demux_seek_back_after_push_duration_check_unlock (ogg); if (res != GST_FLOW_OK) return res; return GST_FLOW_SKIP_PUSH; } else { GST_PUSH_UNLOCK (ogg); } return GST_FLOW_SKIP_PUSH; } } /* if we're seeking, look at time, and decide what to do */ if (ogg->push_state != PUSH_PLAYING && ogg->push_state != PUSH_LINEAR2) { GstClockTime t; gint64 best = -1; GstEvent *sevent; int res; gboolean close_enough; float seek_quality; /* ignore -1 granpos when seeking, we want to sync on a real granpos */ if (granpos < 0) { GST_PUSH_UNLOCK (ogg); if (ogg_stream_pagein (&pad->map.stream, page) != 0) goto choked; return GST_FLOW_SKIP_PUSH; } t = gst_ogg_stream_get_end_time_for_granulepos (&pad->map, granpos); if (ogg->push_state == PUSH_BISECT1 || ogg->push_state == PUSH_BISECT2) { GstClockTime sync_time; if (pad->push_sync_time == GST_CLOCK_TIME_NONE) pad->push_sync_time = t; GST_DEBUG_OBJECT (ogg, "Got timestamp %" GST_TIME_FORMAT " for %s", GST_TIME_ARGS (t), gst_ogg_stream_get_media_type (&pad->map)); sync_time = gst_ogg_demux_collect_sync_time (ogg, ogg->building_chain); if (sync_time == GST_CLOCK_TIME_NONE) { GST_PUSH_UNLOCK (ogg); GST_DEBUG_OBJECT (ogg, "Not enough timing info collected for sync, waiting for more"); if (ogg_stream_pagein (&pad->map.stream, page) != 0) goto choked; return GST_FLOW_SKIP_PUSH; } ogg->push_last_seek_time = sync_time; GST_DEBUG_OBJECT (ogg, "Bisection just seeked at %" G_GINT64_FORMAT ", time %" GST_TIME_FORMAT ", target was %" GST_TIME_FORMAT, ogg->push_last_seek_offset, GST_TIME_ARGS (ogg->push_last_seek_time), GST_TIME_ARGS (ogg->push_seek_time_target)); if (ogg->push_time1 != GST_CLOCK_TIME_NONE) { seek_quality = gst_ogg_demux_estimate_seek_quality (ogg); GST_DEBUG_OBJECT (ogg, "Interval was %" G_GINT64_FORMAT " - %" G_GINT64_FORMAT " (%" G_GINT64_FORMAT "), time %" GST_TIME_FORMAT " - %" GST_TIME_FORMAT " (%" GST_TIME_FORMAT "), seek quality %f", ogg->push_offset0, ogg->push_offset1, ogg->push_offset1 - ogg->push_offset0, GST_TIME_ARGS (ogg->push_time0), GST_TIME_ARGS (ogg->push_time1), GST_TIME_ARGS (ogg->push_time1 - ogg->push_time0), seek_quality); } else { /* in a open ended seek, we can't do bisection, so we pretend we like our result so far */ seek_quality = 1.0f; GST_DEBUG_OBJECT (ogg, "Interval was %" G_GINT64_FORMAT " - %" G_GINT64_FORMAT " (%" G_GINT64_FORMAT "), time %" GST_TIME_FORMAT " - unknown", ogg->push_offset0, ogg->push_offset1, ogg->push_offset1 - ogg->push_offset0, GST_TIME_ARGS (ogg->push_time0)); } ogg->push_prev_seek_time = ogg->push_last_seek_time; gst_ogg_demux_setup_bisection_bounds (ogg); best = gst_ogg_demux_estimate_bisection_target (ogg, seek_quality); if (ogg->push_seek_time_target == 0) { GST_DEBUG_OBJECT (ogg, "Seeking to 0, deemed close enough"); close_enough = (ogg->push_last_seek_time == 0); } else { /* TODO: make this dependent on framerate ? */ GstClockTime time_threshold = GST_SECOND / 2; guint64 byte_threshold = (ogg->max_packet_size > 64 * 1024 ? ogg->max_packet_size : 64 * 1024); /* We want to be within half a second before the target, or before the target and half less or equal to the max packet size left to search in */ if (time_threshold > ogg->push_seek_time_target) time_threshold = ogg->push_seek_time_target; close_enough = ogg->push_last_seek_time < ogg->push_seek_time_target && (ogg->push_last_seek_time >= ogg->push_seek_time_target - time_threshold || ogg->push_offset1 <= ogg->push_offset0 + byte_threshold); GST_DEBUG_OBJECT (ogg, "testing if we're close enough: %" GST_TIME_FORMAT " <= %" GST_TIME_FORMAT " < %" GST_TIME_FORMAT ", or %" G_GUINT64_FORMAT " <= %" G_GUINT64_FORMAT " ? %s", GST_TIME_ARGS (ogg->push_seek_time_target - time_threshold), GST_TIME_ARGS (ogg->push_last_seek_time), GST_TIME_ARGS (ogg->push_seek_time_target), ogg->push_offset1 - ogg->push_offset0, byte_threshold, close_enough ? "Yes" : "No"); } if (close_enough || best == ogg->push_last_seek_offset) { if (ogg->push_state == PUSH_BISECT1) { /* we now know the time segment we'll have to search for the second bisection */ ogg->push_time0 = ogg->push_start_time; ogg->push_offset0 = 0; GST_DEBUG_OBJECT (ogg, "Seek to %" GST_TIME_FORMAT " (%lx) done, now gathering pages for all non-sparse streams", GST_TIME_ARGS (ogg->push_seek_time_target), (long) granpos); ogg->push_state = PUSH_LINEAR1; } else { /* If we're asked for an accurate seek, we'll go forward till we get to the original seek target time, else we'll just drop here at the keyframe */ if (ogg->push_seek_flags & GST_SEEK_FLAG_ACCURATE) { GST_INFO_OBJECT (ogg, "Seek to keyframe at %" GST_TIME_FORMAT " done (we're at %" GST_TIME_FORMAT "), skipping to original target (%" GST_TIME_FORMAT ")", GST_TIME_ARGS (ogg->push_seek_time_target), GST_TIME_ARGS (sync_time), GST_TIME_ARGS (ogg->push_seek_time_original_target)); ogg->push_state = PUSH_LINEAR2; } else { GST_INFO_OBJECT (ogg, "Seek to keyframe done, playing"); /* we're synced to the seek target, so flush stream and stuff any queued pages into the stream so we start decoding there */ ogg->push_state = PUSH_PLAYING; } gst_ogg_demux_update_bisection_stats (ogg); } } } else if (ogg->push_state == PUSH_LINEAR1) { if (pad->push_kf_time == GST_CLOCK_TIME_NONE) { GstClockTime earliest_keyframe_time; gst_ogg_demux_record_keyframe_time (ogg, pad, granpos); GST_DEBUG_OBJECT (ogg, "Previous keyframe for %s stream at %" GST_TIME_FORMAT, gst_ogg_stream_get_media_type (&pad->map), GST_TIME_ARGS (pad->push_kf_time)); earliest_keyframe_time = gst_ogg_demux_get_earliest_keyframe_time (ogg); if (earliest_keyframe_time != GST_CLOCK_TIME_NONE) { if (earliest_keyframe_time > ogg->push_last_seek_time) { GST_INFO_OBJECT (ogg, "All non sparse streams now have a previous keyframe time, " "and we already decoded it, switching to playing"); ogg->push_state = PUSH_PLAYING; gst_ogg_demux_update_bisection_stats (ogg); } else { GST_INFO_OBJECT (ogg, "All non sparse streams now have a previous keyframe time, " "bisecting again to %" GST_TIME_FORMAT, GST_TIME_ARGS (earliest_keyframe_time)); ogg->push_seek_time_target = earliest_keyframe_time; ogg->push_offset0 = 0; ogg->push_time0 = ogg->push_start_time; ogg->push_offset1 = ogg->push_last_seek_offset; ogg->push_time1 = ogg->push_last_seek_time; ogg->push_prev_seek_time = GST_CLOCK_TIME_NONE; ogg->seek_secant = FALSE; ogg->seek_undershot = FALSE; ogg->push_state = PUSH_BISECT2; best = gst_ogg_demux_estimate_bisection_target (ogg, 1.0f); } } } } if (ogg->push_state == PUSH_BISECT1 || ogg->push_state == PUSH_BISECT2) { gint i; ogg_sync_reset (&ogg->sync); for (i = 0; i < ogg->building_chain->streams->len; i++) { GstOggPad *pad = g_array_index (ogg->building_chain->streams, GstOggPad *, i); pad->push_sync_time = GST_CLOCK_TIME_NONE; ogg_stream_reset (&pad->map.stream); } GST_DEBUG_OBJECT (ogg, "seeking to %" G_GINT64_FORMAT " - %" G_GINT64_FORMAT, best, (gint64) - 1); /* do seek */ g_assert (best != -1); ogg->push_bisection_steps[ogg->push_state == PUSH_BISECT2 ? 1 : 0]++; sevent = gst_event_new_seek (ogg->push_seek_rate, GST_FORMAT_BYTES, ogg->push_seek_flags, GST_SEEK_TYPE_SET, best, GST_SEEK_TYPE_NONE, -1); gst_event_set_seqnum (sevent, ogg->push_seek_seqnum); GST_PUSH_UNLOCK (ogg); res = gst_pad_push_event (ogg->sinkpad, sevent); if (!res) { /* We failed to send the seek event, notify the pipeline */ GST_ELEMENT_ERROR (ogg, RESOURCE, SEEK, (NULL), ("Failed to seek")); return GST_FLOW_ERROR; } return GST_FLOW_SKIP_PUSH; } if (ogg->push_state != PUSH_PLAYING) { GST_PUSH_UNLOCK (ogg); return GST_FLOW_SKIP_PUSH; } } GST_PUSH_UNLOCK (ogg); return GST_FLOW_OK; choked: { GST_WARNING_OBJECT (ogg, "ogg stream choked on page (serial %08x), " "resetting stream", pad->map.serialno); gst_ogg_pad_reset (pad); /* we continue to recover */ return GST_FLOW_SKIP_PUSH; } } /* submit a page to an oggpad, this function will then submit all * the packets in the page. */ static GstFlowReturn gst_ogg_pad_submit_page (GstOggPad * pad, ogg_page * page) { GstFlowReturn result = GST_FLOW_OK; GstOggDemux *ogg; gboolean continued = FALSE; ogg = pad->ogg; /* for negative rates we read pages backwards and must therefore be careful * with continued pages */ if (ogg->segment.rate < 0.0) { gint npackets; continued = ogg_page_continued (page); /* number of completed packets in the page */ npackets = ogg_page_packets (page); if (!continued) { /* page is not continued so it contains at least one packet start. It's * possible that no packet ends on this page (npackets == 0). In that * case, the next (continued) page(s) we kept contain the remainder of the * packets. We mark npackets=1 to make us start decoding the pages in the * remainder of the algorithm. */ if (npackets == 0) npackets = 1; } GST_LOG_OBJECT (ogg, "continued: %d, %d packets", continued, npackets); if (npackets == 0) { GST_LOG_OBJECT (ogg, "no decodable packets, we need a previous page"); goto done; } } /* keep track of time in push mode */ if (!ogg->pullmode) { result = gst_ogg_pad_handle_push_mode_state (pad, page); if (result == GST_FLOW_SKIP_PUSH) return GST_FLOW_OK; if (result != GST_FLOW_OK) return result; } if (page->header_len + page->body_len > ogg->max_page_size) ogg->max_page_size = page->header_len + page->body_len; if (ogg_stream_pagein (&pad->map.stream, page) != 0) goto choked; /* flush all packets in the stream layer, this might not give a packet if * the page had no packets finishing on the page (npackets == 0). */ result = gst_ogg_pad_stream_out (pad, 0); if (pad->continued) { ogg_packet packet; /* now send the continued pages to the stream layer */ while (pad->continued) { ogg_page *p = (ogg_page *) pad->continued->data; GST_LOG_OBJECT (ogg, "submitting continued page %p", p); if (ogg_stream_pagein (&pad->map.stream, p) != 0) goto choked; pad->continued = g_list_delete_link (pad->continued, pad->continued); /* free the page */ gst_ogg_page_free (p); } GST_LOG_OBJECT (ogg, "flushing last continued packet"); /* flush 1 continued packet in the stream layer */ result = gst_ogg_pad_stream_out (pad, 1); /* flush all remaining packets, we pushed them in the previous round. * We don't use _reset() because we still want to get the discont when * we submit a next page. */ while (ogg_stream_packetout (&pad->map.stream, &packet) != 0); } done: /* keep continued pages (only in reverse mode) */ if (continued) { ogg_page *p = gst_ogg_page_copy (page); GST_LOG_OBJECT (ogg, "keeping continued page %p", p); pad->continued = g_list_prepend (pad->continued, p); } return result; choked: { GST_WARNING_OBJECT (ogg, "ogg stream choked on page (serial %08x), " "resetting stream", pad->map.serialno); gst_ogg_pad_reset (pad); /* we continue to recover */ return GST_FLOW_OK; } } static GstOggChain * gst_ogg_chain_new (GstOggDemux * ogg) { GstOggChain *chain = g_slice_new0 (GstOggChain); GST_DEBUG_OBJECT (ogg, "creating new chain %p", chain); chain->ogg = ogg; chain->offset = -1; chain->bytes = -1; chain->have_bos = FALSE; chain->streams = g_array_new (FALSE, TRUE, sizeof (GstOggPad *)); chain->begin_time = GST_CLOCK_TIME_NONE; chain->segment_start = GST_CLOCK_TIME_NONE; chain->segment_stop = GST_CLOCK_TIME_NONE; chain->total_time = GST_CLOCK_TIME_NONE; return chain; } static void gst_ogg_chain_free (GstOggChain * chain) { gint i; for (i = 0; i < chain->streams->len; i++) { GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i); gst_object_unref (pad); } g_array_free (chain->streams, TRUE); g_slice_free (GstOggChain, chain); } static void gst_ogg_pad_mark_discont (GstOggPad * pad) { pad->discont = TRUE; pad->map.last_size = 0; } static void gst_ogg_chain_mark_discont (GstOggChain * chain) { gint i; for (i = 0; i < chain->streams->len; i++) { GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i); gst_ogg_pad_mark_discont (pad); } } static void gst_ogg_chain_reset (GstOggChain * chain) { gint i; for (i = 0; i < chain->streams->len; i++) { GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i); gst_ogg_pad_reset (pad); } } static GstOggPad * gst_ogg_chain_new_stream (GstOggChain * chain, guint32 serialno) { GstOggPad *ret; GstTagList *list; gchar *name; GST_DEBUG_OBJECT (chain->ogg, "creating new stream %08x in chain %p", serialno, chain); name = g_strdup_printf ("src_%08x", serialno); ret = g_object_new (GST_TYPE_OGG_PAD, "name", name, NULL); g_free (name); /* we own this one */ gst_object_ref_sink (ret); GST_PAD_DIRECTION (ret) = GST_PAD_SRC; gst_ogg_pad_mark_discont (ret); ret->chain = chain; ret->ogg = chain->ogg; ret->map.serialno = serialno; if (ogg_stream_init (&ret->map.stream, serialno) != 0) goto init_failed; /* FIXME: either do something with it or remove it */ list = gst_tag_list_new_empty (); gst_tag_list_add (list, GST_TAG_MERGE_REPLACE, GST_TAG_SERIAL, serialno, NULL); gst_tag_list_unref (list); GST_DEBUG_OBJECT (chain->ogg, "created new ogg src %p for stream with serial %08x", ret, serialno); g_array_append_val (chain->streams, ret); gst_pad_set_active (GST_PAD_CAST (ret), TRUE); return ret; /* ERRORS */ init_failed: { GST_ERROR ("Could not initialize ogg_stream struct for serial %08x", serialno); gst_object_unref (ret); return NULL; } } static GstOggPad * gst_ogg_chain_get_stream (GstOggChain * chain, guint32 serialno) { gint i; for (i = 0; i < chain->streams->len; i++) { GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i); if (pad->map.serialno == serialno) return pad; } return NULL; } static gboolean gst_ogg_chain_has_stream (GstOggChain * chain, guint32 serialno) { return gst_ogg_chain_get_stream (chain, serialno) != NULL; } /* signals and args */ enum { /* FILL ME */ LAST_SIGNAL }; enum { ARG_0 /* FILL ME */ }; static GstStaticPadTemplate ogg_demux_src_template_factory = GST_STATIC_PAD_TEMPLATE ("src_%08x", GST_PAD_SRC, GST_PAD_SOMETIMES, GST_STATIC_CAPS_ANY); static GstStaticPadTemplate ogg_demux_sink_template_factory = GST_STATIC_PAD_TEMPLATE ("sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS ("application/ogg; audio/ogg; video/ogg; application/kate") ); static void gst_ogg_demux_finalize (GObject * object); static GstFlowReturn gst_ogg_demux_read_chain (GstOggDemux * ogg, GstOggChain ** chain); static GstFlowReturn gst_ogg_demux_read_end_chain (GstOggDemux * ogg, GstOggChain * chain); static gboolean gst_ogg_demux_sink_event (GstPad * pad, GstObject * parent, GstEvent * event); static void gst_ogg_demux_loop (GstOggPad * pad); static GstFlowReturn gst_ogg_demux_chain (GstPad * pad, GstObject * parent, GstBuffer * buffer); static gboolean gst_ogg_demux_sink_activate (GstPad * sinkpad, GstObject * parent); static gboolean gst_ogg_demux_sink_activate_mode (GstPad * sinkpad, GstObject * parent, GstPadMode mode, gboolean active); static GstStateChangeReturn gst_ogg_demux_change_state (GstElement * element, GstStateChange transition); static void gst_ogg_print (GstOggDemux * demux); #define gst_ogg_demux_parent_class parent_class G_DEFINE_TYPE (GstOggDemux, gst_ogg_demux, GST_TYPE_ELEMENT); static void gst_ogg_demux_class_init (GstOggDemuxClass * klass) { GstElementClass *gstelement_class = GST_ELEMENT_CLASS (klass); GObjectClass *gobject_class = G_OBJECT_CLASS (klass); gst_element_class_set_static_metadata (gstelement_class, "Ogg demuxer", "Codec/Demuxer", "demux ogg streams (info about ogg: http://xiph.org)", "Wim Taymans "); gst_element_class_add_pad_template (gstelement_class, gst_static_pad_template_get (&ogg_demux_sink_template_factory)); gst_element_class_add_pad_template (gstelement_class, gst_static_pad_template_get (&ogg_demux_src_template_factory)); gstelement_class->change_state = gst_ogg_demux_change_state; gstelement_class->send_event = gst_ogg_demux_receive_event; gobject_class->finalize = gst_ogg_demux_finalize; } static void gst_ogg_demux_init (GstOggDemux * ogg) { /* create the sink pad */ ogg->sinkpad = gst_pad_new_from_static_template (&ogg_demux_sink_template_factory, "sink"); gst_pad_set_event_function (ogg->sinkpad, gst_ogg_demux_sink_event); gst_pad_set_chain_function (ogg->sinkpad, gst_ogg_demux_chain); gst_pad_set_activate_function (ogg->sinkpad, gst_ogg_demux_sink_activate); gst_pad_set_activatemode_function (ogg->sinkpad, gst_ogg_demux_sink_activate_mode); gst_element_add_pad (GST_ELEMENT (ogg), ogg->sinkpad); g_mutex_init (&ogg->chain_lock); g_mutex_init (&ogg->push_lock); ogg->chains = g_array_new (FALSE, TRUE, sizeof (GstOggChain *)); ogg->stats_nbisections = 0; ogg->stats_bisection_steps[0] = 0; ogg->stats_bisection_steps[1] = 0; ogg->stats_bisection_max_steps[0] = 0; ogg->stats_bisection_max_steps[1] = 0; ogg->newsegment = NULL; ogg->chunk_size = CHUNKSIZE; ogg->flowcombiner = gst_flow_combiner_new (); } static void gst_ogg_demux_finalize (GObject * object) { GstOggDemux *ogg; ogg = GST_OGG_DEMUX (object); g_array_free (ogg->chains, TRUE); g_mutex_clear (&ogg->chain_lock); g_mutex_clear (&ogg->push_lock); ogg_sync_clear (&ogg->sync); if (ogg->newsegment) gst_event_unref (ogg->newsegment); gst_flow_combiner_free (ogg->flowcombiner); G_OBJECT_CLASS (parent_class)->finalize (object); } static void gst_ogg_demux_reset_streams (GstOggDemux * ogg) { GstOggChain *chain; guint i; chain = ogg->current_chain; if (chain == NULL) return; for (i = 0; i < chain->streams->len; i++) { GstOggPad *stream = g_array_index (chain->streams, GstOggPad *, i); stream->start_time = -1; stream->map.accumulated_granule = 0; } ogg->building_chain = chain; GST_DEBUG_OBJECT (ogg, "Resetting current chain"); ogg->current_chain = NULL; ogg->resync = TRUE; ogg->chunk_size = CHUNKSIZE; } static gboolean gst_ogg_demux_sink_event (GstPad * pad, GstObject * parent, GstEvent * event) { gboolean res; GstOggDemux *ogg; ogg = GST_OGG_DEMUX (parent); switch (GST_EVENT_TYPE (event)) { case GST_EVENT_FLUSH_START: res = gst_ogg_demux_send_event (ogg, event); break; case GST_EVENT_FLUSH_STOP: GST_DEBUG_OBJECT (ogg, "got a flush stop event"); ogg_sync_reset (&ogg->sync); res = gst_ogg_demux_send_event (ogg, event); if (ogg->pullmode || ogg->push_state != PUSH_DURATION) { /* it's starting to feel reaaaally dirty :( if we're on a spliced seek to get duration, don't reset streams, we'll need them for the delayed seek */ gst_ogg_demux_reset_streams (ogg); } break; case GST_EVENT_SEGMENT: GST_DEBUG_OBJECT (ogg, "got a new segment event"); { GstSegment segment; gboolean update; gst_event_copy_segment (event, &segment); if (segment.format == GST_FORMAT_BYTES) { GST_PUSH_LOCK (ogg); ogg->push_byte_offset = segment.start; ogg->push_last_seek_offset = segment.start; if (gst_event_get_seqnum (event) == ogg->push_seek_seqnum) { GstSeekType stop_type = GST_SEEK_TYPE_NONE; if (ogg->push_seek_time_original_stop != -1) stop_type = GST_SEEK_TYPE_SET; gst_segment_do_seek (&ogg->segment, ogg->push_seek_rate, GST_FORMAT_TIME, ogg->push_seek_flags, GST_SEEK_TYPE_SET, ogg->push_seek_time_original_target, stop_type, ogg->push_seek_time_original_stop, &update); } GST_PUSH_UNLOCK (ogg); } else { GST_WARNING_OBJECT (ogg, "unexpected segment format: %s", gst_format_get_name (segment.format)); } } gst_event_unref (event); res = TRUE; break; case GST_EVENT_EOS: { GST_DEBUG_OBJECT (ogg, "got an EOS event"); #if 0 /* This would be what is needed (recover from EOS by going on to the next step (issue the delayed seek)), but it does not work if there is a queue2 upstream - see more details comment in gst_ogg_pad_submit_page. If I could find a way to bypass queue2 behavior, this should be enabled. */ GST_PUSH_LOCK (ogg); if (ogg->push_state == PUSH_DURATION) { GST_DEBUG_OBJECT (ogg, "Got EOS while determining length"); res = gst_ogg_demux_seek_back_after_push_duration_check_unlock (ogg); if (res != GST_FLOW_OK) { GST_DEBUG_OBJECT (ogg, "Error seeking back after duration check: %d", res); } break; } GST_PUSH_UNLOCK (ogg); #endif res = gst_ogg_demux_send_event (ogg, event); if (ogg->current_chain == NULL) { GST_ELEMENT_ERROR (ogg, STREAM, DEMUX, (NULL), ("can't get first chain")); } break; } default: res = gst_pad_event_default (pad, parent, event); break; } return res; } /* submit the given buffer to the ogg sync */ static GstFlowReturn gst_ogg_demux_submit_buffer (GstOggDemux * 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)) goto no_buffer; gst_buffer_extract (buffer, 0, oggbuffer, size); if (G_UNLIKELY (ogg_sync_wrote (&ogg->sync, size) < 0)) goto write_failed; if (!ogg->pullmode) { GST_PUSH_LOCK (ogg); ogg->push_byte_offset += size; GST_PUSH_UNLOCK (ogg); } done: gst_buffer_unref (buffer); return ret; /* ERRORS */ no_buffer: { GST_ELEMENT_ERROR (ogg, STREAM, DECODE, (NULL), ("failed to get ogg sync buffer")); ret = GST_FLOW_ERROR; goto done; } write_failed: { GST_ELEMENT_ERROR (ogg, STREAM, DECODE, (NULL), ("failed to write %" G_GSIZE_FORMAT " bytes to the sync buffer", size)); ret = GST_FLOW_ERROR; goto done; } } /* in random access mode this code updates the current read position * and resets the ogg sync buffer so that the next read will happen * from this new location. */ static void gst_ogg_demux_seek (GstOggDemux * ogg, gint64 offset) { GST_LOG_OBJECT (ogg, "seeking to %" G_GINT64_FORMAT, offset); ogg->offset = offset; ogg->read_offset = offset; ogg_sync_reset (&ogg->sync); } /* read more data from the current offset and submit to * the ogg sync layer. */ static GstFlowReturn gst_ogg_demux_get_data (GstOggDemux * ogg, gint64 end_offset) { GstFlowReturn ret; GstBuffer *buffer; gchar *oggbuffer; gsize size; GST_LOG_OBJECT (ogg, "get data %" G_GINT64_FORMAT " %" G_GINT64_FORMAT " %" G_GINT64_FORMAT, ogg->read_offset, ogg->length, end_offset); if (end_offset > 0 && ogg->read_offset >= end_offset) goto boundary_reached; if (ogg->read_offset == ogg->length) goto eos; oggbuffer = ogg_sync_buffer (&ogg->sync, ogg->chunk_size); if (G_UNLIKELY (oggbuffer == NULL)) goto no_buffer; buffer = gst_buffer_new_wrapped_full (0, oggbuffer, ogg->chunk_size, 0, ogg->chunk_size, NULL, NULL); ret = gst_pad_pull_range (ogg->sinkpad, ogg->read_offset, ogg->chunk_size, &buffer); if (ret != GST_FLOW_OK) goto error; size = gst_buffer_get_size (buffer); if (G_UNLIKELY (ogg_sync_wrote (&ogg->sync, size) < 0)) goto write_failed; ogg->read_offset += size; gst_buffer_unref (buffer); return ret; /* ERROR */ boundary_reached: { GST_LOG_OBJECT (ogg, "reached boundary"); return GST_FLOW_LIMIT; } eos: { GST_LOG_OBJECT (ogg, "reached EOS"); return GST_FLOW_EOS; } no_buffer: { GST_ELEMENT_ERROR (ogg, STREAM, DECODE, (NULL), ("failed to get ogg sync buffer")); return GST_FLOW_ERROR; } error: { GST_WARNING_OBJECT (ogg, "got %d (%s) from pull range", ret, gst_flow_get_name (ret)); gst_buffer_unref (buffer); return ret; } write_failed: { GST_ELEMENT_ERROR (ogg, STREAM, DECODE, (NULL), ("failed to write %" G_GSIZE_FORMAT " bytes to the sync buffer", size)); gst_buffer_unref (buffer); return GST_FLOW_ERROR; } } /* Read the next page from the current offset. * boundary: number of bytes ahead we allow looking for; * -1 if no boundary * * @offset will contain the offset the next page starts at when this function * returns GST_FLOW_OK. * * GST_FLOW_EOS is returned on EOS. * * GST_FLOW_LIMIT is returned when we did not find a page before the * boundary. If @boundary is -1, this is never returned. * * Any other error returned while retrieving data from the peer is returned as * is. */ static GstFlowReturn gst_ogg_demux_get_next_page (GstOggDemux * ogg, ogg_page * og, gint64 boundary, gint64 * offset) { gint64 end_offset = -1; GstFlowReturn ret; GST_LOG_OBJECT (ogg, "get next page, current offset %" G_GINT64_FORMAT ", bytes boundary %" G_GINT64_FORMAT, ogg->offset, boundary); if (boundary >= 0) end_offset = ogg->offset + boundary; while (TRUE) { glong more; if (end_offset > 0 && ogg->offset >= end_offset) goto boundary_reached; more = ogg_sync_pageseek (&ogg->sync, og); GST_LOG_OBJECT (ogg, "pageseek gave %ld", more); if (more < 0) { /* skipped n bytes */ ogg->offset -= more; GST_LOG_OBJECT (ogg, "skipped %ld bytes, offset %" G_GINT64_FORMAT, more, ogg->offset); } else if (more == 0) { /* we need more data */ if (boundary == 0) goto boundary_reached; GST_LOG_OBJECT (ogg, "need more data"); ret = gst_ogg_demux_get_data (ogg, end_offset); if (ret != GST_FLOW_OK) break; } else { gint64 res_offset = ogg->offset; /* got a page. Return the offset at the page beginning, advance the internal offset past the page end */ if (offset) *offset = res_offset; ret = GST_FLOW_OK; ogg->offset += more; GST_LOG_OBJECT (ogg, "got page at %" G_GINT64_FORMAT ", serial %08x, end at %" G_GINT64_FORMAT ", granule %" G_GINT64_FORMAT, res_offset, ogg_page_serialno (og), ogg->offset, (gint64) ogg_page_granulepos (og)); break; } } GST_LOG_OBJECT (ogg, "returning %d", ret); return ret; /* ERRORS */ boundary_reached: { GST_LOG_OBJECT (ogg, "offset %" G_GINT64_FORMAT " >= end_offset %" G_GINT64_FORMAT, ogg->offset, end_offset); return GST_FLOW_LIMIT; } } /* from the current offset, find the previous page, seeking backwards * until we find the page. */ static GstFlowReturn gst_ogg_demux_get_prev_page (GstOggDemux * ogg, ogg_page * og, gint64 * offset) { GstFlowReturn ret; gint64 begin = ogg->offset; gint64 end = begin; gint64 cur_offset = -1; GST_LOG_OBJECT (ogg, "getting page before %" G_GINT64_FORMAT, begin); while (cur_offset == -1) { begin -= ogg->chunk_size; if (begin < 0) begin = 0; /* seek ogg->chunk_size back */ GST_LOG_OBJECT (ogg, "seeking back to %" G_GINT64_FORMAT, begin); gst_ogg_demux_seek (ogg, begin); /* now continue reading until we run out of data, if we find a page * start, we save it. It might not be the final page as there could be * another page after this one. */ while (ogg->offset < end) { gint64 new_offset, boundary; /* An Ogg page cannot be more than a bit less than 64 KB, so we can bound the boundary to that size when searching backwards if we haven't found a page yet. So the most we have to look at is twice the max page size, which is the worst case if we start scanning just after a large page, after which also lies a large page. */ boundary = end - ogg->offset; if (boundary > 2 * MAX_OGG_PAGE_SIZE) boundary = 2 * MAX_OGG_PAGE_SIZE; ret = gst_ogg_demux_get_next_page (ogg, og, boundary, &new_offset); /* we hit the upper limit, offset contains the last page start */ if (ret == GST_FLOW_LIMIT) { GST_LOG_OBJECT (ogg, "hit limit"); break; } /* something went wrong */ if (ret == GST_FLOW_EOS) { new_offset = 0; GST_LOG_OBJECT (ogg, "got unexpected"); /* We hit EOS. */ goto beach; } else if (ret != GST_FLOW_OK) { GST_LOG_OBJECT (ogg, "got error %d", ret); return ret; } GST_LOG_OBJECT (ogg, "found page at %" G_GINT64_FORMAT, new_offset); /* offset is next page start */ cur_offset = new_offset; } } GST_LOG_OBJECT (ogg, "found previous page at %" G_GINT64_FORMAT, cur_offset); /* we have the offset. Actually snork and hold the page now */ gst_ogg_demux_seek (ogg, cur_offset); ret = gst_ogg_demux_get_next_page (ogg, og, -1, NULL); if (ret != GST_FLOW_OK) { GST_WARNING_OBJECT (ogg, "can't get last page at %" G_GINT64_FORMAT, cur_offset); /* this shouldn't be possible */ return ret; } if (offset) *offset = cur_offset; beach: return ret; } static gboolean gst_ogg_demux_deactivate_current_chain (GstOggDemux * ogg) { gint i; GstOggChain *chain = ogg->current_chain; if (chain == NULL) return TRUE; GST_DEBUG_OBJECT (ogg, "deactivating chain %p", chain); /* send EOS on all the pads */ for (i = 0; i < chain->streams->len; i++) { GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i); GstEvent *event; if (!pad->added) continue; event = gst_event_new_eos (); gst_event_set_seqnum (event, ogg->seqnum); gst_pad_push_event (GST_PAD_CAST (pad), event); GST_DEBUG_OBJECT (ogg, "removing pad %" GST_PTR_FORMAT, pad); /* deactivate first */ gst_pad_set_active (GST_PAD_CAST (pad), FALSE); gst_flow_combiner_remove_pad (ogg->flowcombiner, GST_PAD_CAST (pad)); gst_element_remove_pad (GST_ELEMENT (ogg), GST_PAD_CAST (pad)); pad->added = FALSE; } /* if we cannot seek back to the chain, we can destroy the chain * completely */ if (!ogg->pullmode) { gst_ogg_chain_free (chain); } return TRUE; } static GstCaps * gst_ogg_demux_set_header_on_caps (GstOggDemux * ogg, GstCaps * caps, GList * headers) { GstStructure *structure; GValue array = { 0 }; GST_LOG_OBJECT (ogg, "caps: %" GST_PTR_FORMAT, caps); if (G_UNLIKELY (!caps)) return NULL; if (G_UNLIKELY (!headers)) return NULL; caps = gst_caps_make_writable (caps); structure = gst_caps_get_structure (caps, 0); g_value_init (&array, GST_TYPE_ARRAY); while (headers) { GValue value = { 0 }; GstBuffer *buffer; ogg_packet *op = headers->data; g_assert (op); buffer = gst_buffer_new_and_alloc (op->bytes); if (op->bytes) gst_buffer_fill (buffer, 0, op->packet, op->bytes); GST_BUFFER_FLAG_SET (buffer, GST_BUFFER_FLAG_HEADER); g_value_init (&value, GST_TYPE_BUFFER); gst_value_take_buffer (&value, buffer); gst_value_array_append_value (&array, &value); g_value_unset (&value); headers = headers->next; } gst_structure_take_value (structure, "streamheader", &array); GST_LOG_OBJECT (ogg, "here are the newly set caps: %" GST_PTR_FORMAT, caps); return caps; } static void gst_ogg_demux_push_queued_buffers (GstOggDemux * ogg, GstOggPad * pad) { GList *walk; /* push queued packets */ for (walk = pad->map.queued; walk; walk = g_list_next (walk)) { ogg_packet *p = walk->data; gst_ogg_demux_chain_peer (pad, p, TRUE); _ogg_packet_free (p); } /* and free the queued buffers */ g_list_free (pad->map.queued); pad->map.queued = NULL; } static gboolean gst_ogg_demux_activate_chain (GstOggDemux * ogg, GstOggChain * chain, GstEvent * event) { gint i; gint bitrate, idx_bitrate; g_return_val_if_fail (chain != NULL, FALSE); if (chain == ogg->current_chain) { if (event) gst_event_unref (event); for (i = 0; i < chain->streams->len; i++) { GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i); gst_ogg_demux_push_queued_buffers (ogg, pad); } return TRUE; } GST_DEBUG_OBJECT (ogg, "activating chain %p", chain); bitrate = idx_bitrate = 0; /* first add the pads */ for (i = 0; i < chain->streams->len; i++) { GstOggPad *pad; GstEvent *ss_event; gchar *stream_id; pad = g_array_index (chain->streams, GstOggPad *, i); if (pad->map.idx_bitrate) idx_bitrate = MAX (idx_bitrate, pad->map.idx_bitrate); bitrate += pad->map.bitrate; /* mark discont */ gst_ogg_pad_mark_discont (pad); pad->last_ret = GST_FLOW_OK; if (pad->map.is_skeleton || pad->map.is_cmml || pad->added || !pad->map.caps) continue; GST_DEBUG_OBJECT (ogg, "adding pad %" GST_PTR_FORMAT, pad); /* activate first */ gst_pad_set_active (GST_PAD_CAST (pad), TRUE); stream_id = gst_pad_create_stream_id_printf (GST_PAD (pad), GST_ELEMENT_CAST (ogg), "%08x", pad->map.serialno); ss_event = gst_pad_get_sticky_event (ogg->sinkpad, GST_EVENT_STREAM_START, 0); if (ss_event) { if (gst_event_parse_group_id (ss_event, &ogg->group_id)) ogg->have_group_id = TRUE; else ogg->have_group_id = FALSE; gst_event_unref (ss_event); } else if (!ogg->have_group_id) { ogg->have_group_id = TRUE; ogg->group_id = gst_util_group_id_next (); } ss_event = gst_event_new_stream_start (stream_id); if (ogg->have_group_id) gst_event_set_group_id (ss_event, ogg->group_id); gst_pad_push_event (GST_PAD (pad), ss_event); g_free (stream_id); /* Set headers on caps */ pad->map.caps = gst_ogg_demux_set_header_on_caps (ogg, pad->map.caps, pad->map.headers); gst_pad_set_caps (GST_PAD_CAST (pad), pad->map.caps); gst_element_add_pad (GST_ELEMENT (ogg), GST_PAD_CAST (pad)); pad->added = TRUE; gst_flow_combiner_add_pad (ogg->flowcombiner, GST_PAD_CAST (pad)); } /* prefer the index bitrate over the ones encoded in the streams */ ogg->bitrate = (idx_bitrate ? idx_bitrate : bitrate); /* after adding the new pads, remove the old pads */ gst_ogg_demux_deactivate_current_chain (ogg); GST_DEBUG_OBJECT (ogg, "Setting current chain to %p", chain); ogg->current_chain = chain; /* we are finished now */ gst_element_no_more_pads (GST_ELEMENT (ogg)); GST_DEBUG_OBJECT (ogg, "starting chain"); /* then send out any headers and queued packets */ for (i = 0; i < chain->streams->len; i++) { GList *walk; GstOggPad *pad; GstTagList *tags; pad = g_array_index (chain->streams, GstOggPad *, i); /* Skip pads that were not added, e.g. Skeleton streams */ if (!pad->added) continue; /* FIXME, must be sent from the streaming thread */ if (event) gst_pad_push_event (GST_PAD_CAST (pad), gst_event_ref (event)); /* FIXME also streaming thread */ if (pad->map.taglist) { GST_DEBUG_OBJECT (ogg, "pushing tags"); gst_pad_push_event (GST_PAD_CAST (pad), gst_event_new_tag (pad->map.taglist)); pad->map.taglist = NULL; } tags = gst_tag_list_new (GST_TAG_CONTAINER_FORMAT, "Ogg", NULL); gst_tag_list_set_scope (tags, GST_TAG_SCOPE_GLOBAL); gst_pad_push_event (GST_PAD (pad), gst_event_new_tag (tags)); GST_DEBUG_OBJECT (ogg, "pushing headers"); /* push headers */ for (walk = pad->map.headers; walk; walk = g_list_next (walk)) { ogg_packet *p = walk->data; gst_ogg_demux_chain_peer (pad, p, TRUE); } GST_DEBUG_OBJECT (ogg, "pushing queued buffers"); gst_ogg_demux_push_queued_buffers (ogg, pad); } if (event) gst_event_unref (event); return TRUE; } static gboolean do_binary_search (GstOggDemux * ogg, GstOggChain * chain, gint64 begin, gint64 end, gint64 begintime, gint64 endtime, gint64 target, gint64 * offset, gboolean only_serial_no, gint serialno) { gint64 best; GstFlowReturn ret; gint64 result = 0; best = begin; GST_DEBUG_OBJECT (ogg, "chain offset %" G_GINT64_FORMAT ", end offset %" G_GINT64_FORMAT, begin, end); GST_DEBUG_OBJECT (ogg, "chain begin time %" GST_TIME_FORMAT ", end time %" GST_TIME_FORMAT, GST_TIME_ARGS (begintime), GST_TIME_ARGS (endtime)); GST_DEBUG_OBJECT (ogg, "target %" GST_TIME_FORMAT, GST_TIME_ARGS (target)); /* perform the seek */ while (begin < end) { gint64 bisect; if ((end - begin < ogg->chunk_size) || (endtime == begintime)) { bisect = begin; } else { /* take a (pretty decent) guess, avoiding overflow */ gint64 rate = (end - begin) * GST_MSECOND / (endtime - begintime); bisect = (target - begintime) / GST_MSECOND * rate + begin - ogg->chunk_size; if (bisect <= begin) bisect = begin; GST_DEBUG_OBJECT (ogg, "Initial guess: %" G_GINT64_FORMAT, bisect); } gst_ogg_demux_seek (ogg, bisect); while (begin < end) { ogg_page og; GST_DEBUG_OBJECT (ogg, "after seek, bisect %" G_GINT64_FORMAT ", begin %" G_GINT64_FORMAT ", end %" G_GINT64_FORMAT, bisect, begin, end); ret = gst_ogg_demux_get_next_page (ogg, &og, end - ogg->offset, &result); GST_LOG_OBJECT (ogg, "looking for next page returned %" G_GINT64_FORMAT, result); if (ret == GST_FLOW_LIMIT) { /* we hit the upper limit, go back a bit */ if (bisect <= begin + 1) { end = begin; /* found it */ } else { if (bisect == 0) goto seek_error; bisect -= ogg->chunk_size; if (bisect <= begin) bisect = begin + 1; gst_ogg_demux_seek (ogg, bisect); } } else if (ret == GST_FLOW_OK) { /* found offset of next ogg page */ gint64 granulepos; GstClockTime granuletime; GstOggPad *pad; /* get the granulepos */ GST_LOG_OBJECT (ogg, "found next ogg page at %" G_GINT64_FORMAT, result); granulepos = ogg_page_granulepos (&og); if (granulepos == -1) { GST_LOG_OBJECT (ogg, "granulepos of next page is -1"); continue; } /* Avoid seeking to an incorrect granuletime by only considering the stream for which we found the earliest time */ if (only_serial_no && ogg_page_serialno (&og) != serialno) continue; /* get the stream */ pad = gst_ogg_chain_get_stream (chain, ogg_page_serialno (&og)); if (pad == NULL || pad->map.is_skeleton) continue; /* convert granulepos to time */ granuletime = gst_ogg_stream_get_end_time_for_granulepos (&pad->map, granulepos); if (granuletime < pad->start_time) continue; GST_LOG_OBJECT (ogg, "granulepos %" G_GINT64_FORMAT " maps to time %" GST_TIME_FORMAT, granulepos, GST_TIME_ARGS (granuletime)); granuletime -= pad->start_time; granuletime += chain->begin_time; GST_DEBUG_OBJECT (ogg, "found page with granule %" G_GINT64_FORMAT " and time %" GST_TIME_FORMAT, granulepos, GST_TIME_ARGS (granuletime)); if (granuletime < target) { best = result; /* raw offset of packet with granulepos */ begin = ogg->offset; /* raw offset of next page */ begintime = granuletime; bisect = begin; /* *not* begin + 1 */ } else { if (bisect <= begin + 1) { end = begin; /* found it */ } else { if (end == ogg->offset) { /* we're pretty close - we'd be stuck in */ end = result; bisect -= ogg->chunk_size; /* an endless loop otherwise. */ if (bisect <= begin) bisect = begin + 1; gst_ogg_demux_seek (ogg, bisect); } else { end = result; endtime = granuletime; break; } } } } else goto seek_error; } } GST_DEBUG_OBJECT (ogg, "seeking to %" G_GINT64_FORMAT, best); gst_ogg_demux_seek (ogg, best); *offset = best; return TRUE; /* ERRORS */ seek_error: { GST_DEBUG_OBJECT (ogg, "got a seek error"); return FALSE; } } static gboolean do_index_search (GstOggDemux * ogg, GstOggChain * chain, gint64 begin, gint64 end, gint64 begintime, gint64 endtime, gint64 target, gint64 * p_offset, gint64 * p_timestamp) { guint i; guint64 timestamp, offset; guint64 r_timestamp, r_offset; gboolean result = FALSE; target -= begintime; r_offset = -1; r_timestamp = -1; for (i = 0; i < chain->streams->len; i++) { GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i); timestamp = target; if (gst_ogg_map_search_index (&pad->map, TRUE, ×tamp, &offset)) { GST_INFO ("found %" G_GUINT64_FORMAT " at offset %" G_GUINT64_FORMAT, timestamp, offset); if (r_offset == -1 || offset < r_offset) { r_offset = offset; r_timestamp = timestamp; } result |= TRUE; } } if (p_timestamp) *p_timestamp = r_timestamp; if (p_offset) *p_offset = r_offset; return result; } /* * do seek to time @position, return FALSE or chain and TRUE */ static gboolean gst_ogg_demux_do_seek (GstOggDemux * ogg, GstSegment * segment, gboolean accurate, gboolean keyframe, GstOggChain ** rchain) { guint64 position; GstOggChain *chain = NULL; gint64 begin, end; gint64 begintime, endtime; gint64 target, keytarget; gint64 best; gint64 total; gint64 result = 0; GstFlowReturn ret; gint i, pending; gint serialno = 0; position = segment->position; /* first find the chain to search in */ total = ogg->total_time; if (ogg->chains->len == 0) goto no_chains; for (i = ogg->chains->len - 1; i >= 0; i--) { chain = g_array_index (ogg->chains, GstOggChain *, i); total -= chain->total_time; if (position >= total) break; } /* first step, locate page containing the required data */ begin = chain->offset; end = chain->end_offset; begintime = chain->begin_time; endtime = begintime + chain->total_time; target = position - total + begintime; if (!do_binary_search (ogg, chain, begin, end, begintime, endtime, target, &best, FALSE, 0)) goto seek_error; /* second step: find pages for all relevant streams. We use the * keyframe_granule to keep track of which ones we saw. If we have * seen a page for each stream we can calculate the positions of * each keyframe. * Relevant streams are defined as those streams which are not * Skeleton (which only has header pages). Discontinuous streams * such as Kate and CMML are currently excluded, as they could * cause performance issues if there are few pages in the area. * TODO: We might want to include them on a flag, if we want to * not miss a subtitle (Kate has repeat packets for this purpose, * but a stream does not have to use them). */ pending = chain->streams->len; for (i = 0; i < chain->streams->len; i++) { GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i); if (!pad) { GST_WARNING_OBJECT (ogg, "No pad at index %d", i); pending--; continue; } if (pad->map.is_skeleton) { GST_DEBUG_OBJECT (ogg, "Not finding pages for Skeleton stream %08x", pad->map.serialno); pending--; continue; } if (pad->map.is_sparse) { GST_DEBUG_OBJECT (ogg, "Not finding pages for sparse stream %08x (%s)", pad->map.serialno, gst_ogg_stream_get_media_type (&pad->map)); pending--; continue; } } GST_DEBUG_OBJECT (ogg, "find keyframes for %d/%d streams", pending, chain->streams->len); /* figure out where the keyframes are */ keytarget = target; while (TRUE) { ogg_page og; gint64 granulepos; GstOggPad *pad; GstClockTime keyframe_time, granule_time; ret = gst_ogg_demux_get_next_page (ogg, &og, end - ogg->offset, &result); GST_LOG_OBJECT (ogg, "looking for next page returned %" G_GINT64_FORMAT, result); if (ret == GST_FLOW_LIMIT) { GST_LOG_OBJECT (ogg, "reached limit"); break; } else if (ret != GST_FLOW_OK) goto seek_error; /* get the stream */ pad = gst_ogg_chain_get_stream (chain, ogg_page_serialno (&og)); if (pad == NULL) continue; if (pad->map.is_skeleton || pad->map.is_sparse) goto next; granulepos = ogg_page_granulepos (&og); if (granulepos == -1 || granulepos == 0) { GST_LOG_OBJECT (ogg, "granulepos of next page is -1"); continue; } /* in reverse we want to go past the page with the lower timestamp */ if (segment->rate < 0.0) { /* get time for this pad */ granule_time = gst_ogg_stream_get_end_time_for_granulepos (&pad->map, granulepos); GST_LOG_OBJECT (ogg, "looking at page with ts %" GST_TIME_FORMAT ", target %" GST_TIME_FORMAT, GST_TIME_ARGS (granule_time), GST_TIME_ARGS (target)); if (granule_time < target) continue; } /* we've seen this pad before */ if (pad->keyframe_granule != -1) continue; /* convert granule of this pad to the granule of the keyframe */ pad->keyframe_granule = gst_ogg_stream_granulepos_to_key_granule (&pad->map, granulepos); GST_LOG_OBJECT (ogg, "marking stream granule %" G_GINT64_FORMAT, pad->keyframe_granule); /* get time of the keyframe */ keyframe_time = gst_ogg_stream_granule_to_time (&pad->map, pad->keyframe_granule); GST_LOG_OBJECT (ogg, "stream %08x granule time %" GST_TIME_FORMAT, pad->map.serialno, GST_TIME_ARGS (keyframe_time)); /* collect smallest value */ if (keyframe_time != -1) { keyframe_time += begintime; if (keyframe_time < keytarget) { serialno = pad->map.serialno; keytarget = keyframe_time; } } next: pending--; if (pending == 0) break; } /* for negative rates we will get to the keyframe backwards */ if (segment->rate < 0.0) goto done; if (keytarget != target) { GST_LOG_OBJECT (ogg, "final seek to target %" GST_TIME_FORMAT, GST_TIME_ARGS (keytarget)); /* last step, seek to the location of the keyframe */ if (!do_binary_search (ogg, chain, begin, end, begintime, endtime, keytarget, &best, TRUE, serialno)) goto seek_error; } else { /* seek back to previous position */ GST_LOG_OBJECT (ogg, "keyframe on target"); gst_ogg_demux_seek (ogg, best); } done: if (keyframe) { if (segment->rate > 0.0) segment->time = keytarget; segment->position = keytarget - begintime; } *rchain = chain; return TRUE; no_chains: { GST_DEBUG_OBJECT (ogg, "no chains"); return FALSE; } seek_error: { GST_DEBUG_OBJECT (ogg, "got a seek error"); return FALSE; } } /* does not take ownership of the event */ static gboolean gst_ogg_demux_perform_seek_pull (GstOggDemux * ogg, GstEvent * event) { GstOggChain *chain = NULL; gboolean res; gboolean flush, accurate, keyframe; GstFormat format; gdouble rate; GstSeekFlags flags; GstSeekType start_type, stop_type; gint64 start, stop; gboolean update; guint32 seqnum; GstEvent *tevent; if (event) { GST_DEBUG_OBJECT (ogg, "seek with event"); gst_event_parse_seek (event, &rate, &format, &flags, &start_type, &start, &stop_type, &stop); /* we can only seek on time */ if (format != GST_FORMAT_TIME) { GST_DEBUG_OBJECT (ogg, "can only seek on TIME"); goto error; } seqnum = gst_event_get_seqnum (event); } else { GST_DEBUG_OBJECT (ogg, "seek without event"); flags = 0; rate = 1.0; seqnum = gst_util_seqnum_next (); } GST_DEBUG_OBJECT (ogg, "seek, rate %g", rate); flush = flags & GST_SEEK_FLAG_FLUSH; accurate = flags & GST_SEEK_FLAG_ACCURATE; keyframe = flags & GST_SEEK_FLAG_KEY_UNIT; /* first step is to unlock the streaming thread if it is * blocked in a chain call, we do this by starting the flush. because * we cannot yet hold any streaming lock, we have to protect the chains * with their own lock. */ if (flush) { gint i; tevent = gst_event_new_flush_start (); gst_event_set_seqnum (tevent, seqnum); gst_event_ref (tevent); gst_pad_push_event (ogg->sinkpad, tevent); GST_CHAIN_LOCK (ogg); for (i = 0; i < ogg->chains->len; i++) { GstOggChain *chain = g_array_index (ogg->chains, GstOggChain *, i); gint j; for (j = 0; j < chain->streams->len; j++) { GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, j); gst_event_ref (tevent); gst_pad_push_event (GST_PAD (pad), tevent); } } GST_CHAIN_UNLOCK (ogg); gst_event_unref (tevent); } else { gst_pad_pause_task (ogg->sinkpad); } /* now grab the stream lock so that streaming cannot continue, for * non flushing seeks when the element is in PAUSED this could block * forever. */ GST_PAD_STREAM_LOCK (ogg->sinkpad); if (event) { gst_segment_do_seek (&ogg->segment, rate, format, flags, start_type, start, stop_type, stop, &update); } GST_DEBUG_OBJECT (ogg, "segment positions set to %" GST_TIME_FORMAT "-%" GST_TIME_FORMAT, GST_TIME_ARGS (ogg->segment.start), GST_TIME_ARGS (ogg->segment.stop)); /* we need to stop flushing on the srcpad as we're going to use it * next. We can do this as we have the STREAM lock now. */ if (flush) { tevent = gst_event_new_flush_stop (TRUE); gst_event_set_seqnum (tevent, seqnum); gst_pad_push_event (ogg->sinkpad, tevent); } { gint i; /* reset all ogg streams now, need to do this from within the lock to * make sure the streaming thread is not messing with the stream */ for (i = 0; i < ogg->chains->len; i++) { GstOggChain *chain = g_array_index (ogg->chains, GstOggChain *, i); gst_ogg_chain_reset (chain); } } /* for reverse we will already seek accurately */ res = gst_ogg_demux_do_seek (ogg, &ogg->segment, accurate, keyframe, &chain); /* seek failed, make sure we continue the current chain */ if (!res) { GST_DEBUG_OBJECT (ogg, "seek failed"); chain = ogg->current_chain; } else { GST_DEBUG_OBJECT (ogg, "seek success"); } if (!chain) goto no_chain; /* now we have a new position, prepare for streaming again */ { GstEvent *event; gint64 stop; gint64 start; gint64 position, begin_time; GstSegment segment; /* we have to send the flush to the old chain, not the new one */ if (flush) { tevent = gst_event_new_flush_stop (TRUE); gst_event_set_seqnum (tevent, seqnum); gst_ogg_demux_send_event (ogg, tevent); } /* we need this to see how far inside the chain we need to start */ if (chain->begin_time != GST_CLOCK_TIME_NONE) begin_time = chain->begin_time; else begin_time = 0; /* segment.start gives the start over all chains, we calculate the amount * of time into this chain we need to start */ start = ogg->segment.start - begin_time; if (chain->segment_start != GST_CLOCK_TIME_NONE) start += chain->segment_start; if ((stop = ogg->segment.stop) == -1) stop = ogg->segment.duration; /* segment.stop gives the stop time over all chains, calculate the amount of * time we need to stop in this chain */ if (stop != -1) { if (stop > begin_time) stop -= begin_time; else stop = 0; stop += chain->segment_start; /* we must stop when this chain ends and switch to the next chain to play * the remainder of the segment. */ stop = MIN (stop, chain->segment_stop); } position = ogg->segment.position; if (chain->segment_start != GST_CLOCK_TIME_NONE) position += chain->segment_start; gst_segment_copy_into (&ogg->segment, &segment); /* create the segment event we are going to send out */ if (ogg->segment.rate >= 0.0) { segment.start = position; segment.stop = stop; } else { segment.start = start; segment.stop = position; } event = gst_event_new_segment (&segment); gst_event_set_seqnum (event, seqnum); if (chain != ogg->current_chain) { /* switch to different chain, send segment on new chain */ gst_ogg_demux_activate_chain (ogg, chain, event); } else { /* mark discont and send segment on current chain */ gst_ogg_chain_mark_discont (chain); /* This event should be sent from the streaming thread (sink pad task) */ if (ogg->newsegment) gst_event_unref (ogg->newsegment); ogg->newsegment = event; } /* notify start of new segment */ if (ogg->segment.flags & GST_SEEK_FLAG_SEGMENT) { GstMessage *message; message = gst_message_new_segment_start (GST_OBJECT (ogg), GST_FORMAT_TIME, ogg->segment.position); gst_message_set_seqnum (message, seqnum); gst_element_post_message (GST_ELEMENT (ogg), message); } ogg->seqnum = seqnum; /* restart our task since it might have been stopped when we did the * flush. */ gst_pad_start_task (ogg->sinkpad, (GstTaskFunction) gst_ogg_demux_loop, ogg->sinkpad, NULL); } /* streaming can continue now */ GST_PAD_STREAM_UNLOCK (ogg->sinkpad); return res; /* ERRORS */ error: { GST_DEBUG_OBJECT (ogg, "seek failed"); return FALSE; } no_chain: { GST_DEBUG_OBJECT (ogg, "no chain to seek in"); GST_PAD_STREAM_UNLOCK (ogg->sinkpad); return FALSE; } } static gboolean gst_ogg_demux_get_duration_push (GstOggDemux * ogg, int flags) { /* In push mode, we get to the end of the stream to get the duration */ gint64 position; GstEvent *sevent; gboolean res; /* A full Ogg page can be almost 64 KB. There's no guarantee that there'll be a granpos there, but it's fairly likely */ position = ogg->push_byte_length - DURATION_CHUNK_OFFSET - EOS_AVOIDANCE_THRESHOLD; if (position < 0) position = 0; GST_DEBUG_OBJECT (ogg, "Getting duration, seeking near the end, to %" G_GINT64_FORMAT, position); ogg->push_state = PUSH_DURATION; /* do not read the last byte */ sevent = gst_event_new_seek (1.0, GST_FORMAT_BYTES, flags, GST_SEEK_TYPE_SET, position, GST_SEEK_TYPE_SET, ogg->push_byte_length - 1); res = gst_pad_push_event (ogg->sinkpad, sevent); if (res) { GST_DEBUG_OBJECT (ogg, "Seek succesful"); return TRUE; } else { GST_INFO_OBJECT (ogg, "Seek failed, duration will stay unknown"); ogg->push_state = PUSH_PLAYING; ogg->push_disable_seeking = TRUE; return FALSE; } } static gboolean gst_ogg_demux_check_duration_push (GstOggDemux * ogg, GstSeekFlags flags, GstEvent * event) { if (ogg->push_byte_length < 0) { GstPad *peer; GST_DEBUG_OBJECT (ogg, "Trying to find byte/time length"); if ((peer = gst_pad_get_peer (ogg->sinkpad)) != NULL) { gint64 length; int res; res = gst_pad_query_duration (peer, GST_FORMAT_BYTES, &length); if (res && length > 0) { ogg->push_byte_length = length; GST_DEBUG_OBJECT (ogg, "File byte length %" G_GINT64_FORMAT, ogg->push_byte_length); } else { GST_DEBUG_OBJECT (ogg, "File byte length unknown, assuming live"); ogg->push_disable_seeking = TRUE; gst_object_unref (peer); return TRUE; } res = gst_pad_query_duration (peer, GST_FORMAT_TIME, &length); gst_object_unref (peer); if (res && length >= 0) { ogg->push_time_length = length; GST_DEBUG_OBJECT (ogg, "File time length %" GST_TIME_FORMAT, GST_TIME_ARGS (ogg->push_time_length)); } else if (!ogg->push_disable_seeking) { gboolean res; res = gst_ogg_demux_get_duration_push (ogg, flags); if (res) { GST_DEBUG_OBJECT (ogg, "File time length unknown, trying to determine"); ogg->push_mode_seek_delayed_event = NULL; if (event) { GST_DEBUG_OBJECT (ogg, "Let me intercept this innocent looking seek request"); ogg->push_mode_seek_delayed_event = gst_event_copy (event); } return FALSE; } } } } return TRUE; } static gboolean gst_ogg_demux_perform_seek_push (GstOggDemux * ogg, GstEvent * event) { gint bitrate; gboolean res = TRUE; GstFormat format; gdouble rate; GstSeekFlags flags; GstSeekType start_type, stop_type; gint64 start, stop; GstEvent *sevent; GstOggChain *chain; gint64 best, best_time; gint i; GST_DEBUG_OBJECT (ogg, "Push mode seek request received"); gst_event_parse_seek (event, &rate, &format, &flags, &start_type, &start, &stop_type, &stop); if (format != GST_FORMAT_TIME) { GST_DEBUG_OBJECT (ogg, "can only seek on TIME"); goto error; } if (start_type != GST_SEEK_TYPE_SET) { GST_DEBUG_OBJECT (ogg, "can only seek to a SET target"); goto error; } /* If stop is unset, make sure it is -1, as this value will be tested later to check whether stop is set or not */ if (stop_type == GST_SEEK_TYPE_NONE) stop = -1; if (!(flags & GST_SEEK_FLAG_FLUSH)) { GST_DEBUG_OBJECT (ogg, "can only do flushing seeks"); goto error; } GST_DEBUG_OBJECT (ogg, "Push mode seek request: %" GST_TIME_FORMAT, GST_TIME_ARGS (start)); chain = ogg->current_chain; if (!chain) { GST_WARNING_OBJECT (ogg, "No chain to seek on"); goto error; } /* start accessing push_* members */ GST_PUSH_LOCK (ogg); /* not if we disabled seeking (chained streams) */ if (ogg->push_disable_seeking) { GST_DEBUG_OBJECT (ogg, "Seeking disabled"); goto error_locked; } /* not when we're trying to work out duration */ if (ogg->push_state == PUSH_DURATION) { GST_DEBUG_OBJECT (ogg, "Busy working out duration, try again later"); goto error_locked; } /* actually, not if we're doing any seeking already */ if (ogg->push_state != PUSH_PLAYING) { GST_DEBUG_OBJECT (ogg, "Already doing some seeking, try again later"); goto error_locked; } /* on the first seek, get length if we can */ if (!gst_ogg_demux_check_duration_push (ogg, flags, event)) { GST_PUSH_UNLOCK (ogg); return FALSE; } if (do_index_search (ogg, chain, 0, -1, 0, -1, start, &best, &best_time)) { /* the index gave some result */ GST_DEBUG_OBJECT (ogg, "found offset %" G_GINT64_FORMAT " with time %" G_GUINT64_FORMAT, best, best_time); } else { if (ogg->push_time_length > 0) { /* if we know the time length, we know the full segment bitrate */ GST_DEBUG_OBJECT (ogg, "Using real file bitrate"); bitrate = gst_util_uint64_scale (ogg->push_byte_length, 8 * GST_SECOND, ogg->push_time_length); } else if (ogg->push_time_offset > 0) { /* get a first approximation using known bitrate to the current position */ GST_DEBUG_OBJECT (ogg, "Using file bitrate so far"); bitrate = gst_util_uint64_scale (ogg->push_byte_offset, 8 * GST_SECOND, ogg->push_time_offset); } else if (ogg->bitrate > 0) { /* nominal bitrate is better than nothing, even if it lies often */ GST_DEBUG_OBJECT (ogg, "Using nominal bitrate"); bitrate = ogg->bitrate; } else { /* meh */ GST_DEBUG_OBJECT (ogg, "At stream start, and no nominal bitrate, using some random magic " "number to seed"); /* the bisection, once started, should give us a better approximation */ bitrate = 1000; } best = gst_util_uint64_scale (start, bitrate, 8 * GST_SECOND); } /* offset by typical page length, and ensure our best guess is within reasonable bounds */ best -= ogg->chunk_size; if (best < 0) best = 0; if (ogg->push_byte_length > 0 && best >= ogg->push_byte_length) best = ogg->push_byte_length - 1; /* set up bisection search */ ogg->push_offset0 = 0; ogg->push_offset1 = ogg->push_byte_length - 1; ogg->push_time0 = ogg->push_start_time; ogg->push_time1 = ogg->push_time_length; ogg->push_seek_seqnum = gst_event_get_seqnum (event); ogg->push_seek_time_target = start; ogg->push_prev_seek_time = GST_CLOCK_TIME_NONE; ogg->push_seek_time_original_target = start; ogg->push_seek_time_original_stop = stop; ogg->push_state = PUSH_BISECT1; ogg->seek_secant = FALSE; ogg->seek_undershot = FALSE; /* reset pad push mode seeking state */ for (i = 0; i < chain->streams->len; i++) { GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i); pad->push_kf_time = GST_CLOCK_TIME_NONE; pad->push_sync_time = GST_CLOCK_TIME_NONE; } GST_DEBUG_OBJECT (ogg, "Setting up bisection search for %" G_GINT64_FORMAT " - %" G_GINT64_FORMAT " (time %" GST_TIME_FORMAT " - %" GST_TIME_FORMAT ")", ogg->push_offset0, ogg->push_offset1, GST_TIME_ARGS (ogg->push_time0), GST_TIME_ARGS (ogg->push_time1)); GST_DEBUG_OBJECT (ogg, "Target time is %" GST_TIME_FORMAT ", best first guess is %" G_GINT64_FORMAT, GST_TIME_ARGS (ogg->push_seek_time_target), best); ogg->push_seek_rate = rate; ogg->push_seek_flags = flags; ogg->push_mode_seek_delayed_event = NULL; ogg->push_bisection_steps[0] = 1; ogg->push_bisection_steps[1] = 0; sevent = gst_event_new_seek (rate, GST_FORMAT_BYTES, flags, start_type, best, GST_SEEK_TYPE_NONE, -1); gst_event_set_seqnum (sevent, gst_event_get_seqnum (event)); GST_PUSH_UNLOCK (ogg); res = gst_pad_push_event (ogg->sinkpad, sevent); return res; /* ERRORS */ error: { GST_DEBUG_OBJECT (ogg, "seek failed"); return FALSE; } error_locked: GST_PUSH_UNLOCK (ogg); goto error; } static gboolean gst_ogg_demux_perform_seek (GstOggDemux * ogg, GstEvent * event) { gboolean res; if (ogg->pullmode) { res = gst_ogg_demux_perform_seek_pull (ogg, event); } else { res = gst_ogg_demux_perform_seek_push (ogg, event); } return res; } /* finds each bitstream link one at a time using a bisection search * (has to begin by knowing the offset of the lb's initial page). * Recurses for each link so it can alloc the link storage after * finding them all, then unroll and fill the cache at the same time */ static GstFlowReturn gst_ogg_demux_bisect_forward_serialno (GstOggDemux * ogg, gint64 begin, gint64 searched, gint64 end, GstOggChain * chain, glong m) { gint64 endsearched = end; gint64 next = end; ogg_page og; GstFlowReturn ret; gint64 offset; GstOggChain *nextchain; GST_LOG_OBJECT (ogg, "bisect begin: %" G_GINT64_FORMAT ", searched: %" G_GINT64_FORMAT ", end %" G_GINT64_FORMAT ", chain: %p", begin, searched, end, chain); /* the below guards against garbage seperating the last and * first pages of two links. */ while (searched < endsearched) { gint64 bisect; if (endsearched - searched < ogg->chunk_size) { bisect = searched; } else { bisect = (searched + endsearched) / 2; } gst_ogg_demux_seek (ogg, bisect); ret = gst_ogg_demux_get_next_page (ogg, &og, -1, &offset); if (ret == GST_FLOW_EOS) { endsearched = bisect; } else if (ret == GST_FLOW_OK) { guint32 serial = ogg_page_serialno (&og); if (!gst_ogg_chain_has_stream (chain, serial)) { endsearched = bisect; next = offset; } else { searched = offset + og.header_len + og.body_len; } } else return ret; } GST_LOG_OBJECT (ogg, "current chain ends at %" G_GINT64_FORMAT, searched); chain->end_offset = searched; ret = gst_ogg_demux_read_end_chain (ogg, chain); if (ret != GST_FLOW_OK) return ret; GST_LOG_OBJECT (ogg, "found begin at %" G_GINT64_FORMAT, next); gst_ogg_demux_seek (ogg, next); ret = gst_ogg_demux_read_chain (ogg, &nextchain); if (ret == GST_FLOW_EOS) { nextchain = NULL; ret = GST_FLOW_OK; GST_LOG_OBJECT (ogg, "no next chain"); } else if (ret != GST_FLOW_OK) goto done; if (searched < end && nextchain != NULL) { ret = gst_ogg_demux_bisect_forward_serialno (ogg, next, ogg->offset, end, nextchain, m + 1); if (ret != GST_FLOW_OK) goto done; } GST_LOG_OBJECT (ogg, "adding chain %p", chain); g_array_insert_val (ogg->chains, 0, chain); done: return ret; } /* read a chain from the ogg file. This code will * read all BOS pages and will create and return a GstOggChain * structure with the results. * * This function will also read N pages from each stream in the * chain and submit them to the internal ogg stream parser/mapper * until we know the timestamp of the first page in the chain. */ static GstFlowReturn gst_ogg_demux_read_chain (GstOggDemux * ogg, GstOggChain ** res_chain) { GstFlowReturn ret; GstOggChain *chain = NULL; gint64 offset = ogg->offset; ogg_page og; gboolean done; gint i; GST_LOG_OBJECT (ogg, "reading chain at %" G_GINT64_FORMAT, offset); /* first read the BOS pages, detect the stream types, create the internal * stream mappers, send data to them. */ while (TRUE) { GstOggPad *pad; guint32 serial; ret = gst_ogg_demux_get_next_page (ogg, &og, -1, NULL); if (ret != GST_FLOW_OK) { if (ret == GST_FLOW_EOS) { GST_DEBUG_OBJECT (ogg, "Reached EOS, done reading end chain"); } else { GST_WARNING_OBJECT (ogg, "problem reading BOS page: ret=%d", ret); } break; } if (!ogg_page_bos (&og)) { GST_INFO_OBJECT (ogg, "page is not BOS page, all streams identified"); /* if we did not find a chain yet, assume this is a bogus stream and * ignore it */ if (!chain) { GST_WARNING_OBJECT (ogg, "No chain found, no Ogg data in stream ?"); ret = GST_FLOW_EOS; } break; } if (chain == NULL) { chain = gst_ogg_chain_new (ogg); chain->offset = offset; } serial = ogg_page_serialno (&og); if (gst_ogg_chain_get_stream (chain, serial) != NULL) { GST_WARNING_OBJECT (ogg, "found serial %08x BOS page twice, ignoring", serial); continue; } pad = gst_ogg_chain_new_stream (chain, serial); gst_ogg_pad_submit_page (pad, &og); } if (ret != GST_FLOW_OK || chain == NULL) { if (ret == GST_FLOW_OK) { GST_WARNING_OBJECT (ogg, "no chain was found"); ret = GST_FLOW_ERROR; } else if (ret != GST_FLOW_EOS) { GST_WARNING_OBJECT (ogg, "failed to read chain"); } else { GST_DEBUG_OBJECT (ogg, "done reading chains"); } if (chain) { gst_ogg_chain_free (chain); } if (res_chain) *res_chain = NULL; return ret; } chain->have_bos = TRUE; GST_INFO_OBJECT (ogg, "read bos pages, "); /* now read pages until each ogg stream mapper has figured out the * timestamp of the first packet in the chain */ /* save the offset to the first non bos page in the chain: if searching for * pad->first_time we read past the end of the chain, we'll seek back to this * position */ offset = ogg->offset; done = FALSE; while (!done) { guint32 serial; gboolean known_serial = FALSE; GstFlowReturn ret; serial = ogg_page_serialno (&og); done = TRUE; for (i = 0; i < chain->streams->len; i++) { GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i); GST_LOG_OBJECT (ogg, "serial %08x time %" GST_TIME_FORMAT, pad->map.serialno, GST_TIME_ARGS (pad->start_time)); if (pad->map.serialno == serial) { known_serial = TRUE; /* submit the page now, this will fill in the start_time when the * internal stream mapper finds it */ gst_ogg_pad_submit_page (pad, &og); if (!pad->map.is_skeleton && pad->start_time == -1 && ogg_page_eos (&og)) { /* got EOS on a pad before we could find its start_time. * We have no chance of finding a start_time for every pad so * stop searching for the other start_time(s). */ done = TRUE; break; } } /* the timestamp will be filled in when we submit the pages */ if (!pad->map.is_sparse) done &= (pad->start_time != GST_CLOCK_TIME_NONE); GST_LOG_OBJECT (ogg, "done %08x now %d", pad->map.serialno, done); } /* we read a page not belonging to the current chain: seek back to the * beginning of the chain */ if (!known_serial) { GST_LOG_OBJECT (ogg, "unknown serial %08x", serial); gst_ogg_demux_seek (ogg, offset); break; } if (!done) { ret = gst_ogg_demux_get_next_page (ogg, &og, -1, NULL); if (ret != GST_FLOW_OK) break; } } GST_LOG_OBJECT (ogg, "done reading chain"); if (res_chain) *res_chain = chain; return GST_FLOW_OK; } /* read the last pages from the ogg stream to get the final * page end_offsets. */ static GstFlowReturn gst_ogg_demux_read_end_chain (GstOggDemux * ogg, GstOggChain * chain) { gint64 begin = chain->end_offset; gint64 end = begin; gint64 last_granule = -1; GstOggPad *last_pad = NULL; GstFlowReturn ret; gboolean done = FALSE; ogg_page og; gint i; while (!done) { begin -= ogg->chunk_size; if (begin < 0) begin = 0; gst_ogg_demux_seek (ogg, begin); /* now continue reading until we run out of data, if we find a page * start, we save it. It might not be the final page as there could be * another page after this one. */ while (ogg->offset < end) { ret = gst_ogg_demux_get_next_page (ogg, &og, end - ogg->offset, NULL); if (ret == GST_FLOW_LIMIT) break; if (ret != GST_FLOW_OK) return ret; for (i = 0; i < chain->streams->len; i++) { GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i); if (pad->map.is_skeleton) continue; if (pad->map.serialno == ogg_page_serialno (&og)) { gint64 granulepos = ogg_page_granulepos (&og); if (granulepos != -1) { last_granule = granulepos; last_pad = pad; done = TRUE; } break; } } } } if (last_pad) { chain->segment_stop = gst_ogg_stream_get_end_time_for_granulepos (&last_pad->map, last_granule); } else { chain->segment_stop = GST_CLOCK_TIME_NONE; } GST_INFO ("segment stop %" G_GUINT64_FORMAT, chain->segment_stop); return GST_FLOW_OK; } /* find a pad with a given serial number */ static GstOggPad * gst_ogg_demux_find_pad (GstOggDemux * ogg, guint32 serialno) { GstOggPad *pad; gint i; /* first look in building chain if any */ if (ogg->building_chain) { pad = gst_ogg_chain_get_stream (ogg->building_chain, serialno); if (pad) return pad; } /* then look in current chain if any */ if (ogg->current_chain) { pad = gst_ogg_chain_get_stream (ogg->current_chain, serialno); if (pad) return pad; } for (i = 0; i < ogg->chains->len; i++) { GstOggChain *chain = g_array_index (ogg->chains, GstOggChain *, i); pad = gst_ogg_chain_get_stream (chain, serialno); if (pad) return pad; } return NULL; } /* find a chain with a given serial number */ static GstOggChain * gst_ogg_demux_find_chain (GstOggDemux * ogg, guint32 serialno) { GstOggPad *pad; pad = gst_ogg_demux_find_pad (ogg, serialno); if (pad) { return pad->chain; } return NULL; } /* returns TRUE if all streams have valid start time */ static gboolean gst_ogg_demux_collect_chain_info (GstOggDemux * ogg, GstOggChain * chain) { gboolean res = TRUE; chain->total_time = GST_CLOCK_TIME_NONE; GST_DEBUG_OBJECT (ogg, "trying to collect chain info"); /* see if we have a start time on all streams */ chain->segment_start = gst_ogg_demux_collect_start_time (ogg, chain); if (chain->segment_start == G_MAXUINT64) { /* not yet, stream some more data */ res = FALSE; } else if (chain->segment_stop != GST_CLOCK_TIME_NONE) { /* we can calculate a total time */ chain->total_time = chain->segment_stop - chain->segment_start; } GST_DEBUG ("total time %" G_GUINT64_FORMAT, chain->total_time); GST_DEBUG_OBJECT (ogg, "return %d", res); return res; } static void gst_ogg_demux_collect_info (GstOggDemux * ogg) { gint i; /* collect all info */ ogg->total_time = 0; for (i = 0; i < ogg->chains->len; i++) { GstOggChain *chain = g_array_index (ogg->chains, GstOggChain *, i); chain->begin_time = ogg->total_time; gst_ogg_demux_collect_chain_info (ogg, chain); ogg->total_time += chain->total_time; } ogg->segment.duration = ogg->total_time; } /* find all the chains in the ogg file, this reads the first and * last page of the ogg stream, if they match then the ogg file has * just one chain, else we do a binary search for all chains. */ static GstFlowReturn gst_ogg_demux_find_chains (GstOggDemux * ogg) { ogg_page og; GstPad *peer; gboolean res; guint32 serialno; GstOggChain *chain; GstFlowReturn ret; /* get peer to figure out length */ if ((peer = gst_pad_get_peer (ogg->sinkpad)) == NULL) goto no_peer; /* find length to read last page, we store this for later use. */ res = gst_pad_query_duration (peer, GST_FORMAT_BYTES, &ogg->length); gst_object_unref (peer); if (!res || ogg->length <= 0) goto no_length; GST_DEBUG_OBJECT (ogg, "file length %" G_GINT64_FORMAT, ogg->length); /* read chain from offset 0, this is the first chain of the * ogg file. */ gst_ogg_demux_seek (ogg, 0); ret = gst_ogg_demux_read_chain (ogg, &chain); if (ret != GST_FLOW_OK) goto no_first_chain; /* read page from end offset, we use this page to check if its serial * number is contained in the first chain. If this is the case then * this ogg is not a chained ogg and we can skip the scanning. */ gst_ogg_demux_seek (ogg, ogg->length); ret = gst_ogg_demux_get_prev_page (ogg, &og, NULL); if (ret != GST_FLOW_OK) goto no_last_page; serialno = ogg_page_serialno (&og); if (!gst_ogg_chain_has_stream (chain, serialno)) { /* the last page is not in the first stream, this means we should * find all the chains in this chained ogg. */ ret = gst_ogg_demux_bisect_forward_serialno (ogg, 0, 0, ogg->length, chain, 0); } else { /* we still call this function here but with an empty range so that * we can reuse the setup code in this routine. */ ret = gst_ogg_demux_bisect_forward_serialno (ogg, 0, ogg->length, ogg->length, chain, 0); } if (ret != GST_FLOW_OK) goto done; /* all fine, collect and print */ gst_ogg_demux_collect_info (ogg); /* dump our chains and streams */ gst_ogg_print (ogg); done: return ret; /*** error cases ***/ no_peer: { GST_ELEMENT_ERROR (ogg, STREAM, DEMUX, (NULL), ("we don't have a peer")); return GST_FLOW_NOT_LINKED; } no_length: { GST_ELEMENT_ERROR (ogg, STREAM, DEMUX, (NULL), ("can't get file length")); return GST_FLOW_NOT_SUPPORTED; } no_first_chain: { GST_ELEMENT_ERROR (ogg, STREAM, DEMUX, (NULL), ("can't get first chain")); return GST_FLOW_ERROR; } no_last_page: { GST_DEBUG_OBJECT (ogg, "can't get last page"); if (chain) gst_ogg_chain_free (chain); return ret; } } static void gst_ogg_demux_update_chunk_size (GstOggDemux * ogg, ogg_page * page) { long size = page->header_len + page->body_len; long chunk_size = size * 2; if (chunk_size > ogg->chunk_size) { GST_LOG_OBJECT (ogg, "Updating chunk size to %ld", chunk_size); ogg->chunk_size = chunk_size; } } static GstFlowReturn gst_ogg_demux_handle_page (GstOggDemux * ogg, ogg_page * page) { GstOggPad *pad; gint64 granule; guint32 serialno; GstFlowReturn result = GST_FLOW_OK; serialno = ogg_page_serialno (page); granule = ogg_page_granulepos (page); gst_ogg_demux_update_chunk_size (ogg, page); GST_LOG_OBJECT (ogg, "processing ogg page (serial %08x, " "pageno %ld, granulepos %" G_GINT64_FORMAT ", bos %d)", serialno, ogg_page_pageno (page), granule, ogg_page_bos (page)); if (ogg_page_bos (page)) { GstOggChain *chain; /* first page */ /* see if we know about the chain already */ chain = gst_ogg_demux_find_chain (ogg, serialno); if (chain) { GstEvent *event; gint64 start = 0; GstSegment segment; if (chain->segment_start != GST_CLOCK_TIME_NONE) start = chain->segment_start; /* create the newsegment event we are going to send out */ gst_segment_copy_into (&ogg->segment, &segment); segment.start = start; segment.stop = chain->segment_stop; segment.time = chain->begin_time; segment.base += chain->begin_time; event = gst_event_new_segment (&segment); gst_event_set_seqnum (event, ogg->seqnum); GST_DEBUG_OBJECT (ogg, "segment: start %" GST_TIME_FORMAT ", stop %" GST_TIME_FORMAT ", time %" GST_TIME_FORMAT, GST_TIME_ARGS (start), GST_TIME_ARGS (chain->segment_stop), GST_TIME_ARGS (chain->begin_time)); /* activate it as it means we have a non-header, this will also deactivate * the currently running chain. */ gst_ogg_demux_activate_chain (ogg, chain, event); pad = gst_ogg_demux_find_pad (ogg, serialno); } else { GstClockTime chain_time; gint64 current_time; /* this can only happen in push mode */ if (ogg->pullmode) goto unknown_chain; current_time = ogg->segment.position; /* time of new chain is current time */ chain_time = current_time; if (ogg->building_chain == NULL) { GstOggChain *newchain; newchain = gst_ogg_chain_new (ogg); newchain->offset = 0; /* set new chain begin time aligned with end time of old chain */ newchain->begin_time = chain_time; GST_DEBUG_OBJECT (ogg, "new chain, begin time %" GST_TIME_FORMAT, GST_TIME_ARGS (chain_time)); /* and this is the one we are building now */ ogg->building_chain = newchain; } pad = gst_ogg_chain_new_stream (ogg->building_chain, serialno); } } else { pad = gst_ogg_demux_find_pad (ogg, serialno); } if (pad) { result = gst_ogg_pad_submit_page (pad, page); } else { GST_PUSH_LOCK (ogg); if (!ogg->pullmode && !ogg->push_disable_seeking) { /* no pad while probing for duration, we must have a chained stream, and we don't support them, so back off */ GST_INFO_OBJECT (ogg, "We seem to have a chained stream, we won't seek"); if (ogg->push_state == PUSH_DURATION) { GstFlowReturn res; res = gst_ogg_demux_seek_back_after_push_duration_check_unlock (ogg); if (res != GST_FLOW_OK) return res; } /* only once we seeked back */ GST_PUSH_LOCK (ogg); ogg->push_disable_seeking = TRUE; } else { GST_PUSH_UNLOCK (ogg); /* no pad. This means an ogg page without bos has been seen for this * serialno. we just ignore it but post a warning... */ GST_ELEMENT_WARNING (ogg, STREAM, DECODE, (NULL), ("unknown ogg pad for serial %08x detected", serialno)); return GST_FLOW_OK; } GST_PUSH_UNLOCK (ogg); } return result; /* ERRORS */ unknown_chain: { GST_ELEMENT_ERROR (ogg, STREAM, DECODE, (NULL), ("unknown ogg chain for serial %08x detected", serialno)); return GST_FLOW_ERROR; } } /* streaming mode, receive a buffer, parse it, create pads for * the serialno, submit pages and packets to the oggpads */ static GstFlowReturn gst_ogg_demux_chain (GstPad * pad, GstObject * parent, GstBuffer * buffer) { GstOggDemux *ogg; gint ret = 0; GstFlowReturn result = GST_FLOW_OK; ogg = GST_OGG_DEMUX (parent); GST_DEBUG_OBJECT (ogg, "enter"); result = gst_ogg_demux_submit_buffer (ogg, buffer); if (result < 0) { GST_DEBUG_OBJECT (ogg, "gst_ogg_demux_submit_buffer returned %d", result); } while (result == GST_FLOW_OK) { ogg_page page; ret = ogg_sync_pageout (&ogg->sync, &page); if (ret == 0) /* need more data */ break; if (ret == -1) { /* discontinuity in the pages */ GST_DEBUG_OBJECT (ogg, "discont in page found, continuing"); } else { result = gst_ogg_demux_handle_page (ogg, &page); if (result < 0) { GST_DEBUG_OBJECT (ogg, "gst_ogg_demux_handle_page returned %d", result); } } } if (ret == 0 || result == GST_FLOW_OK) { gst_ogg_demux_sync_streams (ogg); } GST_DEBUG_OBJECT (ogg, "leave with %d", result); return result; } static gboolean gst_ogg_demux_send_event (GstOggDemux * ogg, GstEvent * event) { GstOggChain *chain = ogg->current_chain; gboolean res = TRUE; if (!chain) chain = ogg->building_chain; if (chain) { gint i; for (i = 0; i < chain->streams->len; i++) { GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i); gst_event_ref (event); GST_DEBUG_OBJECT (pad, "Pushing event %" GST_PTR_FORMAT, event); res &= gst_pad_push_event (GST_PAD (pad), event); } } else { GST_WARNING_OBJECT (ogg, "No chain to forward event to"); } gst_event_unref (event); return res; } static GstFlowReturn gst_ogg_demux_combine_flows (GstOggDemux * ogg, GstOggPad * pad, GstFlowReturn ret) { /* store the value */ pad->last_ret = ret; pad->is_eos = (ret == GST_FLOW_EOS); return gst_flow_combiner_update_flow (ogg->flowcombiner, ret); } static GstFlowReturn gst_ogg_demux_loop_forward (GstOggDemux * ogg) { GstFlowReturn ret; GstBuffer *buffer = NULL; if (ogg->offset == ogg->length) { GST_LOG_OBJECT (ogg, "no more data to pull %" G_GINT64_FORMAT " == %" G_GINT64_FORMAT, ogg->offset, ogg->length); ret = GST_FLOW_EOS; goto done; } GST_LOG_OBJECT (ogg, "pull data %" G_GINT64_FORMAT, ogg->offset); ret = gst_pad_pull_range (ogg->sinkpad, ogg->offset, ogg->chunk_size, &buffer); if (ret != GST_FLOW_OK) { GST_LOG_OBJECT (ogg, "Failed pull_range"); goto done; } ogg->offset += gst_buffer_get_size (buffer); if (G_UNLIKELY (ogg->newsegment)) { gst_ogg_demux_send_event (ogg, ogg->newsegment); ogg->newsegment = NULL; } ret = gst_ogg_demux_chain (ogg->sinkpad, GST_OBJECT_CAST (ogg), buffer); if (ret != GST_FLOW_OK && ret != GST_FLOW_EOS) { GST_LOG_OBJECT (ogg, "Failed demux_chain"); } done: return ret; } /* reverse mode. * * We read the pages backwards and send the packets forwards. The first packet * in the page will be pushed with the DISCONT flag set. * * Special care has to be taken for continued pages, which we can only decode * when we have the previous page(s). */ static GstFlowReturn gst_ogg_demux_loop_reverse (GstOggDemux * ogg) { GstFlowReturn ret; ogg_page page; gint64 offset; if (ogg->offset == 0) { GST_LOG_OBJECT (ogg, "no more data to pull %" G_GINT64_FORMAT " == 0", ogg->offset); ret = GST_FLOW_EOS; goto done; } GST_LOG_OBJECT (ogg, "read page from %" G_GINT64_FORMAT, ogg->offset); ret = gst_ogg_demux_get_prev_page (ogg, &page, &offset); if (ret != GST_FLOW_OK) goto done; ogg->offset = offset; if (G_UNLIKELY (ogg->newsegment)) { gst_ogg_demux_send_event (ogg, ogg->newsegment); ogg->newsegment = NULL; } ret = gst_ogg_demux_handle_page (ogg, &page); done: return ret; } static void gst_ogg_demux_sync_streams (GstOggDemux * ogg) { GstClockTime cur; GstOggChain *chain; guint i; chain = ogg->current_chain; cur = ogg->segment.position; if (chain == NULL || cur == -1) return; for (i = 0; i < chain->streams->len; i++) { GstOggPad *stream = g_array_index (chain->streams, GstOggPad *, i); /* Theoretically, we should be doing this for all streams, but we're only * doing it for known-to-be-sparse streams at the moment in order not to * break things for wrongly-muxed streams (like we used to produce once) */ if (stream->map.is_sparse && stream->position != GST_CLOCK_TIME_NONE) { /* Does this stream lag? Random threshold of 2 seconds */ if (GST_CLOCK_DIFF (stream->position, cur) > (2 * GST_SECOND)) { GST_DEBUG_OBJECT (stream, "synchronizing stream with others by " "advancing time from %" GST_TIME_FORMAT " to %" GST_TIME_FORMAT, GST_TIME_ARGS (stream->position), GST_TIME_ARGS (cur)); stream->position = cur; gst_pad_push_event (GST_PAD_CAST (stream), gst_event_new_gap (stream->position, cur - stream->position)); } } } } /* random access code * * - first find all the chains and streams by scanning the file. * - then get and chain buffers, just like the streaming case. * - when seeking, we can use the chain info to perform the seek. */ static void gst_ogg_demux_loop (GstOggPad * pad) { GstOggDemux *ogg; GstFlowReturn ret; ogg = GST_OGG_DEMUX (GST_OBJECT_PARENT (pad)); if (ogg->need_chains) { gboolean res; /* this is the only place where we write chains and thus need to lock. */ GST_CHAIN_LOCK (ogg); ret = gst_ogg_demux_find_chains (ogg); GST_CHAIN_UNLOCK (ogg); if (ret != GST_FLOW_OK) goto chain_read_failed; ogg->need_chains = FALSE; GST_OBJECT_LOCK (ogg); ogg->running = TRUE; GST_OBJECT_UNLOCK (ogg); /* and seek to configured positions without FLUSH */ res = gst_ogg_demux_perform_seek_pull (ogg, NULL); if (!res) goto seek_failed; } if (ogg->segment.rate >= 0.0) ret = gst_ogg_demux_loop_forward (ogg); else ret = gst_ogg_demux_loop_reverse (ogg); if (ret != GST_FLOW_OK) goto pause; gst_ogg_demux_sync_streams (ogg); return; /* ERRORS */ chain_read_failed: { /* error was posted */ goto pause; } seek_failed: { GST_ELEMENT_ERROR (ogg, STREAM, DEMUX, (NULL), ("failed to start demuxing ogg")); ret = GST_FLOW_ERROR; goto pause; } pause: { const gchar *reason = gst_flow_get_name (ret); GstEvent *event = NULL; GST_LOG_OBJECT (ogg, "pausing task, reason %s", reason); gst_pad_pause_task (ogg->sinkpad); if (ret == GST_FLOW_EOS) { /* perform EOS logic */ if (ogg->segment.flags & GST_SEEK_FLAG_SEGMENT) { gint64 stop; GstMessage *message; /* for segment playback we need to post when (in stream time) * we stopped, this is either stop (when set) or the duration. */ if ((stop = ogg->segment.stop) == -1) stop = ogg->segment.duration; GST_LOG_OBJECT (ogg, "Sending segment done, at end of segment"); message = gst_message_new_segment_done (GST_OBJECT (ogg), GST_FORMAT_TIME, stop); gst_message_set_seqnum (message, ogg->seqnum); gst_element_post_message (GST_ELEMENT (ogg), message); event = gst_event_new_segment_done (GST_FORMAT_TIME, stop); gst_event_set_seqnum (event, ogg->seqnum); gst_ogg_demux_send_event (ogg, event); event = NULL; } else { /* normal playback, send EOS to all linked pads */ GST_LOG_OBJECT (ogg, "Sending EOS, at end of stream"); event = gst_event_new_eos (); } } else if (ret == GST_FLOW_NOT_LINKED || ret < GST_FLOW_EOS) { GST_ELEMENT_ERROR (ogg, STREAM, FAILED, (_("Internal data stream error.")), ("stream stopped, reason %s", reason)); event = gst_event_new_eos (); } /* For wrong-state we still want to pause the task and stop * but no error message or other things are necessary. * wrong-state is no real error and will be caused by flushing, * e.g. because of a flushing seek. */ if (event) { /* guard against corrupt/truncated files, where one can hit EOS before prerolling is done and a chain created. If we have no chain to send the event to, error out. */ if (ogg->current_chain || ogg->building_chain) { gst_event_set_seqnum (event, ogg->seqnum); gst_ogg_demux_send_event (ogg, event); } else { gst_event_unref (event); GST_ELEMENT_ERROR (ogg, STREAM, DEMUX, (NULL), ("EOS before finding a chain")); } } return; } } static void gst_ogg_demux_clear_chains (GstOggDemux * ogg) { gint i; gst_ogg_demux_deactivate_current_chain (ogg); GST_CHAIN_LOCK (ogg); for (i = 0; i < ogg->chains->len; i++) { GstOggChain *chain = g_array_index (ogg->chains, GstOggChain *, i); gst_ogg_chain_free (chain); } ogg->chains = g_array_set_size (ogg->chains, 0); GST_CHAIN_UNLOCK (ogg); } /* this function is called when the pad is activated and should start * processing data. * * We check if we can do random access to decide if we work push or * pull based. */ static gboolean gst_ogg_demux_sink_activate (GstPad * sinkpad, GstObject * parent) { GstQuery *query; gboolean pull_mode = FALSE; GstSchedulingFlags flags; query = gst_query_new_scheduling (); if (!gst_pad_peer_query (sinkpad, query)) { gst_query_unref (query); goto activate_push; } gst_query_parse_scheduling (query, &flags, NULL, NULL, NULL); /* Don't use pull mode if sequential access is suggested */ if (gst_query_has_scheduling_mode (query, GST_PAD_MODE_PULL)) { pull_mode = (flags & GST_SCHEDULING_FLAG_SEEKABLE) && !(flags & GST_SCHEDULING_FLAG_SEQUENTIAL); } gst_query_unref (query); if (!pull_mode) goto activate_push; GST_DEBUG_OBJECT (sinkpad, "activating pull"); return gst_pad_activate_mode (sinkpad, GST_PAD_MODE_PULL, TRUE); activate_push: { GST_DEBUG_OBJECT (sinkpad, "activating push"); return gst_pad_activate_mode (sinkpad, GST_PAD_MODE_PUSH, TRUE); } } static gboolean gst_ogg_demux_sink_activate_mode (GstPad * sinkpad, GstObject * parent, GstPadMode mode, gboolean active) { gboolean res; GstOggDemux *ogg; ogg = GST_OGG_DEMUX (parent); switch (mode) { case GST_PAD_MODE_PUSH: ogg->pullmode = FALSE; ogg->resync = FALSE; res = TRUE; break; case GST_PAD_MODE_PULL: if (active) { ogg->need_chains = TRUE; ogg->pullmode = TRUE; res = gst_pad_start_task (sinkpad, (GstTaskFunction) gst_ogg_demux_loop, sinkpad, NULL); } else { res = gst_pad_stop_task (sinkpad); } break; default: res = FALSE; break; } return res; } static GstStateChangeReturn gst_ogg_demux_change_state (GstElement * element, GstStateChange transition) { GstOggDemux *ogg; GstStateChangeReturn result = GST_STATE_CHANGE_FAILURE; ogg = GST_OGG_DEMUX (element); switch (transition) { case GST_STATE_CHANGE_NULL_TO_READY: ogg->basetime = 0; ogg_sync_init (&ogg->sync); break; case GST_STATE_CHANGE_READY_TO_PAUSED: ogg_sync_reset (&ogg->sync); ogg->running = FALSE; ogg->bitrate = 0; ogg->total_time = -1; GST_PUSH_LOCK (ogg); ogg->push_byte_offset = 0; ogg->push_byte_length = -1; ogg->push_time_length = GST_CLOCK_TIME_NONE; ogg->push_time_offset = GST_CLOCK_TIME_NONE; ogg->push_state = PUSH_PLAYING; ogg->have_group_id = FALSE; ogg->group_id = G_MAXUINT; ogg->push_disable_seeking = FALSE; if (!ogg->pullmode) { GstPad *peer; if ((peer = gst_pad_get_peer (ogg->sinkpad)) != NULL) { gint64 length = -1; if (!gst_pad_query_duration (peer, GST_FORMAT_BYTES, &length) || length <= 0) { GST_DEBUG_OBJECT (ogg, "Unable to determine stream size, assuming live, seeking disabled"); ogg->push_disable_seeking = TRUE; } gst_object_unref (peer); } } GST_PUSH_UNLOCK (ogg); gst_segment_init (&ogg->segment, GST_FORMAT_TIME); break; case GST_STATE_CHANGE_PAUSED_TO_PLAYING: break; default: break; } result = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition); switch (transition) { case GST_STATE_CHANGE_PLAYING_TO_PAUSED: break; case GST_STATE_CHANGE_PAUSED_TO_READY: gst_ogg_demux_clear_chains (ogg); GST_OBJECT_LOCK (ogg); ogg->running = FALSE; GST_OBJECT_UNLOCK (ogg); break; case GST_STATE_CHANGE_READY_TO_NULL: ogg_sync_clear (&ogg->sync); break; default: break; } return result; } gboolean gst_ogg_demux_plugin_init (GstPlugin * plugin) { GST_DEBUG_CATEGORY_INIT (gst_ogg_demux_debug, "oggdemux", 0, "ogg demuxer"); GST_DEBUG_CATEGORY_INIT (gst_ogg_demux_setup_debug, "oggdemux_setup", 0, "ogg demuxer setup stage when parsing pipeline"); #ifdef ENABLE_NLS GST_DEBUG ("binding text domain %s to locale dir %s", GETTEXT_PACKAGE, LOCALEDIR); bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); #endif return gst_element_register (plugin, "oggdemux", GST_RANK_PRIMARY, GST_TYPE_OGG_DEMUX); } /* prints all info about the element */ #undef GST_CAT_DEFAULT #define GST_CAT_DEFAULT gst_ogg_demux_setup_debug #ifdef GST_DISABLE_GST_DEBUG static void gst_ogg_print (GstOggDemux * ogg) { /* NOP */ } #else /* !GST_DISABLE_GST_DEBUG */ static void gst_ogg_print (GstOggDemux * ogg) { guint j, i; GST_INFO_OBJECT (ogg, "%u chains", ogg->chains->len); GST_INFO_OBJECT (ogg, " total time: %" GST_TIME_FORMAT, GST_TIME_ARGS (ogg->total_time)); for (i = 0; i < ogg->chains->len; i++) { GstOggChain *chain = g_array_index (ogg->chains, GstOggChain *, i); GST_INFO_OBJECT (ogg, " chain %d (%u streams):", i, chain->streams->len); GST_INFO_OBJECT (ogg, " offset: %" G_GINT64_FORMAT " - %" G_GINT64_FORMAT, chain->offset, chain->end_offset); GST_INFO_OBJECT (ogg, " begin time: %" GST_TIME_FORMAT, GST_TIME_ARGS (chain->begin_time)); GST_INFO_OBJECT (ogg, " total time: %" GST_TIME_FORMAT, GST_TIME_ARGS (chain->total_time)); GST_INFO_OBJECT (ogg, " segment start: %" GST_TIME_FORMAT, GST_TIME_ARGS (chain->segment_start)); GST_INFO_OBJECT (ogg, " segment stop: %" GST_TIME_FORMAT, GST_TIME_ARGS (chain->segment_stop)); for (j = 0; j < chain->streams->len; j++) { GstOggPad *stream = g_array_index (chain->streams, GstOggPad *, j); GST_INFO_OBJECT (ogg, " stream %08x: %s", stream->map.serialno, gst_ogg_stream_get_media_type (&stream->map)); GST_INFO_OBJECT (ogg, " start time: %" GST_TIME_FORMAT, GST_TIME_ARGS (stream->start_time)); } } } #endif /* GST_DISABLE_GST_DEBUG */