mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-30 04:00:37 +00:00
5162 lines
154 KiB
C
5162 lines
154 KiB
C
/* 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 (64*1024)
|
|
|
|
/* stop duration checks within this much of EOS */
|
|
#define EOS_AVOIDANCE_THRESHOLD 8192
|
|
|
|
/* An Ogg page can not be larger than 255 segments of 255 bytes, plus
|
|
26 bytes of header */
|
|
#define MAX_OGG_PAGE_SIZE (255 * 255 + 26)
|
|
|
|
#define GST_FLOW_LIMIT GST_FLOW_CUSTOM_ERROR
|
|
#define GST_FLOW_SKIP_PUSH GST_FLOW_CUSTOM_SUCCESS_1
|
|
|
|
#define 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 = -1; /* we've no clue, sadly, without seeking */
|
|
}
|
|
}
|
|
}
|
|
|
|
gst_query_set_seeking (query, GST_FORMAT_TIME, seekable, 0, stop);
|
|
GST_CHAIN_UNLOCK (ogg);
|
|
} else {
|
|
res = FALSE;
|
|
}
|
|
break;
|
|
}
|
|
case GST_QUERY_SEGMENT:{
|
|
GstFormat format;
|
|
gint64 start, stop;
|
|
|
|
format = ogg->segment.format;
|
|
|
|
start =
|
|
gst_segment_to_stream_time (&ogg->segment, format,
|
|
ogg->segment.start);
|
|
if ((stop = ogg->segment.stop) == -1)
|
|
stop = ogg->segment.duration;
|
|
else
|
|
stop = gst_segment_to_stream_time (&ogg->segment, format, stop);
|
|
|
|
gst_query_set_segment (query, ogg->segment.rate, format, start, stop);
|
|
res = TRUE;
|
|
break;
|
|
}
|
|
default:
|
|
res = gst_pad_query_default (pad, parent, query);
|
|
break;
|
|
}
|
|
done:
|
|
|
|
return res;
|
|
|
|
/* ERRORS */
|
|
wrong_format:
|
|
{
|
|
GST_DEBUG_OBJECT (ogg, "only query duration on TIME is supported");
|
|
res = FALSE;
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
gst_ogg_demux_receive_event (GstElement * element, GstEvent * event)
|
|
{
|
|
gboolean res;
|
|
GstOggDemux *ogg;
|
|
|
|
ogg = GST_OGG_DEMUX (element);
|
|
|
|
switch (GST_EVENT_TYPE (event)) {
|
|
case GST_EVENT_SEEK:
|
|
/* now do the seek */
|
|
res = gst_ogg_demux_perform_seek (ogg, event);
|
|
gst_event_unref (event);
|
|
break;
|
|
default:
|
|
GST_DEBUG_OBJECT (ogg, "We only handle seek events here");
|
|
goto error;
|
|
}
|
|
|
|
return res;
|
|
|
|
/* ERRORS */
|
|
error:
|
|
{
|
|
GST_DEBUG_OBJECT (ogg, "error handling event");
|
|
gst_event_unref (event);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
gst_ogg_pad_event (GstPad * pad, GstObject * parent, GstEvent * event)
|
|
{
|
|
gboolean res;
|
|
GstOggDemux *ogg;
|
|
|
|
ogg = GST_OGG_DEMUX (parent);
|
|
|
|
switch (GST_EVENT_TYPE (event)) {
|
|
case GST_EVENT_SEEK:
|
|
/* now do the seek */
|
|
res = gst_ogg_demux_perform_seek (ogg, event);
|
|
gst_event_unref (event);
|
|
break;
|
|
case GST_EVENT_RECONFIGURE:
|
|
GST_OGG_PAD (pad)->last_ret = GST_FLOW_OK;
|
|
res = gst_pad_event_default (pad, parent, event);
|
|
break;
|
|
default:
|
|
res = gst_pad_event_default (pad, parent, event);
|
|
break;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
static void
|
|
gst_ogg_pad_reset (GstOggPad * pad)
|
|
{
|
|
ogg_stream_reset (&pad->map.stream);
|
|
|
|
GST_DEBUG_OBJECT (pad, "doing reset");
|
|
|
|
/* clear continued pages */
|
|
g_list_foreach (pad->continued, (GFunc) gst_ogg_page_free, NULL);
|
|
g_list_free (pad->continued);
|
|
pad->continued = NULL;
|
|
|
|
pad->last_ret = GST_FLOW_OK;
|
|
pad->position = GST_CLOCK_TIME_NONE;
|
|
pad->current_granule = -1;
|
|
pad->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;
|
|
}
|
|
|
|
pad->position = ogg->segment.position;
|
|
|
|
/* don't push the header packets when we are asked to skip them */
|
|
if (!packet->b_o_s || push_headers) {
|
|
if (pad->last_ret == GST_FLOW_OK) {
|
|
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);
|
|
|
|
/* 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;
|
|
|
|
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 + EOS_AVOIDANCE_THRESHOLD >=
|
|
ogg->push_byte_length) {
|
|
GstMessage *message;
|
|
GstFlowReturn res;
|
|
|
|
/* tell the pipeline we've just found out the duration */
|
|
ogg->push_time_length = ogg->total_time;
|
|
GST_INFO_OBJECT (ogg, "New duration found: %" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (ogg->total_time));
|
|
message = gst_message_new_duration_changed (GST_OBJECT (ogg));
|
|
gst_element_post_message (GST_ELEMENT (ogg), message);
|
|
|
|
GST_DEBUG_OBJECT (ogg,
|
|
"We're close enough to the end, and we're scared "
|
|
"to get too close, seeking back to start");
|
|
|
|
res = gst_ogg_demux_seek_back_after_push_duration_check_unlock (ogg);
|
|
if (res != GST_FLOW_OK)
|
|
return res;
|
|
return GST_FLOW_SKIP_PUSH;
|
|
} else {
|
|
GST_PUSH_UNLOCK (ogg);
|
|
}
|
|
return GST_FLOW_SKIP_PUSH;
|
|
}
|
|
}
|
|
|
|
/* if we're seeking, look at time, and decide what to do */
|
|
if (ogg->push_state != PUSH_PLAYING && ogg->push_state != PUSH_LINEAR2) {
|
|
GstClockTime t;
|
|
gint64 best = -1;
|
|
GstEvent *sevent;
|
|
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;
|
|
}
|
|
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");
|
|
#if 0
|
|
/* This would be what is needed (recover from EOS by going on to
|
|
the next step (issue the delayed seek)), but it does not work
|
|
if there is a queue2 upstream - see more details comment in
|
|
gst_ogg_pad_submit_page.
|
|
If I could find a way to bypass queue2 behavior, this should
|
|
be enabled. */
|
|
GST_PUSH_LOCK (ogg);
|
|
if (ogg->push_state == PUSH_DURATION) {
|
|
GST_DEBUG_OBJECT (ogg, "Got EOS while determining length");
|
|
res = gst_ogg_demux_seek_back_after_push_duration_check_unlock (ogg);
|
|
if (res != GST_FLOW_OK) {
|
|
GST_DEBUG_OBJECT (ogg, "Error seeking back after duration check: %d",
|
|
res);
|
|
}
|
|
break;
|
|
}
|
|
GST_PUSH_UNLOCK (ogg);
|
|
#endif
|
|
res = gst_ogg_demux_send_event (ogg, event);
|
|
if (ogg->current_chain == NULL) {
|
|
GST_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 NULL;
|
|
|
|
caps = gst_caps_make_writable (caps);
|
|
structure = gst_caps_get_structure (caps, 0);
|
|
|
|
g_value_init (&array, GST_TYPE_ARRAY);
|
|
|
|
while (headers) {
|
|
GValue value = { 0 };
|
|
GstBuffer *buffer;
|
|
ogg_packet *op = headers->data;
|
|
g_assert (op);
|
|
buffer = gst_buffer_new_and_alloc (op->bytes);
|
|
if (op->bytes)
|
|
gst_buffer_fill (buffer, 0, op->packet, op->bytes);
|
|
GST_BUFFER_FLAG_SET (buffer, GST_BUFFER_FLAG_HEADER);
|
|
g_value_init (&value, GST_TYPE_BUFFER);
|
|
gst_value_take_buffer (&value, buffer);
|
|
gst_value_array_append_value (&array, &value);
|
|
g_value_unset (&value);
|
|
headers = headers->next;
|
|
}
|
|
|
|
gst_structure_take_value (structure, "streamheader", &array);
|
|
GST_LOG_OBJECT (ogg, "here are the newly set caps: %" GST_PTR_FORMAT, caps);
|
|
|
|
return caps;
|
|
}
|
|
|
|
static void
|
|
gst_ogg_demux_push_queued_buffers (GstOggDemux * ogg, GstOggPad * pad)
|
|
{
|
|
GList *walk;
|
|
|
|
/* push queued packets */
|
|
for (walk = pad->map.queued; walk; walk = g_list_next (walk)) {
|
|
ogg_packet *p = walk->data;
|
|
|
|
gst_ogg_demux_chain_peer (pad, p, TRUE);
|
|
_ogg_packet_free (p);
|
|
}
|
|
/* and free the queued buffers */
|
|
g_list_free (pad->map.queued);
|
|
pad->map.queued = NULL;
|
|
}
|
|
|
|
static gboolean
|
|
gst_ogg_demux_activate_chain (GstOggDemux * ogg, GstOggChain * chain,
|
|
GstEvent * event)
|
|
{
|
|
gint i;
|
|
gint bitrate, idx_bitrate;
|
|
|
|
g_return_val_if_fail (chain != NULL, FALSE);
|
|
|
|
if (chain == ogg->current_chain) {
|
|
if (event)
|
|
gst_event_unref (event);
|
|
|
|
for (i = 0; i < chain->streams->len; i++) {
|
|
GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i);
|
|
gst_ogg_demux_push_queued_buffers (ogg, pad);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
GST_DEBUG_OBJECT (ogg, "activating chain %p", chain);
|
|
|
|
bitrate = idx_bitrate = 0;
|
|
|
|
/* first add the pads */
|
|
for (i = 0; i < chain->streams->len; i++) {
|
|
GstOggPad *pad;
|
|
GstEvent *ss_event;
|
|
gchar *stream_id;
|
|
|
|
pad = g_array_index (chain->streams, GstOggPad *, i);
|
|
|
|
if (pad->map.idx_bitrate)
|
|
idx_bitrate = MAX (idx_bitrate, pad->map.idx_bitrate);
|
|
|
|
bitrate += pad->map.bitrate;
|
|
|
|
/* mark discont */
|
|
gst_ogg_pad_mark_discont (pad);
|
|
pad->last_ret = GST_FLOW_OK;
|
|
|
|
if (pad->map.is_skeleton || pad->map.is_cmml || pad->added
|
|
|| !pad->map.caps)
|
|
continue;
|
|
|
|
GST_DEBUG_OBJECT (ogg, "adding pad %" GST_PTR_FORMAT, pad);
|
|
|
|
/* activate first */
|
|
gst_pad_set_active (GST_PAD_CAST (pad), TRUE);
|
|
|
|
stream_id =
|
|
gst_pad_create_stream_id_printf (GST_PAD (pad), GST_ELEMENT_CAST (ogg),
|
|
"%08x", pad->map.serialno);
|
|
ss_event =
|
|
gst_pad_get_sticky_event (ogg->sinkpad, GST_EVENT_STREAM_START, 0);
|
|
if (ss_event) {
|
|
if (gst_event_parse_group_id (ss_event, &ogg->group_id))
|
|
ogg->have_group_id = TRUE;
|
|
else
|
|
ogg->have_group_id = FALSE;
|
|
gst_event_unref (ss_event);
|
|
} else if (!ogg->have_group_id) {
|
|
ogg->have_group_id = TRUE;
|
|
ogg->group_id = gst_util_group_id_next ();
|
|
}
|
|
ss_event = gst_event_new_stream_start (stream_id);
|
|
if (ogg->have_group_id)
|
|
gst_event_set_group_id (ss_event, ogg->group_id);
|
|
|
|
gst_pad_push_event (GST_PAD (pad), ss_event);
|
|
g_free (stream_id);
|
|
|
|
/* Set headers on caps */
|
|
pad->map.caps =
|
|
gst_ogg_demux_set_header_on_caps (ogg, pad->map.caps, pad->map.headers);
|
|
gst_pad_set_caps (GST_PAD_CAST (pad), pad->map.caps);
|
|
|
|
gst_element_add_pad (GST_ELEMENT (ogg), GST_PAD_CAST (pad));
|
|
pad->added = TRUE;
|
|
gst_flow_combiner_add_pad (ogg->flowcombiner, GST_PAD_CAST (pad));
|
|
}
|
|
/* prefer the index bitrate over the ones encoded in the streams */
|
|
ogg->bitrate = (idx_bitrate ? idx_bitrate : bitrate);
|
|
|
|
/* after adding the new pads, remove the old pads */
|
|
gst_ogg_demux_deactivate_current_chain (ogg);
|
|
|
|
GST_DEBUG_OBJECT (ogg, "Setting current chain to %p", chain);
|
|
ogg->current_chain = chain;
|
|
|
|
/* we are finished now */
|
|
gst_element_no_more_pads (GST_ELEMENT (ogg));
|
|
|
|
GST_DEBUG_OBJECT (ogg, "starting chain");
|
|
|
|
/* then send out any headers and queued packets */
|
|
for (i = 0; i < chain->streams->len; i++) {
|
|
GList *walk;
|
|
GstOggPad *pad;
|
|
GstTagList *tags;
|
|
|
|
pad = g_array_index (chain->streams, GstOggPad *, i);
|
|
|
|
/* Skip pads that were not added, e.g. Skeleton streams */
|
|
if (!pad->added)
|
|
continue;
|
|
|
|
/* FIXME, must be sent from the streaming thread */
|
|
if (event)
|
|
gst_pad_push_event (GST_PAD_CAST (pad), gst_event_ref (event));
|
|
|
|
/* FIXME also streaming thread */
|
|
if (pad->map.taglist) {
|
|
GST_DEBUG_OBJECT (ogg, "pushing tags");
|
|
gst_pad_push_event (GST_PAD_CAST (pad),
|
|
gst_event_new_tag (pad->map.taglist));
|
|
pad->map.taglist = NULL;
|
|
}
|
|
|
|
tags = gst_tag_list_new (GST_TAG_CONTAINER_FORMAT, "Ogg", NULL);
|
|
gst_tag_list_set_scope (tags, GST_TAG_SCOPE_GLOBAL);
|
|
gst_pad_push_event (GST_PAD (pad), gst_event_new_tag (tags));
|
|
|
|
GST_DEBUG_OBJECT (ogg, "pushing headers");
|
|
/* push headers */
|
|
for (walk = pad->map.headers; walk; walk = g_list_next (walk)) {
|
|
ogg_packet *p = walk->data;
|
|
|
|
gst_ogg_demux_chain_peer (pad, p, TRUE);
|
|
}
|
|
|
|
GST_DEBUG_OBJECT (ogg, "pushing queued buffers");
|
|
gst_ogg_demux_push_queued_buffers (ogg, pad);
|
|
}
|
|
|
|
if (event)
|
|
gst_event_unref (event);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
do_binary_search (GstOggDemux * ogg, GstOggChain * chain, gint64 begin,
|
|
gint64 end, gint64 begintime, gint64 endtime, gint64 target,
|
|
gint64 * offset, gboolean only_serial_no, gint serialno)
|
|
{
|
|
gint64 best;
|
|
GstFlowReturn ret;
|
|
gint64 result = 0;
|
|
|
|
best = begin;
|
|
|
|
GST_DEBUG_OBJECT (ogg,
|
|
"chain offset %" G_GINT64_FORMAT ", end offset %" G_GINT64_FORMAT,
|
|
begin, end);
|
|
GST_DEBUG_OBJECT (ogg,
|
|
"chain begin time %" GST_TIME_FORMAT ", end time %" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (begintime), GST_TIME_ARGS (endtime));
|
|
GST_DEBUG_OBJECT (ogg, "target %" GST_TIME_FORMAT, GST_TIME_ARGS (target));
|
|
|
|
/* perform the seek */
|
|
while (begin < end) {
|
|
gint64 bisect;
|
|
|
|
if ((end - begin < ogg->chunk_size) || (endtime == begintime)) {
|
|
bisect = begin;
|
|
} else {
|
|
/* take a (pretty decent) guess, avoiding overflow */
|
|
gint64 rate = (end - begin) * GST_MSECOND / (endtime - begintime);
|
|
|
|
bisect =
|
|
(target - begintime) / GST_MSECOND * rate + begin - ogg->chunk_size;
|
|
|
|
if (bisect <= begin)
|
|
bisect = begin;
|
|
GST_DEBUG_OBJECT (ogg, "Initial guess: %" G_GINT64_FORMAT, bisect);
|
|
}
|
|
gst_ogg_demux_seek (ogg, bisect);
|
|
|
|
while (begin < end) {
|
|
ogg_page og;
|
|
|
|
GST_DEBUG_OBJECT (ogg,
|
|
"after seek, bisect %" G_GINT64_FORMAT ", begin %" G_GINT64_FORMAT
|
|
", end %" G_GINT64_FORMAT, bisect, begin, end);
|
|
|
|
ret = gst_ogg_demux_get_next_page (ogg, &og, end - ogg->offset, &result);
|
|
GST_LOG_OBJECT (ogg, "looking for next page returned %" G_GINT64_FORMAT,
|
|
result);
|
|
|
|
if (ret == GST_FLOW_LIMIT) {
|
|
/* we hit the upper limit, go back a bit */
|
|
if (bisect <= begin + 1) {
|
|
end = begin; /* found it */
|
|
} else {
|
|
if (bisect == 0)
|
|
goto seek_error;
|
|
|
|
bisect -= ogg->chunk_size;
|
|
if (bisect <= begin)
|
|
bisect = begin + 1;
|
|
|
|
gst_ogg_demux_seek (ogg, bisect);
|
|
}
|
|
} else if (ret == GST_FLOW_OK) {
|
|
/* found offset of next ogg page */
|
|
gint64 granulepos;
|
|
GstClockTime granuletime;
|
|
GstOggPad *pad;
|
|
|
|
/* get the granulepos */
|
|
GST_LOG_OBJECT (ogg, "found next ogg page at %" G_GINT64_FORMAT,
|
|
result);
|
|
granulepos = ogg_page_granulepos (&og);
|
|
if (granulepos == -1) {
|
|
GST_LOG_OBJECT (ogg, "granulepos of next page is -1");
|
|
continue;
|
|
}
|
|
|
|
/* Avoid seeking to an incorrect granuletime by only considering
|
|
the stream for which we found the earliest time */
|
|
if (only_serial_no && ogg_page_serialno (&og) != serialno)
|
|
continue;
|
|
|
|
/* get the stream */
|
|
pad = gst_ogg_chain_get_stream (chain, ogg_page_serialno (&og));
|
|
if (pad == NULL || pad->map.is_skeleton)
|
|
continue;
|
|
|
|
/* convert granulepos to time */
|
|
granuletime = gst_ogg_stream_get_end_time_for_granulepos (&pad->map,
|
|
granulepos);
|
|
if (granuletime < pad->start_time)
|
|
continue;
|
|
|
|
GST_LOG_OBJECT (ogg, "granulepos %" G_GINT64_FORMAT " maps to time %"
|
|
GST_TIME_FORMAT, granulepos, GST_TIME_ARGS (granuletime));
|
|
|
|
granuletime -= pad->start_time;
|
|
granuletime += chain->begin_time;
|
|
|
|
GST_DEBUG_OBJECT (ogg,
|
|
"found page with granule %" G_GINT64_FORMAT " and time %"
|
|
GST_TIME_FORMAT, granulepos, GST_TIME_ARGS (granuletime));
|
|
|
|
if (granuletime < target) {
|
|
best = result; /* raw offset of packet with granulepos */
|
|
begin = ogg->offset; /* raw offset of next page */
|
|
begintime = granuletime;
|
|
|
|
bisect = begin; /* *not* begin + 1 */
|
|
} else {
|
|
if (bisect <= begin + 1) {
|
|
end = begin; /* found it */
|
|
} else {
|
|
if (end == ogg->offset) { /* we're pretty close - we'd be stuck in */
|
|
end = result;
|
|
bisect -= ogg->chunk_size; /* an endless loop otherwise. */
|
|
if (bisect <= begin)
|
|
bisect = begin + 1;
|
|
gst_ogg_demux_seek (ogg, bisect);
|
|
} else {
|
|
end = result;
|
|
endtime = granuletime;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else
|
|
goto seek_error;
|
|
}
|
|
}
|
|
GST_DEBUG_OBJECT (ogg, "seeking to %" G_GINT64_FORMAT, best);
|
|
gst_ogg_demux_seek (ogg, best);
|
|
*offset = best;
|
|
|
|
return TRUE;
|
|
|
|
/* ERRORS */
|
|
seek_error:
|
|
{
|
|
GST_DEBUG_OBJECT (ogg, "got a seek error");
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
do_index_search (GstOggDemux * ogg, GstOggChain * chain, gint64 begin,
|
|
gint64 end, gint64 begintime, gint64 endtime, gint64 target,
|
|
gint64 * p_offset, gint64 * p_timestamp)
|
|
{
|
|
guint i;
|
|
guint64 timestamp, offset;
|
|
guint64 r_timestamp, r_offset;
|
|
gboolean result = FALSE;
|
|
|
|
target -= begintime;
|
|
|
|
r_offset = -1;
|
|
r_timestamp = -1;
|
|
|
|
for (i = 0; i < chain->streams->len; i++) {
|
|
GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i);
|
|
|
|
timestamp = target;
|
|
if (gst_ogg_map_search_index (&pad->map, TRUE, ×tamp, &offset)) {
|
|
GST_INFO ("found %" G_GUINT64_FORMAT " at offset %" G_GUINT64_FORMAT,
|
|
timestamp, offset);
|
|
|
|
if (r_offset == -1 || offset < r_offset) {
|
|
r_offset = offset;
|
|
r_timestamp = timestamp;
|
|
}
|
|
result |= TRUE;
|
|
}
|
|
}
|
|
|
|
if (p_timestamp)
|
|
*p_timestamp = r_timestamp;
|
|
if (p_offset)
|
|
*p_offset = r_offset;
|
|
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
* do seek to time @position, return FALSE or chain and TRUE
|
|
*/
|
|
static gboolean
|
|
gst_ogg_demux_do_seek (GstOggDemux * ogg, GstSegment * segment,
|
|
gboolean accurate, gboolean keyframe, GstOggChain ** rchain)
|
|
{
|
|
guint64 position;
|
|
GstOggChain *chain = NULL;
|
|
gint64 begin, end;
|
|
gint64 begintime, endtime;
|
|
gint64 target, keytarget;
|
|
gint64 best;
|
|
gint64 total;
|
|
gint64 result = 0;
|
|
GstFlowReturn ret;
|
|
gint i, pending;
|
|
gint serialno = 0;
|
|
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 ts %" 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);
|
|
|
|
GST_LOG_OBJECT (ogg,
|
|
"looking at page with ts %" GST_TIME_FORMAT ", target %"
|
|
GST_TIME_FORMAT, GST_TIME_ARGS (granule_time),
|
|
GST_TIME_ARGS (target));
|
|
if (granule_time < target)
|
|
continue;
|
|
}
|
|
|
|
/* we've seen this pad before */
|
|
if (pad->keyframe_granule != -1)
|
|
continue;
|
|
|
|
/* convert granule of this pad to the granule of the keyframe */
|
|
pad->keyframe_granule =
|
|
gst_ogg_stream_granulepos_to_key_granule (&pad->map, granulepos);
|
|
GST_LOG_OBJECT (ogg, "marking stream granule %" G_GINT64_FORMAT,
|
|
pad->keyframe_granule);
|
|
|
|
/* get time of the keyframe */
|
|
keyframe_time =
|
|
gst_ogg_stream_granule_to_time (&pad->map, pad->keyframe_granule);
|
|
GST_LOG_OBJECT (ogg,
|
|
"stream %08x granule time %" GST_TIME_FORMAT,
|
|
pad->map.serialno, GST_TIME_ARGS (keyframe_time));
|
|
|
|
/* collect smallest value */
|
|
if (keyframe_time != -1) {
|
|
keyframe_time += begintime;
|
|
if (keyframe_time < keytarget) {
|
|
serialno = pad->map.serialno;
|
|
keytarget = keyframe_time;
|
|
found_keyframe = TRUE;
|
|
}
|
|
}
|
|
|
|
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 - EOS_AVOIDANCE_THRESHOLD;
|
|
if (position < 0)
|
|
position = 0;
|
|
|
|
GST_DEBUG_OBJECT (ogg,
|
|
"Getting duration, seeking near the end, to %" G_GINT64_FORMAT, position);
|
|
ogg->push_state = PUSH_DURATION;
|
|
/* do not read the last byte */
|
|
sevent = gst_event_new_seek (1.0, GST_FORMAT_BYTES, flags, GST_SEEK_TYPE_SET,
|
|
position, GST_SEEK_TYPE_SET, ogg->push_byte_length - 1);
|
|
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, chain->segment_stop);
|
|
|
|
return GST_FLOW_OK;
|
|
}
|
|
|
|
/* find a pad with a given serial number
|
|
*/
|
|
static GstOggPad *
|
|
gst_ogg_demux_find_pad (GstOggDemux * ogg, guint32 serialno)
|
|
{
|
|
GstOggPad *pad;
|
|
gint i;
|
|
|
|
/* first look in building chain if any */
|
|
if (ogg->building_chain) {
|
|
pad = gst_ogg_chain_get_stream (ogg->building_chain, serialno);
|
|
if (pad)
|
|
return pad;
|
|
}
|
|
|
|
/* then look in current chain if any */
|
|
if (ogg->current_chain) {
|
|
pad = gst_ogg_chain_get_stream (ogg->current_chain, serialno);
|
|
if (pad)
|
|
return pad;
|
|
}
|
|
|
|
for (i = 0; i < ogg->chains->len; i++) {
|
|
GstOggChain *chain = g_array_index (ogg->chains, GstOggChain *, i);
|
|
|
|
pad = gst_ogg_chain_get_stream (chain, serialno);
|
|
if (pad)
|
|
return pad;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* find a chain with a given serial number
|
|
*/
|
|
static GstOggChain *
|
|
gst_ogg_demux_find_chain (GstOggDemux * ogg, guint32 serialno)
|
|
{
|
|
GstOggPad *pad;
|
|
|
|
pad = gst_ogg_demux_find_pad (ogg, serialno);
|
|
if (pad) {
|
|
return pad->chain;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* returns TRUE if all streams have valid start time */
|
|
static gboolean
|
|
gst_ogg_demux_collect_chain_info (GstOggDemux * ogg, GstOggChain * chain)
|
|
{
|
|
gboolean res = TRUE;
|
|
|
|
chain->total_time = GST_CLOCK_TIME_NONE;
|
|
GST_DEBUG_OBJECT (ogg, "trying to collect chain info");
|
|
|
|
/* see if we have a start time on all streams */
|
|
chain->segment_start = gst_ogg_demux_collect_start_time (ogg, chain);
|
|
|
|
if (chain->segment_start == G_MAXUINT64) {
|
|
/* not yet, stream some more data */
|
|
res = FALSE;
|
|
} else if (chain->segment_stop != GST_CLOCK_TIME_NONE) {
|
|
/* we can calculate a total time */
|
|
chain->total_time = chain->segment_stop - chain->segment_start;
|
|
}
|
|
|
|
GST_DEBUG ("total time %" G_GUINT64_FORMAT, chain->total_time);
|
|
|
|
GST_DEBUG_OBJECT (ogg, "return %d", res);
|
|
|
|
return res;
|
|
}
|
|
|
|
static void
|
|
gst_ogg_demux_collect_info (GstOggDemux * ogg)
|
|
{
|
|
gint i;
|
|
|
|
/* collect all info */
|
|
ogg->total_time = 0;
|
|
|
|
for (i = 0; i < ogg->chains->len; i++) {
|
|
GstOggChain *chain = g_array_index (ogg->chains, GstOggChain *, i);
|
|
|
|
chain->begin_time = ogg->total_time;
|
|
|
|
gst_ogg_demux_collect_chain_info (ogg, chain);
|
|
|
|
ogg->total_time += chain->total_time;
|
|
}
|
|
ogg->segment.duration = ogg->total_time;
|
|
}
|
|
|
|
/* find all the chains in the ogg file, this reads the first and
|
|
* last page of the ogg stream, if they match then the ogg file has
|
|
* just one chain, else we do a binary search for all chains.
|
|
*/
|
|
static GstFlowReturn
|
|
gst_ogg_demux_find_chains (GstOggDemux * ogg)
|
|
{
|
|
ogg_page og;
|
|
GstPad *peer;
|
|
gboolean res;
|
|
guint32 serialno;
|
|
GstOggChain *chain;
|
|
GstFlowReturn ret;
|
|
|
|
/* get peer to figure out length */
|
|
if ((peer = gst_pad_get_peer (ogg->sinkpad)) == NULL)
|
|
goto no_peer;
|
|
|
|
/* find length to read last page, we store this for later use. */
|
|
res = gst_pad_query_duration (peer, GST_FORMAT_BYTES, &ogg->length);
|
|
gst_object_unref (peer);
|
|
if (!res || ogg->length <= 0)
|
|
goto no_length;
|
|
|
|
GST_DEBUG_OBJECT (ogg, "file length %" G_GINT64_FORMAT, ogg->length);
|
|
|
|
/* read chain from offset 0, this is the first chain of the
|
|
* ogg file. */
|
|
gst_ogg_demux_seek (ogg, 0);
|
|
ret = gst_ogg_demux_read_chain (ogg, &chain);
|
|
if (ret != GST_FLOW_OK) {
|
|
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)
|
|
{
|
|
GstOggPad *pad;
|
|
gint64 granule;
|
|
guint32 serialno;
|
|
GstFlowReturn result = GST_FLOW_OK;
|
|
|
|
serialno = ogg_page_serialno (page);
|
|
granule = ogg_page_granulepos (page);
|
|
|
|
gst_ogg_demux_update_chunk_size (ogg, page);
|
|
|
|
GST_LOG_OBJECT (ogg,
|
|
"processing ogg page (serial %08x, "
|
|
"pageno %ld, granulepos %" G_GINT64_FORMAT ", bos %d)", serialno,
|
|
ogg_page_pageno (page), granule, ogg_page_bos (page));
|
|
|
|
if (ogg_page_bos (page)) {
|
|
GstOggChain *chain;
|
|
|
|
/* first page */
|
|
/* see if we know about the chain already */
|
|
chain = gst_ogg_demux_find_chain (ogg, serialno);
|
|
if (chain) {
|
|
GstEvent *event;
|
|
gint64 start = 0;
|
|
GstSegment segment;
|
|
|
|
if (chain->segment_start != GST_CLOCK_TIME_NONE)
|
|
start = chain->segment_start;
|
|
|
|
/* create the newsegment event we are going to send out */
|
|
gst_segment_copy_into (&ogg->segment, &segment);
|
|
segment.start = start;
|
|
segment.stop = chain->segment_stop;
|
|
segment.time = chain->begin_time;
|
|
segment.base += chain->begin_time;
|
|
event = gst_event_new_segment (&segment);
|
|
gst_event_set_seqnum (event, ogg->seqnum);
|
|
|
|
GST_DEBUG_OBJECT (ogg,
|
|
"segment: start %" GST_TIME_FORMAT ", stop %" GST_TIME_FORMAT
|
|
", time %" GST_TIME_FORMAT, GST_TIME_ARGS (start),
|
|
GST_TIME_ARGS (chain->segment_stop),
|
|
GST_TIME_ARGS (chain->begin_time));
|
|
|
|
/* activate it as it means we have a non-header, this will also deactivate
|
|
* the currently running chain. */
|
|
gst_ogg_demux_activate_chain (ogg, chain, event);
|
|
pad = gst_ogg_demux_find_pad (ogg, serialno);
|
|
} else {
|
|
GstClockTime chain_time;
|
|
gint64 current_time;
|
|
|
|
/* this can only happen in push mode */
|
|
if (ogg->pullmode)
|
|
goto unknown_chain;
|
|
|
|
current_time = ogg->segment.position;
|
|
|
|
/* time of new chain is current time */
|
|
chain_time = current_time;
|
|
|
|
if (ogg->building_chain == NULL) {
|
|
GstOggChain *newchain;
|
|
|
|
newchain = gst_ogg_chain_new (ogg);
|
|
newchain->offset = 0;
|
|
/* set new chain begin time aligned with end time of old chain */
|
|
newchain->begin_time = chain_time;
|
|
GST_DEBUG_OBJECT (ogg, "new chain, begin time %" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (chain_time));
|
|
|
|
/* and this is the one we are building now */
|
|
ogg->building_chain = newchain;
|
|
}
|
|
pad = gst_ogg_chain_new_stream (ogg->building_chain, serialno);
|
|
}
|
|
} else {
|
|
pad = gst_ogg_demux_find_pad (ogg, serialno);
|
|
}
|
|
if (pad) {
|
|
result = gst_ogg_pad_submit_page (pad, page);
|
|
} else {
|
|
GST_PUSH_LOCK (ogg);
|
|
if (!ogg->pullmode && !ogg->push_disable_seeking) {
|
|
/* no pad while probing for duration, we must have a chained stream,
|
|
and we don't support them, so back off */
|
|
GST_INFO_OBJECT (ogg, "We seem to have a chained stream, we won't seek");
|
|
if (ogg->push_state == PUSH_DURATION) {
|
|
GstFlowReturn res;
|
|
|
|
res = gst_ogg_demux_seek_back_after_push_duration_check_unlock (ogg);
|
|
if (res != GST_FLOW_OK)
|
|
return res;
|
|
}
|
|
|
|
/* only once we seeked back */
|
|
GST_PUSH_LOCK (ogg);
|
|
ogg->push_disable_seeking = TRUE;
|
|
} else {
|
|
GST_PUSH_UNLOCK (ogg);
|
|
/* no pad. This means an ogg page without bos has been seen for this
|
|
* serialno. we just ignore it but post a warning... */
|
|
GST_ELEMENT_WARNING (ogg, STREAM, DECODE,
|
|
(NULL), ("unknown ogg pad for serial %08x detected", serialno));
|
|
return GST_FLOW_OK;
|
|
}
|
|
GST_PUSH_UNLOCK (ogg);
|
|
}
|
|
return result;
|
|
|
|
/* ERRORS */
|
|
unknown_chain:
|
|
{
|
|
GST_ELEMENT_ERROR (ogg, STREAM, DECODE,
|
|
(NULL), ("unknown ogg chain for serial %08x detected", serialno));
|
|
return GST_FLOW_ERROR;
|
|
}
|
|
}
|
|
|
|
/* streaming mode, receive a buffer, parse it, create pads for
|
|
* the serialno, submit pages and packets to the oggpads
|
|
*/
|
|
static GstFlowReturn
|
|
gst_ogg_demux_chain (GstPad * pad, GstObject * parent, GstBuffer * buffer)
|
|
{
|
|
GstOggDemux *ogg;
|
|
gint ret = 0;
|
|
GstFlowReturn result = GST_FLOW_OK;
|
|
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_ERROR_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);
|
|
if (result < 0) {
|
|
GST_DEBUG_OBJECT (ogg, "gst_ogg_demux_handle_page returned %d", result);
|
|
}
|
|
}
|
|
}
|
|
if (ret == 0 || result == GST_FLOW_OK) {
|
|
gst_ogg_demux_sync_streams (ogg);
|
|
}
|
|
GST_DEBUG_OBJECT (ogg, "leave with %d", result);
|
|
return result;
|
|
}
|
|
|
|
static gboolean
|
|
gst_ogg_demux_send_event (GstOggDemux * ogg, GstEvent * event)
|
|
{
|
|
GstOggChain *chain = ogg->current_chain;
|
|
gboolean res = TRUE;
|
|
|
|
if (!chain)
|
|
chain = ogg->building_chain;
|
|
|
|
if (chain) {
|
|
gint i;
|
|
|
|
for (i = 0; i < chain->streams->len; i++) {
|
|
GstOggPad *pad = g_array_index (chain->streams, GstOggPad *, i);
|
|
|
|
gst_event_ref (event);
|
|
GST_DEBUG_OBJECT (pad, "Pushing event %" GST_PTR_FORMAT, event);
|
|
res &= gst_pad_push_event (GST_PAD (pad), event);
|
|
}
|
|
} else {
|
|
GST_WARNING_OBJECT (ogg, "No chain to forward event to");
|
|
}
|
|
gst_event_unref (event);
|
|
|
|
return res;
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_ogg_demux_combine_flows (GstOggDemux * ogg, GstOggPad * pad,
|
|
GstFlowReturn ret)
|
|
{
|
|
/* store the value */
|
|
pad->last_ret = ret;
|
|
pad->is_eos = (ret == GST_FLOW_EOS);
|
|
|
|
return gst_flow_combiner_update_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;
|
|
}
|
|
|
|
ret = gst_ogg_demux_handle_page (ogg, &page);
|
|
|
|
done:
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
gst_ogg_demux_sync_streams (GstOggDemux * ogg)
|
|
{
|
|
GstClockTime cur;
|
|
GstOggChain *chain;
|
|
guint i;
|
|
|
|
chain = ogg->current_chain;
|
|
cur = ogg->segment.position;
|
|
if (chain == NULL || cur == -1)
|
|
return;
|
|
|
|
for (i = 0; i < chain->streams->len; i++) {
|
|
GstOggPad *stream = g_array_index (chain->streams, GstOggPad *, i);
|
|
|
|
/* Theoretically, we should be doing this for all streams, 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_ERROR (ogg, STREAM, DEMUX, (NULL),
|
|
("failed to start demuxing ogg"));
|
|
ret = GST_FLOW_ERROR;
|
|
}
|
|
goto pause;
|
|
}
|
|
pause:
|
|
{
|
|
const gchar *reason = gst_flow_get_name (ret);
|
|
GstEvent *event = NULL;
|
|
|
|
GST_LOG_OBJECT (ogg, "pausing task, reason %s", reason);
|
|
gst_pad_pause_task (ogg->sinkpad);
|
|
|
|
if (ret == GST_FLOW_EOS) {
|
|
/* perform EOS logic */
|
|
if (ogg->segment.flags & GST_SEEK_FLAG_SEGMENT) {
|
|
gint64 stop;
|
|
GstMessage *message;
|
|
|
|
/* for segment playback we need to post when (in stream time)
|
|
* we stopped, this is either stop (when set) or the duration. */
|
|
if ((stop = ogg->segment.stop) == -1)
|
|
stop = ogg->segment.duration;
|
|
|
|
GST_LOG_OBJECT (ogg, "Sending segment done, at end of segment");
|
|
message =
|
|
gst_message_new_segment_done (GST_OBJECT (ogg), GST_FORMAT_TIME,
|
|
stop);
|
|
gst_message_set_seqnum (message, ogg->seqnum);
|
|
|
|
gst_element_post_message (GST_ELEMENT (ogg), message);
|
|
|
|
event = gst_event_new_segment_done (GST_FORMAT_TIME, stop);
|
|
gst_event_set_seqnum (event, ogg->seqnum);
|
|
gst_ogg_demux_send_event (ogg, event);
|
|
event = NULL;
|
|
} else {
|
|
/* normal playback, send EOS to all linked pads */
|
|
GST_LOG_OBJECT (ogg, "Sending EOS, at end of stream");
|
|
event = gst_event_new_eos ();
|
|
}
|
|
} else if (ret == GST_FLOW_NOT_LINKED || ret < GST_FLOW_EOS) {
|
|
GST_ELEMENT_ERROR (ogg, STREAM, FAILED,
|
|
(_("Internal data stream error.")),
|
|
("stream stopped, reason %s", reason));
|
|
event = gst_event_new_eos ();
|
|
}
|
|
|
|
/* For wrong-state we still want to pause the task and stop
|
|
* but no error message or other things are necessary.
|
|
* wrong-state is no real error and will be caused by flushing,
|
|
* e.g. because of a flushing seek.
|
|
*/
|
|
if (event) {
|
|
/* guard against corrupt/truncated files, where one can hit EOS
|
|
before prerolling is done and a chain created. If we have no
|
|
chain to send the event to, error out. */
|
|
if (ogg->current_chain || ogg->building_chain) {
|
|
gst_event_set_seqnum (event, ogg->seqnum);
|
|
gst_ogg_demux_send_event (ogg, event);
|
|
} else {
|
|
gst_event_unref (event);
|
|
GST_ELEMENT_ERROR (ogg, STREAM, DEMUX, (NULL),
|
|
("EOS before finding a chain"));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* 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;
|
|
|
|
while (1) {
|
|
g_mutex_lock (&ogg->seek_event_mutex);
|
|
if (ogg->seek_event_thread_stop) {
|
|
g_mutex_unlock (&ogg->seek_event_mutex);
|
|
break;
|
|
}
|
|
g_cond_wait (&ogg->seek_event_cond, &ogg->seek_event_mutex);
|
|
if (ogg->seek_event_thread_stop) {
|
|
g_mutex_unlock (&ogg->seek_event_mutex);
|
|
break;
|
|
}
|
|
g_mutex_unlock (&ogg->seek_event_mutex);
|
|
|
|
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)
|
|
continue;
|
|
|
|
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");
|
|
}
|
|
}
|
|
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);
|
|
|
|
gst_ogg_chain_free (chain);
|
|
}
|
|
ogg->chains = g_array_set_size (ogg->chains, 0);
|
|
ogg->current_chain = NULL;
|
|
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;
|
|
g_mutex_init (&ogg->seek_event_mutex);
|
|
g_cond_init (&ogg->seek_event_cond);
|
|
ogg->seek_event_thread = g_thread_new ("seek_event_thread",
|
|
(GThreadFunc) gst_ogg_demux_loop_push, gst_object_ref (ogg));
|
|
} 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);
|
|
g_cond_clear (&ogg->seek_event_cond);
|
|
g_mutex_clear (&ogg->seek_event_mutex);
|
|
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->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 */
|