blob: 03ac00f4e7ec9ede549c9fa4a24236e69b6df37b [file] [log] [blame]
/* GStreamer
* Copyright (C) 2004 Wim Taymans <wim@fluendo.com>
*
* gstoggdemux.c: ogg stream demuxer
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
/**
* SECTION:element-oggdemux
* @see_also: <link linkend="gst-plugins-base-plugins-oggmux">oggmux</link>
*
* This element demuxes ogg files into their encoded audio and video components.
*
* <refsect2>
* <title>Example pipelines</title>
* |[
* 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.
* </refsect2>
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <string.h>
#include <gst/gst-i18n-plugin.h>
#include <gst/tag/tag.h>
#include <gst/audio/audio.h>
#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 (ogg->segment.rate > 0.0 && pad->current_granule != -1) {
pad->current_granule += duration;
if (!delta_unit) {
pad->keyframe_granule = pad->current_granule;
}
GST_DEBUG_OBJECT (ogg, "interpolating granule %" G_GUINT64_FORMAT,
pad->current_granule);
}
if (ogg->segment.rate < 0.0 && packet->granulepos == -1) {
/* negative rates, only set timestamp on the packets with a granulepos */
out_timestamp = -1;
out_duration = -1;
out_offset = -1;
out_offset_end = -1;
} else {
/* we only push buffers after we have a valid granule. This is done so that
* we nicely skip packets without a timestamp after a seek. This is ok
* because we base or seek on the packet after the page with the smaller
* timestamp. */
if (pad->current_granule == -1)
goto no_timestamp;
if (pad->map.is_ogm) {
out_timestamp = gst_ogg_stream_granule_to_time (&pad->map,
pad->current_granule);
out_duration = gst_util_uint64_scale (duration,
GST_SECOND * pad->map.granulerate_d, pad->map.granulerate_n);
} else if (pad->map.is_sparse) {
out_timestamp = gst_ogg_stream_granule_to_time (&pad->map,
pad->current_granule);
if (duration == GST_CLOCK_TIME_NONE) {
out_duration = GST_CLOCK_TIME_NONE;
} else {
out_duration = gst_util_uint64_scale (duration,
GST_SECOND * pad->map.granulerate_d, pad->map.granulerate_n);
}
} else {
/* 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 = 0;
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)
clip_start = -pad->map.granule_offset;
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)
clip_start = -pad->map.granule_offset;
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 (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,
"Adding audio clipping %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT,
clip_start, clip_end);
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 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;
}
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);
}
}
}
granule = gst_ogg_stream_granulepos_to_granule (&pad->map,
packet->granulepos);
if (granule > 0) {
GST_DEBUG_OBJECT (ogg, "%p has granulepos %" G_GINT64_FORMAT, pad, granule);
pad->current_granule = granule;
} else if (granule == 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.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. */
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; n < packets; ++n) {
int ret = ogg_stream_packetout (&os, &op);
if (ret < 0) {
GST_WARNING_OBJECT (pad, "Failed to read packets off first page");
granule = -1;
break;
}
if (ret == 0) {
GST_WARNING_OBJECT (pad,
"Short read getting %d packets off first page", packets);
granule = -1;
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 (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) {
if (ogg->push_start_time == GST_CLOCK_TIME_NONE) {
ogg->push_start_time =
gst_ogg_stream_get_start_time_for_granulepos (&pad->map, granpos);
GST_DEBUG_OBJECT (ogg, "Stream start time: %" GST_TIME_FORMAT,
GST_TIME_ARGS (ogg->push_start_time));
}
ogg->push_time_offset =
gst_ogg_stream_get_end_time_for_granulepos (&pad->map, granpos);
if (ogg->push_time_offset > 0) {
GST_DEBUG_OBJECT (ogg, "Bitrate since start: %" G_GUINT64_FORMAT,
gst_util_uint64_scale (ogg->push_byte_offset, 8 * GST_SECOND,
ogg->push_time_offset));
}
if (ogg->push_state == PUSH_DURATION) {
GstClockTime t =
gst_ogg_stream_get_end_time_for_granulepos (&pad->map, granpos);
if (ogg->total_time == GST_CLOCK_TIME_NONE || t > ogg->total_time) {
GST_DEBUG_OBJECT (ogg, "New total time: %" GST_TIME_FORMAT,
GST_TIME_ARGS (t));
ogg->total_time = t;
ogg->push_time_length = t;
}
/* If we were determining the duration of the stream, we're now done,
and can get back to sending the original event we delayed.
We stop a bit before the end of the stream, as if we get a EOS
event and there is a queue2 upstream (such as when using playbin),
it will pause the task *after* we come back from the EOS handler,
so we cannot prevent the pausing by issuing a seek. */
if (ogg->push_byte_offset >= 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 timestamp %" GST_TIME_FORMAT " for %s",
GST_TIME_ARGS (t), gst_ogg_stream_get_media_type (&pad->map));
sync_time = gst_ogg_demux_collect_sync_time (ogg, ogg->building_chain);
if (sync_time == GST_CLOCK_TIME_NONE) {
GST_PUSH_UNLOCK (ogg);
GST_DEBUG_OBJECT (ogg,
"Not enough timing info collected for sync, waiting for more");
if (ogg_stream_pagein (&pad->map.stream, page) != 0)
goto choked;
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)
{
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 <wim@fluendo.com>");
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);
ogg->chains = g_array_new (FALSE, TRUE, sizeof (GstOggChain *));
ogg->stats_nbisections = 0;
ogg->stats_bisection_steps[0] = 0;
ogg->stats_bisection_steps[1] = 0;
ogg->stats_bisection_max_steps[0] = 0;
ogg->stats_bisection_max_steps[1] = 0;
ogg->newsegment = NULL;
ogg->chunk_size = CHUNKSIZE;
ogg->flowcombiner = gst_flow_combiner_new ();
}
static void
gst_ogg_demux_finalize (GObject * object)
{
GstOggDemux *ogg;
ogg = GST_OGG_DEMUX (object);
g_array_free (ogg->chains, TRUE);
g_mutex_clear (&ogg->chain_lock);
g_mutex_clear (&ogg->push_lock);
ogg_sync_clear (&ogg->sync);
if (ogg->newsegment)
gst_event_unref (ogg->newsegment);
gst_flow_combiner_free (ogg->flowcombiner);
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:
res = gst_ogg_demux_send_event (ogg, event);
break;
case GST_EVENT_FLUSH_STOP:
GST_DEBUG_OBJECT (ogg, "got a flush stop event");
ogg_sync_reset (&ogg->sync);
res = gst_ogg_demux_send_event (ogg, event);
if (ogg->pullmode || ogg->push_state != PUSH_DURATION) {
/* it's starting to feel reaaaally dirty :(
if we're on a spliced seek to get duration, don't reset streams,
we'll need them for the delayed seek */
gst_ogg_demux_reset_streams (ogg);
}
break;
case GST_EVENT_SEGMENT:
GST_DEBUG_OBJECT (ogg, "got a new segment event");
{
GstSegment segment;
gboolean update;
gst_event_copy_segment (event, &segment);
if (segment.format == GST_FORMAT_BYTES) {
GST_PUSH_LOCK (ogg);
ogg->push_byte_offset = segment.start;
ogg->push_last_seek_offset = segment.start;
if (gst_event_get_seqnum (event) == ogg->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);
}
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->seek_event_drop_till);
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;
}
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 (