/* 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 * @title: oggdemux * @see_also: oggmux * * This element demuxes ogg files into their encoded audio and video components. * * ## Example pipelines * |[ * gst-launch-1.0 -v filesrc location=test.ogg ! oggdemux ! vorbisdec ! audioconvert ! audioresample ! autoaudiosink * ]| * Decodes a vorbis audio stream stored inside an ogg container and plays it. * */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #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 (128*1024) /* 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 SEEK_GIVE_UP_THRESHOLD (3*GST_SECOND) #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->prev_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 = ogg->push_time_length; if (stop == -1) stop = ogg->total_time; } } } 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->prev_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; guint64 clip_start = 0, clip_end = 0; 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 we get a hole at start, it might be we're catching a stream * partway through. In that case, if the stream has an index, the * index might be mooted. However, as it's totally valid to index * a stream with a hole at start (ie, capturing a live stream and * then index it), we now check whether the index references some * offset beyond the byte length (if known). If this is the case, * we can be reasonably sure we're getting a stream partway, with * its index being now useless since we don't know how many bytes * were skipped, preventing us from patching the index offsets to * match the hole size. */ if (!is_header && ogg->check_index_overflow) { GstQuery *query; GstFormat format; int i; gint64 length; gboolean beyond; if (ogg->current_chain) { query = gst_query_new_duration (GST_FORMAT_BYTES); if (gst_pad_peer_query (ogg->sinkpad, query)) { gst_query_parse_duration (query, &format, &length); if (format == GST_FORMAT_BYTES && length >= 0) { for (i = 0; i < ogg->current_chain->streams->len; i++) { GstOggPad *ipad = g_array_index (ogg->current_chain->streams, GstOggPad *, i); if (!ipad->map.index) continue; beyond = ipad->map.n_index && ipad->map.index[ipad->map.n_index - 1].offset >= length; if (beyond) { GST_WARNING_OBJECT (pad, "Index offsets beyong byte length"); if (ipad->discont) { /* hole - the index is most likely screwed up */ GST_WARNING_OBJECT (ogg, "Discarding entire index"); g_free (ipad->map.index); ipad->map.index = NULL; ipad->map.n_index = 0; } else { /* no hole - we can just clip the index if needed */ GST_WARNING_OBJECT (ogg, "Clipping index"); while (ipad->map.n_index > 0 && ipad->map.index[ipad->map.n_index - 1].offset >= length) ipad->map.n_index--; if (ipad->map.n_index == 0) { GST_WARNING_OBJECT (ogg, "The entire index was clipped"); g_free (ipad->map.index); ipad->map.index = NULL; } } /* We can't trust the total time if the index goes beyond */ ipad->map.total_time = -1; } else { /* use total time to update the total ogg time */ if (ogg->total_time == -1) { ogg->total_time = ipad->map.total_time; } else if (ipad->map.total_time > 0) { ogg->total_time = MAX (ogg->total_time, ipad->map.total_time); } } } } } gst_query_unref (query); } ogg->check_index_overflow = FALSE; } 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 (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 && pad->current_granule == -1) { /* negative rates, allow output of packets with no timestamp, let downstream reconstruct */ out_timestamp = -1; out_duration = -1; out_offset = -1; out_offset_end = -1; pad->prev_granule = -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 our seek on the packet after the page with the smaller * timestamp. */ if (pad->current_granule == -1) { pad->prev_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 { /* The last packet may be clipped. This will be represented by the last granule being smaller than what it would otherwise have been, had no content been clipped. In that case, we cannot calculate the PTS of the audio from the packet length and granule. */ if (packet->e_o_s) { if (pad->prev_granule >= 0) out_timestamp = gst_ogg_stream_granule_to_time (&pad->map, pad->prev_granule); else out_timestamp = GST_CLOCK_TIME_NONE; if (pad->map.audio_clipping && pad->current_granule < pad->prev_granule + duration) { clip_end = pad->prev_granule + duration - pad->current_granule; } if (pad->map.audio_clipping && pad->current_granule - duration < -pad->map.granule_offset) { if (pad->current_granule >= -pad->map.granule_offset) { guint64 already_removed = pad->current_granule > duration ? pad->current_granule - duration : 0; clip_start = already_removed > -pad->map.granule_offset ? 0 : -pad->map.granule_offset - already_removed; } else clip_start = pad->current_granule; } } else { out_timestamp = gst_ogg_stream_granule_to_time (&pad->map, pad->current_granule - duration); if (pad->map.audio_clipping && pad->current_granule - duration < -pad->map.granule_offset) { if (pad->current_granule >= -pad->map.granule_offset) { guint64 already_removed = pad->current_granule > duration ? pad->current_granule - duration : 0; clip_start = already_removed > -pad->map.granule_offset ? 0 : -pad->map.granule_offset - already_removed; } else clip_start = pad->current_granule; } } 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); } pad->prev_granule = pad->current_granule; } if (G_UNLIKELY (offset + trim > packet->bytes)) goto invalid_packet; else 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); if (pad->map.audio_clipping && (clip_start || clip_end)) { GST_DEBUG_OBJECT (pad, "Clipping %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " (%" G_GUINT64_FORMAT " / %" G_GUINT64_FORMAT ")", clip_start, clip_end, clip_start + clip_end, duration); gst_buffer_add_audio_clipping_meta (buf, GST_FORMAT_DEFAULT, clip_start, clip_end); } /* 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); if (ogg->segment.rate < 0.0 || GST_BUFFER_TIMESTAMP_IS_VALID (buf)) pad->discont = FALSE; } /* 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) { GST_LOG_OBJECT (ogg, "Pushing buf %" GST_PTR_FORMAT, buf); 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 " (%" G_GINT64_FORMAT ")", GST_TIME_ARGS (current_time), current_time); pad->position = ogg->segment.position; /* 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; } invalid_packet: { GST_DEBUG_OBJECT (ogg, "Skipping invalid 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); ogg->check_index_overflow = TRUE; break; default: break; } } else { GST_WARNING_OBJECT (pad->ogg, "found skeleton fisbone for an unknown stream 0x%08x", serialno); } } } GST_DEBUG_OBJECT (ogg, "%p packet has granulepos %" G_GINT64_FORMAT, pad, packet->granulepos); granule = gst_ogg_stream_granulepos_to_granule (&pad->map, packet->granulepos); if (granule > 0) { GST_DEBUG_OBJECT (ogg, "%p has granule %" G_GINT64_FORMAT, pad, granule); pad->current_granule = granule; } else if (granule == 0) { /* headers */ } 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 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->current_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.position = ogg->push_seek_time_original_target; segment.stop = ogg->push_seek_time_original_stop; segment.time = ogg->push_seek_time_original_target; segment.base = ogg->segment.base; event = gst_event_new_segment (&segment); ogg->push_state = PUSH_PLAYING; } else { segment.rate = ogg->segment.rate; segment.applied_rate = ogg->segment.applied_rate; segment.start = start_time; segment.position = start_time; segment.stop = chain->segment_stop; segment.time = segment_time; segment.base = ogg->segment.base; 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.position = chain->segment_start; segment.stop = chain->segment_stop; segment.time = chain->begin_time; segment.base = ogg->segment.base + segment.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.granulepos < -1) { GST_WARNING_OBJECT (ogg, "Invalid granulepos (%" G_GINT64_FORMAT "), resetting stream", packet.granulepos); gst_ogg_pad_reset (pad); break; } 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_first_granule (GstOggDemux * ogg, GstOggPad * pad, ogg_page * page) { /* When we submit a page, we check if we have started tracking granules. * If not, we calculate the granule corresponding to the first packet * on the page. */ gboolean valid_granule = TRUE; if (pad->current_granule == -1) { ogg_int64_t granpos = ogg_page_granulepos (page); if (granpos > 0) { gint64 granule = (gint64) gst_ogg_stream_granulepos_to_granule (&pad->map, granpos); gint64 duration; int packets = ogg_page_packets (page), n; GST_DEBUG_OBJECT (pad, "This page completes %d packets, granule %" G_GINT64_FORMAT, packets, granule); if (packets > 0) { ogg_stream_state os; ogg_packet op; int last_size = pad->map.last_size; memcpy (&os, &pad->map.stream, sizeof (os)); for (n = 0; valid_granule && n < packets; ++n) { int ret = ogg_stream_packetout (&os, &op); if (ret < 0) { /* This usually means a continued packet after a seek and we can't calc the first granule, * but sometimes not - so if it's ret == -1 and first packet, try again */ if (ret == -1 && n == 0) { n--; continue; } GST_DEBUG_OBJECT (pad, "Failed to read packet off first page"); valid_granule = FALSE; break; } if (ret == 0) { GST_WARNING_OBJECT (pad, "Short read getting %d packets off first page", packets); valid_granule = FALSE; break; } duration = gst_ogg_stream_get_packet_duration (&pad->map, &op); GST_DEBUG_OBJECT (pad, "Packet %d has duration %" G_GINT64_FORMAT, n, duration); granule -= duration; } pad->map.last_size = last_size; if (valid_granule) { if (granule >= 0) { pad->current_granule = granule; GST_INFO_OBJECT (pad, "Starting with first granule %" G_GINT64_FORMAT, granule); } else { pad->current_granule = 0; GST_INFO_OBJECT (pad, "Extrapolated first granule is negative, " "used to clip samples at start"); } } } else { GST_WARNING_OBJECT (pad, "Ogg page finishing no packets, but a valid granule"); } } } } 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; /* if we haven't learnt about the total time yet, disable seeking */ if (ogg->total_time == -1) ogg->push_disable_seeking = TRUE; ogg->push_state = PUSH_PLAYING; /* If there is one, perform it. Otherwise, seek back at start to start * normal playback */ if (!event) { 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); } gst_event_replace (&ogg->seek_event, event); gst_event_unref (event); GST_PUSH_UNLOCK (ogg); g_mutex_lock (&ogg->seek_event_mutex); g_cond_broadcast (&ogg->seek_event_cond); g_mutex_unlock (&ogg->seek_event_mutex); return GST_FLOW_OK; } static float gst_ogg_demux_estimate_seek_quality (GstOggDemux * ogg) { GstClockTimeDiff diff; /* how far from the goal we ended up */ GstClockTimeDiff 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 = GST_CLOCK_DIFF (ogg->push_seek_time_target, ogg->push_last_seek_time); if (diff < 0) diff = -diff; dist = GST_CLOCK_DIFF (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_STIME_FORMAT ", we're off by %" GST_STIME_FORMAT ", seek quality %f", GST_STIME_ARGS (dist), GST_STIME_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 && pad->have_type) { 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're still receiving data from before the seek segment, drop it */ if (ogg->seek_event_drop_till != 0) { GST_PUSH_UNLOCK (ogg); return GST_FLOW_SKIP_PUSH; } /* 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 >= 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; 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; if (pad->current_granule == -1) gst_ogg_demux_setup_first_granule (ogg, pad, page); 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 PTS %" 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; if (pad->current_granule == -1) gst_ogg_demux_setup_first_granule (ogg, pad, page); 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->seqnum); gst_event_replace (&ogg->seek_event, sevent); gst_event_unref (sevent); GST_PUSH_UNLOCK (ogg); g_mutex_lock (&ogg->seek_event_mutex); g_cond_broadcast (&ogg->seek_event_cond); g_mutex_unlock (&ogg->seek_event_mutex); 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; } } static void gst_ogg_demux_query_duration_push (GstOggDemux * ogg) { if (!ogg->pullmode && ogg->push_byte_length == -1) { GstQuery *query; gboolean seekable = FALSE; query = gst_query_new_seeking (GST_FORMAT_BYTES); if (gst_pad_peer_query (ogg->sinkpad, query)) gst_query_parse_seeking (query, NULL, &seekable, NULL, NULL); gst_query_unref (query); if (seekable) { gint64 length = -1; if (!gst_element_query_duration (GST_ELEMENT (ogg), GST_FORMAT_BYTES, &length) || length <= 0) { GST_DEBUG_OBJECT (ogg, "Unable to determine stream size, assuming live, seeking disabled"); ogg->push_disable_seeking = TRUE; } else { ogg->push_disable_seeking = FALSE; } } else { GST_DEBUG_OBJECT (ogg, "Stream is not seekable, seeking disabled"); ogg->push_disable_seeking = TRUE; } } } /* 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; } } gst_ogg_demux_query_duration_push (ogg); /* 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; if (pad->current_granule == -1) gst_ogg_demux_setup_first_granule (ogg, pad, page); /* 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) { GST_LOG_OBJECT (pad, "Marking discont on 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; 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; 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_static_pad_template (gstelement_class, &ogg_demux_sink_template_factory); gst_element_class_add_static_pad_template (gstelement_class, &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); g_mutex_init (&ogg->seek_event_mutex); g_cond_init (&ogg->seek_event_cond); g_cond_init (&ogg->thread_started_cond); 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->seqnum = GST_SEQNUM_INVALID; 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); g_cond_clear (&ogg->seek_event_cond); g_cond_clear (&ogg->thread_started_cond); g_mutex_clear (&ogg->seek_event_mutex); ogg_sync_clear (&ogg->sync); if (ogg->newsegment) gst_event_unref (ogg->newsegment); gst_flow_combiner_free (ogg->flowcombiner); if (ogg->building_chain) gst_ogg_chain_free (ogg->building_chain); 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; stream->current_granule = -1; stream->keyframe_granule = -1; } ogg->building_chain = chain; GST_DEBUG_OBJECT (ogg, "Resetting current chain"); ogg->current_chain = NULL; ogg->resync = TRUE; gst_ogg_chain_mark_discont (chain); 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: if (ogg->seqnum != GST_SEQNUM_INVALID) { event = gst_event_make_writable (event); gst_event_set_seqnum (event, ogg->seqnum); } 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); if (ogg->seqnum != GST_SEQNUM_INVALID) { event = gst_event_make_writable (event); gst_event_set_seqnum (event, ogg->seqnum); } 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->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); } else if (ogg->seqnum == GST_SEQNUM_INVALID) { ogg->seqnum = GST_EVENT_SEQNUM (event); } if (!ogg->pullmode && !(ogg->push_seek_flags & GST_SEEK_FLAG_FLUSH)) { int i; GstOggChain *chain = ogg->current_chain; ogg->push_seek_flags = 0; if (!chain) { /* This will happen when we bisect, as we clear the chain when we do the first seek. On subsequent ones, we just reset the ogg sync object as we already reset the chain */ GST_DEBUG_OBJECT (ogg, "No chain, just resetting ogg sync"); ogg_sync_reset (&ogg->sync); } else { /* 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; } ogg_sync_reset (&ogg->sync); gst_ogg_demux_reset_streams (ogg); } } if (!ogg->pullmode) { if (ogg->seek_event_drop_till == gst_event_get_seqnum (event)) { GST_DEBUG_OBJECT (ogg, "Got event seqnum %u, stopping dropping (ogg->seqnum:%u)", ogg->seek_event_drop_till, ogg->seqnum); ogg->seek_event_drop_till = 0; } } 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"); 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; } else GST_PUSH_UNLOCK (ogg); res = gst_ogg_demux_send_event (ogg, event); if (ogg->current_chain == NULL) { GST_WARNING_OBJECT (ogg, "EOS while trying to retrieve chain, seeking disabled"); ogg->push_disable_seeking = TRUE; res = TRUE; } 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) { if (ogg->building_chain == chain) ogg->building_chain = NULL; ogg->current_chain = NULL; 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 caps; 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 PTS %" 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; gboolean found_keyframe = FALSE; GstClockTime ts, first_ts = GST_CLOCK_TIME_NONE; 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; } /* We have a valid granpos, and we bail out when the time since the first seen time to the time corresponding to this granpos is larger then a threshold, to guard against some streams having large holes (eg, a stream ending early, which would cause seeking after that to fill up a queue for streams still active). */ ts = gst_ogg_stream_get_end_time_for_granulepos (&pad->map, granulepos); if (GST_CLOCK_TIME_IS_VALID (ts)) { if (first_ts == GST_CLOCK_TIME_NONE) { GST_WARNING_OBJECT (pad, "Locking on pts %" GST_TIME_FORMAT, GST_TIME_ARGS (ts)); first_ts = ts; } if (ts - first_ts > SEEK_GIVE_UP_THRESHOLD) { GST_WARNING_OBJECT (pad, "No data found for %" GST_TIME_FORMAT ", giving up", GST_TIME_ARGS (SEEK_GIVE_UP_THRESHOLD)); found_keyframe = FALSE; keytarget = target; break; } } /* 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); /* Convert to stream time */ granule_time -= pad->start_time; granule_time += chain->begin_time; GST_LOG_OBJECT (ogg, "looking at page with time %" 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 keyframe granule PTS %" GST_TIME_FORMAT " target %" GST_TIME_FORMAT, pad->map.serialno, GST_TIME_ARGS (keyframe_time), GST_TIME_ARGS (keytarget)); /* collect smallest value */ if (keyframe_time != -1) { keyframe_time -= pad->start_time; keyframe_time += begintime; if (keyframe_time < keytarget) { serialno = pad->map.serialno; keytarget = keyframe_time; found_keyframe = TRUE; GST_LOG_OBJECT (ogg, "storing keytarget %" GST_TIME_FORMAT, GST_TIME_ARGS (keytarget)); } } next: pending--; if (pending == 0) break; } /* for negative rates we will get to the keyframe backwards */ if (segment->rate < 0.0) goto done; /* No keyframe found, no need to bisect again, keytarget == target here */ if (!found_keyframe) best = 0; 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; /* 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; 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); gst_event_replace (&ogg->seek_event, sevent); gst_event_unref (sevent); g_mutex_lock (&ogg->seek_event_mutex); g_cond_broadcast (&ogg->seek_event_cond); g_mutex_unlock (&ogg->seek_event_mutex); return TRUE; } 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; 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->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; if (flags & GST_SEEK_FLAG_FLUSH) { /* 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_event_replace (&ogg->seek_event, sevent); gst_event_unref (sevent); GST_PUSH_UNLOCK (ogg); g_mutex_lock (&ogg->seek_event_mutex); g_cond_broadcast (&ogg->seek_event_cond); g_mutex_unlock (&ogg->seek_event_mutex); 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 ", for last granule %" G_GUINT64_FORMAT, chain->segment_stop, last_granule); 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) { if (ret == GST_FLOW_FLUSHING) goto flushing; else 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; } flushing: { GST_DEBUG_OBJECT (ogg, "Flushing, can't read chain"); return GST_FLOW_FLUSHING; } } 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, gboolean discont) { 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) { /* Reset granule interpolation if chaining in reverse (discont = TRUE) */ if (discont) pad->current_granule = -1; 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); /* Call to function above unlocks, relock */ GST_PUSH_LOCK (ogg); if (res != GST_FLOW_OK) return res; } /* only once we seeked back */ 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; gboolean drop; ogg = GST_OGG_DEMUX (parent); GST_PUSH_LOCK (ogg); drop = (ogg->seek_event_drop_till > 0); GST_PUSH_UNLOCK (ogg); if (drop) { GST_DEBUG_OBJECT (ogg, "Dropping buffer because we have a pending seek"); gst_buffer_unref (buffer); return GST_FLOW_OK; } 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, FALSE); 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 event_sent = FALSE; 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); if (pad->added) event_sent = TRUE; } } gst_event_unref (event); if (!event_sent && GST_EVENT_TYPE (event) == GST_EVENT_EOS) { GST_ELEMENT_ERROR (ogg, STREAM, DEMUX, (NULL), ("EOS before finding a chain")); } 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_pad_flow (ogg->flowcombiner, GST_PAD_CAST (pad), 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; } GST_LOG_OBJECT (ogg, "Handling page at offset %" G_GINT64_FORMAT, ogg->offset); ret = gst_ogg_demux_handle_page (ogg, &page, TRUE); 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, so we're doing * it, but it might break things 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: { gboolean flushing; GST_OBJECT_LOCK (pad); flushing = GST_PAD_IS_FLUSHING (pad); GST_OBJECT_UNLOCK (pad); if (flushing) { ret = GST_FLOW_FLUSHING; } else { GST_ELEMENT_FLOW_ERROR (ogg, ret); 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_FLOW_ERROR (ogg, ret); 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; } } /* The sink pad task function for push mode. * It just sends any seek events queued by the streaming thread. */ static gpointer gst_ogg_demux_loop_push (GstOggDemux * ogg) { GstEvent *event = NULL; g_mutex_lock (&ogg->seek_event_mutex); /* Inform other threads that we started */ ogg->seek_thread_started = TRUE; g_cond_broadcast (&ogg->thread_started_cond); while (!ogg->seek_event_thread_stop) { while (!ogg->seek_event_thread_stop) { GST_PUSH_LOCK (ogg); event = ogg->seek_event; ogg->seek_event = NULL; if (event) ogg->seek_event_drop_till = gst_event_get_seqnum (event); GST_PUSH_UNLOCK (ogg); if (event) break; g_cond_wait (&ogg->seek_event_cond, &ogg->seek_event_mutex); } if (ogg->seek_event_thread_stop) { break; } g_assert (event); g_mutex_unlock (&ogg->seek_event_mutex); GST_DEBUG_OBJECT (ogg->sinkpad, "Pushing event %" GST_PTR_FORMAT, event); if (!gst_pad_push_event (ogg->sinkpad, event)) { GST_WARNING_OBJECT (ogg, "Failed to push event"); GST_PUSH_LOCK (ogg); if (!ogg->pullmode) { ogg->push_state = PUSH_PLAYING; ogg->push_disable_seeking = TRUE; } GST_PUSH_UNLOCK (ogg); } else { GST_DEBUG_OBJECT (ogg->sinkpad, "Pushed event ok"); } g_mutex_lock (&ogg->seek_event_mutex); } g_mutex_unlock (&ogg->seek_event_mutex); gst_object_unref (ogg); return NULL; } 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); if (chain == ogg->current_chain) ogg->current_chain = NULL; if (chain == ogg->building_chain) ogg->building_chain = NULL; gst_ogg_chain_free (chain); } ogg->chains = g_array_set_size (ogg->chains, 0); if (ogg->current_chain != NULL) { GST_FIXME_OBJECT (ogg, "current chain was tracked in existing chains !"); gst_ogg_chain_free (ogg->current_chain); ogg->current_chain = NULL; } if (ogg->building_chain != NULL) { GST_FIXME_OBJECT (ogg, "building chain was tracked in existing chains !"); gst_ogg_chain_free (ogg->building_chain); ogg->building_chain = NULL; } 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; query = gst_query_new_scheduling (); if (!gst_pad_peer_query (sinkpad, query)) { gst_query_unref (query); goto activate_push; } pull_mode = gst_query_has_scheduling_mode_with_flags (query, GST_PAD_MODE_PULL, GST_SCHEDULING_FLAG_SEEKABLE); 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; if (active) { ogg->seek_event_thread_stop = FALSE; ogg->seek_thread_started = FALSE; ogg->seek_event_thread = g_thread_new ("seek_event_thread", (GThreadFunc) gst_ogg_demux_loop_push, gst_object_ref (ogg)); /* And wait for the thread to start. * FIXME : This is hackish. And one wonders why we need a separate thread to * seek to a certain offset */ g_mutex_lock (&ogg->seek_event_mutex); while (!ogg->seek_thread_started) { g_cond_wait (&ogg->thread_started_cond, &ogg->seek_event_mutex); } g_mutex_unlock (&ogg->seek_event_mutex); } else { g_mutex_lock (&ogg->seek_event_mutex); ogg->seek_event_thread_stop = TRUE; g_cond_broadcast (&ogg->seek_event_cond); g_mutex_unlock (&ogg->seek_event_mutex); g_thread_join (ogg->seek_event_thread); ogg->seek_event_thread = NULL; } 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->seqnum = GST_SEQNUM_INVALID; ogg->push_disable_seeking = FALSE; gst_ogg_demux_query_duration_push (ogg); GST_PUSH_UNLOCK (ogg); gst_segment_init (&ogg->segment, GST_FORMAT_TIME); 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 */