pipewire/pinos/gst/gstburstcache.c
2016-04-27 12:16:15 +02:00

1410 lines
43 KiB
C

/* GStreamer
* Copyright (C) <2004> Thomas Vander Stichele <thomas at apestaart dot org>
* Copyright (C) <2012> Wim Taymans <wim.taymans@gmail.com>
*
* 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:gstburstcache
* @short_description: caches and implements burst-on-connect of buffers
*
* GstBurstCache keeps a cache of buffers up to a configurable limit and replays
* this cache for newly added readers. This can be used to implement
* burst-on-connect for various network scenarios such as TCP or UDP.
*
* A new cache is created with gst_burst_cache_new(), which requires a size of
* the structure to hold the reader information.
*
* New buffers are put in the cache with gst_burst_cache_queue_buffer(). Old
* buffers will be removed from the cache. With gst_burst_cache_set_min_amount()
* one can control the amount of data in time/bytes/buffers to keep in the
* cache.
*
* New readers can be constructed with gst_burst_cache_reader_new(). This will
* allocate a new reader structure with the configured size when the cache was
* constructed. A callback needs to be provided that will be called when new
* data is available for the reader.
*
* The caller can configure the reader with gst_burst_cache_reader_set_burst().
* The start_method property will define which buffer in the cahced buffers will
* be sent first to the reader. Readers can be sent the most recent buffer
* (which might not be decodable by the reader if it is not a keyframe), the
* next keyframe received in the cache (which can take some time depending on
* the keyframe rate), or the last received keyframe (which will cause a simple
* burst-on-connect). GstBurstCache will always keep at least one keyframe in
* its internal cache.
*
* There are additional values for the start_method to allow finer control over
* burst-on-connect behaviour. By selecting the 'burst' method a minimum burst
* size can be chosen, 'burst-keyframe' additionally requires that the burst
* begin with a keyframe, and 'burst-with-keyframe' attempts to burst beginning
* with a keyframe, but will prefer a minimum burst size even if it requires
* not starting with a keyframe.
*
* When a reader is created and configured, it can be added to the cache with
* gst_burst_cache_add_reader(). This will trigger the callback when new data is
* available for the reader. The reader should call gst_burst_cache_get_buffer()
* to retrieve the available buffer until the function returns
* GST_BURST_CACHE_RESULT_WAIT, in which case it should wait for the callback again.
* This makes it possible to implement a push or pull model for retrieving data
* from the cache.
*
* If the reader does not call gst_burst_cache_get_buffer() fast enough, it will
* start to lag. With gst_burst_cache_set_limits() you can configure how much a
* reader is allowed to lag. You can configure a soft limit and a hard limit in
* and format.
*
* A reader with a lag of at least soft-max enters the recovery procedure which
* is controlled with the recover property. A recover policy of NONE will do
* nothing, RESYNC_LATEST will send the most recently received buffer as the
* next buffer for the reader, RESYNC_SOFT_LIMIT positions the reader to the
* soft limit in the buffer queue and RESYNC_KEYFRAME positions the reader at
* the most recent keyframe in the buffer queue.
*
* When the reader is lagging more that the soft-limit, its recovery
* procedure will be started, which usually will make it drop buffers to catch
* up. When the hard limit is reached, the reader is removed from the cache.
*
* A reader can be removed from the cache with gst_burst_cache_remove_reader().
*
* When a reader is in error, gst_burst_cache_error_reader() must be called,
* which will cause the reader to be removed from the cache.
*
* When a reader is freed, its GHook destroy function will be called with the
* GHook data. You can install a custom function and data to be notified.
*
* Last reviewed on 2012-11-09 (1.0.3)
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "gstburstcache.h"
#define DEFAULT_LIMIT_FORMAT GST_FORMAT_BUFFERS
#define DEFAULT_LIMIT_MAX -1
#define DEFAULT_LIMIT_SOFT_MAX -1
#define DEFAULT_TIME_MIN -1
#define DEFAULT_BYTES_MIN -1
#define DEFAULT_BUFFERS_MIN -1
#define DEFAULT_RECOVER GST_BURST_CACHE_RECOVER_NONE
enum
{
PROP_0,
PROP_LAST
};
GQuark gst_burst_cache_error_quark (void)
{
static GQuark quark;
if (!quark)
quark = g_quark_from_static_string ("gst-burst-cache-error-quark");
return quark;
}
GST_DEBUG_CATEGORY_STATIC (burstcache_debug);
#define GST_CAT_DEFAULT (burstcache_debug)
#define gst_burst_cache_parent_class parent_class
G_DEFINE_TYPE (GstBurstCache, gst_burst_cache, G_TYPE_OBJECT);
#define CACHE_LOCK_INIT(cache) (g_rec_mutex_init(&(cache)->lock))
#define CACHE_LOCK_CLEAR(cache) (g_rec_mutex_clear(&(cache)->lock))
#define CACHE_LOCK(cache) (g_rec_mutex_lock(&(cache)->lock))
#define CACHE_UNLOCK(cache) (g_rec_mutex_unlock(&(cache)->lock))
#define VALUE_INVALID ((guint64)-1)
static void gst_burst_cache_finalize (GObject * object);
G_DEFINE_POINTER_TYPE (GstBurstCacheReader, gst_burst_cache_reader);
static gint find_keyframe (GstBurstCache * cache, gint idx, gint direction);
#define find_next_keyframe(s,i) find_keyframe(s,i,1)
#define find_prev_keyframe(s,i) find_keyframe(s,i,-1)
static gboolean is_keyframe (GstBurstCache * cache, GstBuffer * buffer);
static gint get_buffers_max (GstBurstCache * cache, GstFormat format,
gint64 max);
static gint gst_burst_cache_recover_reader (GstBurstCache * cache,
GstBurstCacheReader * reader);
static gboolean find_limits (GstBurstCache * cache, gint * min_idx,
gint bytes_min, gint buffers_min, gint64 time_min, gint * max_idx,
gint bytes_max, gint buffers_max, gint64 time_max);
static void
gst_burst_cache_class_init (GstBurstCacheClass * klass)
{
GObjectClass *gobject_class;
gobject_class = (GObjectClass *) klass;
gobject_class->finalize = gst_burst_cache_finalize;
GST_DEBUG_CATEGORY_INIT (burstcache_debug, "burstcache", 0, "GstBurstCache");
}
static void
gst_burst_cache_init (GstBurstCache * this)
{
CACHE_LOCK_INIT (this);
this->bufqueue = g_ptr_array_new ();
this->limit_format = DEFAULT_LIMIT_FORMAT;
this->limit_max = DEFAULT_LIMIT_MAX;
this->limit_soft_max = DEFAULT_LIMIT_SOFT_MAX;
this->time_min = DEFAULT_TIME_MIN;
this->bytes_min = DEFAULT_BYTES_MIN;
this->buffers_min = DEFAULT_BUFFERS_MIN;
this->recover = DEFAULT_RECOVER;
}
static void
gst_burst_cache_finalize (GObject * object)
{
GstBurstCache *this;
this = GST_BURST_CACHE (object);
g_hook_list_clear (&this->readers);
g_ptr_array_foreach (this->bufqueue, (GFunc) gst_buffer_unref, NULL);
g_ptr_array_free (this->bufqueue, TRUE);
CACHE_LOCK_CLEAR (this);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
/**
* gst_burst_cache_new:
* @reader_size: the size of the hook
*
* Make a new burst cache. @hook_size specifies the size of the data structure
* that is kep for each client and must be at least
* sizeof(GstBurstCacheReader).
*
* Returns: a new #GstBurstCache
*/
GstBurstCache *
gst_burst_cache_new (guint reader_size)
{
GstBurstCache *cache;
g_return_val_if_fail (reader_size >= sizeof (GstBurstCacheReader), NULL);
cache = g_object_new (GST_TYPE_BURST_CACHE, NULL);
g_hook_list_init (&cache->readers, reader_size);
return cache;
}
/**
* gst_burst_cache_set_min_amount:
* @cache: a #GstBurstCache
* @bytes_min: minimum amount to cache in bytes
* @time_min: minimum amount to cache in time
* @buffers_min: minimum amount to cache in buffers
*
* Set the minimum amount of data that @cache should keep around. Values can be
* specified in bytes, time and buffers. A value of -1 sets unlimited caching.
*/
void
gst_burst_cache_set_min_amount (GstBurstCache * cache, gint bytes_min,
gint64 time_min, gint buffers_min)
{
g_return_if_fail (GST_IS_BURST_CACHE (cache));
cache->bytes_min = bytes_min;
cache->time_min = time_min;
cache->buffers_min = buffers_min;
}
/**
* gst_burst_cache_get_min_amount:
* @cache: a #GstBurstCache
* @bytes_min: (out) (allow-none): result in bytes
* @time_min: (out) (allow-none): result in time
* @buffers_min: (out) (allow-none): result in buffers
*
* Get the minimum amount of data that @cache keeps around.
*/
void
gst_burst_cache_get_min_amount (GstBurstCache * cache, gint * bytes_min,
gint64 * time_min, gint * buffers_min)
{
g_return_if_fail (GST_IS_BURST_CACHE (cache));
if (bytes_min)
*bytes_min = cache->bytes_min;
if (time_min)
*time_min = cache->time_min;
if (buffers_min)
*buffers_min = cache->buffers_min;
}
/**
* gst_burst_cache_set_limits:
* @cache: a #GstBurstCache
* @format: format of @max and @soft_max
* @max: maximum lag for a reader
* @soft_max: maximum lag for a reader before recovery is performed
* @recover: a #GstBurstCacheRecover
*
* Set the limits for readers. When a reader lags more than @soft_max behind the
* most recent buffer, the receovery procedure @recovery is started for the
* client. If the client lags up to @max, it will be removed from @cache.
*/
void
gst_burst_cache_set_limits (GstBurstCache * cache, GstFormat format,
gint64 max, gint64 soft_max, GstBurstCacheRecover recover)
{
g_return_if_fail (GST_IS_BURST_CACHE (cache));
cache->limit_format = format;
cache->limit_max = max;
cache->limit_soft_max = soft_max;
cache->recover = recover;
}
/**
* gst_burst_cache_get_limits:
* @cache: a #GstBurstCache
* @format: (out) (allow-none): result format of @max and @soft_max
* @max: (out) (allow-none): result maximum lag for a reader
* @soft_max: (out) (allow-none): result maximum lag for a reader before
* recovery is performed
* @recover: (out) (allow-none): result #GstBurstCacheRecover
*
* Get the reader limits. See gst_burst_cache_set_limits().
*/
void
gst_burst_cache_get_limits (GstBurstCache * cache, GstFormat * format,
gint64 * max, gint64 * soft_max, GstBurstCacheRecover * recover)
{
g_return_if_fail (GST_IS_BURST_CACHE (cache));
if (format)
*format = cache->limit_format;
if (max)
*max = cache->limit_max;
if (soft_max)
*soft_max = cache->limit_soft_max;
if (recover)
*recover = cache->recover;
}
/**
* gst_burst_cache_reader_destroy:
* @reader: a #GstBurstCacheReader
*
* Cleanup the memory of @reader.
*/
void
gst_burst_cache_reader_destroy (GstBurstCacheReader * reader)
{
if (reader->reason)
g_error_free (reader->reason);
if (reader->notify)
reader->notify (reader->user_data);
}
/**
* gst_burst_cache_reader_new:
* @cache: a #GstBurstCache
* @callback: a #GstBurstCacheReaderCallback
* @user_data: user data
* @notify: a #GDestroyNotify for @user_data
*
* Make a new #GstBurstCacheReader. When data becomes available for the reader,
* @callback will be called with @user_data.
*
* Returns: a new #GstBurstCacheReader.
*/
GstBurstCacheReader *
gst_burst_cache_reader_new (GstBurstCache * cache,
GstBurstCacheReaderCallback callback, gpointer user_data,
GDestroyNotify notify)
{
GstBurstCacheReader *reader;
reader = (GstBurstCacheReader *) g_hook_alloc (&cache->readers);
reader->hook.data = reader;
reader->hook.destroy = (GDestroyNotify) gst_burst_cache_reader_destroy;
reader->bufpos = -1;
reader->draincount = -1;
reader->new_reader = TRUE;
reader->discont = FALSE;
reader->callback = callback;
reader->user_data = user_data;
reader->notify = notify;
reader->reason = NULL;
reader->start_method = GST_BURST_CACHE_START_LATEST;
reader->bytes_sent = 0;
reader->dropped_buffers = 0;
reader->avg_queue_size = 0;
reader->first_buffer_ts = GST_CLOCK_TIME_NONE;
reader->last_buffer_ts = GST_CLOCK_TIME_NONE;
/* update start time */
reader->add_time = g_get_real_time ();
reader->remove_time = 0;
/* set last activity time to add time */
reader->last_activity_time = reader->add_time;
return reader;
}
/**
* gst_burst_cache_reader_set_burst:
* @reader: a #GstBurstCacheReader
* @start_method: a #GstBurstCacheStart
* @min_format: format of @min_value
* @min_value: minimum burst amount
* @max_format: format of @max_value
* @max_value: maximum burst amount
*
* Set the burst parameters for @reader. @start_method defines where to position
* the reader in the cache. At least @min_value of data and at most @max_value
* of data will be sent to the new client.
*
* Returns: %TRUE on success.
*/
gboolean
gst_burst_cache_reader_set_burst (GstBurstCacheReader * reader,
GstBurstCacheStart start_method, GstFormat min_format, guint64 min_value,
GstFormat max_format, guint64 max_value)
{
/* do limits check if we can */
if (min_format == max_format) {
if (max_value != VALUE_INVALID && min_value != VALUE_INVALID && max_value < min_value)
return FALSE;
}
reader->start_method = start_method;
reader->min_format = min_format;
reader->min_value = min_value;
reader->max_format = max_format;
reader->max_value = max_value;
return TRUE;
}
static gboolean
is_keyframe (GstBurstCache * cache, GstBuffer * buffer)
{
if (GST_BUFFER_FLAG_IS_SET (buffer, GST_BUFFER_FLAG_DELTA_UNIT)) {
return FALSE;
} else if (!GST_BUFFER_FLAG_IS_SET (buffer, GST_BUFFER_FLAG_HEADER)) {
return TRUE;
}
return FALSE;
}
/* find the keyframe in the list of buffers starting the
* search from @idx. @direction as -1 will search backwards,
* 1 will search forwards.
* Returns: the index or -1 if there is no keyframe after idx.
*/
static gint
find_keyframe (GstBurstCache * cache, gint idx, gint direction)
{
gint i, len, result;
/* take length of queued buffers */
len = cache->bufqueue->len;
/* assume we don't find a keyframe */
result = -1;
/* then loop over all buffers to find the first keyframe */
for (i = idx; i >= 0 && i < len; i += direction) {
GstBuffer *buf;
buf = g_ptr_array_index (cache->bufqueue, i);
if (is_keyframe (cache, buf)) {
GST_LOG_OBJECT (cache, "found keyframe at %d from %d, direction %d",
i, idx, direction);
result = i;
break;
}
}
return result;
}
/* Get the number of buffers from the buffer queue needed to satisfy
* the maximum max in the configured units.
* If units are not BUFFERS, and there are insufficient buffers in the
* queue to satify the limit, return len(queue) + 1 */
static gint
get_buffers_max (GstBurstCache * cache, GstFormat format, gint64 max)
{
switch (format) {
case GST_FORMAT_BUFFERS:
return max;
case GST_FORMAT_TIME:
{
GstBuffer *buf;
int i;
int len;
gint64 diff;
GstClockTime first = GST_CLOCK_TIME_NONE;
len = cache->bufqueue->len;
for (i = 0; i < len; i++) {
buf = g_ptr_array_index (cache->bufqueue, i);
if (GST_BUFFER_TIMESTAMP_IS_VALID (buf)) {
if (first == GST_CLOCK_TIME_NONE)
first = GST_BUFFER_TIMESTAMP (buf);
diff = first - GST_BUFFER_TIMESTAMP (buf);
if (diff > max)
return i + 1;
}
}
return len + 1;
}
case GST_FORMAT_BYTES:
{
GstBuffer *buf;
int i;
int len;
gint acc = 0;
len = cache->bufqueue->len;
for (i = 0; i < len; i++) {
buf = g_ptr_array_index (cache->bufqueue, i);
acc += gst_buffer_get_size (buf);
if (acc > max)
return i + 1;
}
return len + 1;
}
default:
return max;
}
}
/* find the positions in the buffer queue where *_min and *_max
* is satisfied
*/
/* count the amount of data in the buffers and return the index
* that satifies the given limits.
*
* Returns: index @idx in the buffer queue so that the given limits are
* satisfied. TRUE if all the limits could be satisfied, FALSE if not
* enough data was in the queue.
*
* FIXME, this code might not work if any of the units is in buffers...
*/
static gboolean
find_limits (GstBurstCache * cache,
gint * min_idx, gint bytes_min, gint buffers_min, gint64 time_min,
gint * max_idx, gint bytes_max, gint buffers_max, gint64 time_max)
{
GstClockTime first, time;
gint i, len, bytes;
gboolean result, max_hit;
/* take length of queue */
len = cache->bufqueue->len;
/* this must hold */
g_assert (len > 0);
GST_LOG_OBJECT (cache,
"bytes_min %d, buffers_min %d, time_min %" GST_TIME_FORMAT
", bytes_max %d, buffers_max %d, time_max %" GST_TIME_FORMAT, bytes_min,
buffers_min, GST_TIME_ARGS (time_min), bytes_max, buffers_max,
GST_TIME_ARGS (time_max));
/* do the trivial buffer limit test */
if (buffers_min != -1 && len < buffers_min) {
*min_idx = len - 1;
*max_idx = len - 1;
return FALSE;
}
result = FALSE;
/* else count bytes and time */
first = -1;
bytes = 0;
/* unset limits */
*min_idx = -1;
*max_idx = -1;
max_hit = FALSE;
i = 0;
/* loop through the buffers, when a limit is ok, mark it
* as -1, we have at least one buffer in the queue. */
do {
GstBuffer *buf;
/* if we checked all min limits, update result */
if (bytes_min == -1 && time_min == -1 && *min_idx == -1) {
/* don't go below 0 */
*min_idx = MAX (i - 1, 0);
}
/* if we reached one max limit break out */
if (max_hit) {
/* i > 0 when we get here, we subtract one to get the position
* of the previous buffer. */
*max_idx = i - 1;
/* we have valid complete result if we found a min_idx too */
result = *min_idx != -1;
break;
}
buf = g_ptr_array_index (cache->bufqueue, i);
bytes += gst_buffer_get_size (buf);
/* take timestamp and save for the base first timestamp */
if ((time = GST_BUFFER_TIMESTAMP (buf)) != GST_CLOCK_TIME_NONE) {
GST_LOG_OBJECT (cache, "Ts %" GST_TIME_FORMAT " on buffer",
GST_TIME_ARGS (time));
if (first == GST_CLOCK_TIME_NONE)
first = time;
/* increase max usage if we did not fill enough. Note that
* buffers are sorted from new to old, so the first timestamp is
* bigger than the next one. */
if (time_min != -1 && first - time >= (guint64) time_min)
time_min = -1;
if (time_max != -1 && first - time >= (guint64) time_max)
max_hit = TRUE;
} else {
GST_LOG_OBJECT (cache, "No timestamp on buffer");
}
/* time is OK or unknown, check and increase if not enough bytes */
if (bytes_min != -1) {
if (bytes >= bytes_min)
bytes_min = -1;
}
if (bytes_max != -1) {
if (bytes >= bytes_max) {
max_hit = TRUE;
}
}
i++;
}
while (i < len);
/* if we did not hit the max or min limit, set to buffer size */
if (*max_idx == -1)
*max_idx = len - 1;
/* make sure min does not exceed max */
if (*min_idx == -1)
*min_idx = *max_idx;
return result;
}
/* parse the unit/value pair and assign it to the result value of the
* right type, leave the other values untouched
*
* Returns: FALSE if the unit is unknown or undefined. TRUE otherwise.
*/
static gboolean
assign_value (GstFormat format, guint64 value, gint * bytes, gint * buffers,
GstClockTime * time)
{
gboolean res = TRUE;
/* set only the limit of the given format to the given value */
switch (format) {
case GST_FORMAT_BUFFERS:
*buffers = (gint) value;
break;
case GST_FORMAT_TIME:
*time = value;
break;
case GST_FORMAT_BYTES:
*bytes = (gint) value;
break;
case GST_FORMAT_UNDEFINED:
default:
res = FALSE;
break;
}
return res;
}
/* count the index in the buffer queue to satisfy the given unit
* and value pair starting from buffer at index 0.
*
* Returns: TRUE if there was enough data in the queue to satisfy the
* burst values. @idx contains the index in the buffer that contains enough
* data to satisfy the limits or the last buffer in the queue when the
* function returns FALSE.
*/
static gboolean
count_burst_unit (GstBurstCache * cache, gint * min_idx,
GstFormat min_format, guint64 min_value, gint * max_idx,
GstFormat max_format, guint64 max_value)
{
gint bytes_min = -1, buffers_min = -1;
gint bytes_max = -1, buffers_max = -1;
GstClockTime time_min = GST_CLOCK_TIME_NONE, time_max = GST_CLOCK_TIME_NONE;
assign_value (min_format, min_value, &bytes_min, &buffers_min, &time_min);
assign_value (max_format, max_value, &bytes_max, &buffers_max, &time_max);
return find_limits (cache, min_idx, bytes_min, buffers_min, time_min,
max_idx, bytes_max, buffers_max, time_max);
}
/* decide where in the current buffer queue this new reader should start
* receiving buffers from.
* This function is called whenever a reader is added and has not yet
* received a buffer.
*/
static void
handle_new_reader (GstBurstCache * cache, GstBurstCacheReader * reader)
{
gint position;
GST_DEBUG_OBJECT (cache,
"%s new reader, deciding where to start in queue", reader->debug);
GST_DEBUG_OBJECT (cache, "queue is currently %d buffers long",
cache->bufqueue->len);
switch (reader->start_method) {
case GST_BURST_CACHE_START_LATEST:
/* no syncing, we are happy with whatever the reader is going to get */
position = reader->bufpos;
GST_DEBUG_OBJECT (cache,
"%s BURST_CACHE_START_LATEST, position %d", reader->debug, position);
break;
case GST_BURST_CACHE_START_NEXT_KEYFRAME:
{
/* if one of the new buffers (between reader->bufpos and 0) in the queue
* is a key frame, we can proceed, otherwise we need to keep waiting */
GST_LOG_OBJECT (cache,
"%s new reader, bufpos %d, waiting for keyframe",
reader->debug, reader->bufpos);
position = find_prev_keyframe (cache, reader->bufpos);
if (position != -1) {
GST_DEBUG_OBJECT (cache,
"%s BURST_CACHE_START_NEXT_KEYFRAME: position %d", reader->debug,
position);
break;
}
/* reader is not on a keyframe, need to skip these buffers and
* wait some more */
GST_LOG_OBJECT (cache,
"%s new reader, skipping buffer(s), no keyframe found",
reader->debug);
reader->bufpos = -1;
break;
}
case GST_BURST_CACHE_START_LATEST_KEYFRAME:
{
GST_DEBUG_OBJECT (cache, "%s BURST_CACHE_START_LATEST_KEYFRAME",
reader->debug);
/* for new readers we initially scan the complete buffer queue for
* a keyframe when a buffer is added. If we don't find a keyframe,
* we need to wait for the next keyframe and so we change the reader's
* start method to GST_BURST_CACHE_START_NEXT_KEYFRAME.
*/
position = find_next_keyframe (cache, 0);
if (position != -1) {
GST_DEBUG_OBJECT (cache,
"%s BURST_CACHE_START_LATEST_KEYFRAME: position %d", reader->debug,
position);
break;
}
GST_DEBUG_OBJECT (cache,
"%s BURST_CACHE_START_LATEST_KEYFRAME: no keyframe found, "
"switching to BURST_CACHE_START_NEXT_KEYFRAME", reader->debug);
/* throw reader to the waiting state */
reader->bufpos = -1;
/* and make reader sync to next keyframe */
reader->start_method = GST_BURST_CACHE_START_NEXT_KEYFRAME;
break;
}
case GST_BURST_CACHE_START_BURST:
{
gboolean ok;
gint max;
/* move to the position where we satisfy the reader's burst
* parameters. If we could not satisfy the parameters because there
* is not enough data, we just send what we have (which is in position).
* We use the max value to limit the search
*/
ok = count_burst_unit (cache, &position, reader->min_format,
reader->min_value, &max, reader->max_format, reader->max_value);
GST_DEBUG_OBJECT (cache,
"%s BURST_CACHE_START_BURST: burst_unit returned %d, position %d",
reader->debug, ok, position);
GST_LOG_OBJECT (cache, "min %d, max %d", position, max);
/* we hit the max and it is below the min, use that then */
if (max != -1 && max <= position) {
position = MAX (max - 1, 0);
GST_DEBUG_OBJECT (cache,
"%s BURST_CACHE_START_BURST: position above max, taken down to %d",
reader->debug, position);
}
break;
}
case GST_BURST_CACHE_START_BURST_KEYFRAME:
{
gint min_idx, max_idx;
gint next_keyframe, prev_keyframe;
/* BURST_KEYFRAME:
*
* _always_ start sending a keyframe to the reader. We first search
* a keyframe between min/max limits. If there is none, we send it the
* last keyframe before min. If there is none, the behaviour is like
* NEXT_KEYFRAME.
*/
/* gather burst limits */
count_burst_unit (cache, &min_idx, reader->min_format,
reader->min_value, &max_idx, reader->max_format, reader->max_value);
GST_LOG_OBJECT (cache, "min %d, max %d", min_idx, max_idx);
/* first find a keyframe after min_idx */
next_keyframe = find_next_keyframe (cache, min_idx);
if (next_keyframe != -1 && next_keyframe < max_idx) {
/* we have a valid keyframe and it's below the max */
GST_LOG_OBJECT (cache, "found keyframe in min/max limits");
position = next_keyframe;
break;
}
/* no valid keyframe, try to find one below min */
prev_keyframe = find_prev_keyframe (cache, min_idx);
if (prev_keyframe != -1) {
GST_WARNING_OBJECT (cache,
"using keyframe below min in BURST_KEYFRAME start mode");
position = prev_keyframe;
break;
}
/* no prev keyframe or not enough data */
GST_WARNING_OBJECT (cache,
"no prev keyframe found in BURST_KEYFRAME start mode, waiting for next");
/* throw reader to the waiting state */
reader->bufpos = -1;
/* and make reader sync to next keyframe */
reader->start_method = GST_BURST_CACHE_START_NEXT_KEYFRAME;
position = -1;
break;
}
case GST_BURST_CACHE_START_BURST_WITH_KEYFRAME:
{
gint min_idx, max_idx;
gint next_keyframe;
/* BURST_WITH_KEYFRAME:
*
* try to start sending a keyframe to the reader. We first search
* a keyframe between min/max limits. If there is none, we send it the
* amount of data up 'till min.
*/
/* gather enough data to burst */
count_burst_unit (cache, &min_idx, reader->min_format,
reader->min_value, &max_idx, reader->max_format, reader->max_value);
GST_LOG_OBJECT (cache, "min %d, max %d", min_idx, max_idx);
/* first find a keyframe after min_idx */
next_keyframe = find_next_keyframe (cache, min_idx);
if (next_keyframe != -1 && next_keyframe < max_idx) {
/* we have a valid keyframe and it's below the max */
GST_LOG_OBJECT (cache, "found keyframe in min/max limits");
position = next_keyframe;
break;
}
/* no keyframe, send data from min_idx */
GST_WARNING_OBJECT (cache, "using min in BURST_WITH_KEYFRAME start mode");
/* make sure we don't go over the max limit */
if (max_idx != -1 && max_idx <= min_idx) {
position = MAX (max_idx - 1, 0);
} else {
position = min_idx;
}
break;
}
default:
g_warning ("unknown start method %d", reader->start_method);
position = reader->bufpos;
break;
}
if (position >= 0) {
/* we got a valid spot in the queue */
reader->new_reader = FALSE;
reader->bufpos = position;
/* signal that the reader is ready */
reader->callback (cache, reader, reader->user_data);
}
}
/**
* gst_burst_cache_add_reader:
* @cache: a #GstBurstCache
* @reader: a #GstBurstCacheReader
*
* Add @reader to @cache.
*
* Returns: %TRUE when @reader could be added
*/
gboolean
gst_burst_cache_add_reader (GstBurstCache * cache, GstBurstCacheReader * reader)
{
g_return_val_if_fail (GST_IS_BURST_CACHE (cache), FALSE);
g_return_val_if_fail (reader != NULL, FALSE);
g_return_val_if_fail (reader->new_reader, FALSE);
/* do limits check if we can */
if (reader->min_format == reader->max_format) {
if (reader->max_value != VALUE_INVALID && reader->min_value != VALUE_INVALID &&
reader->max_value < reader->min_value)
goto wrong_limits;
}
CACHE_LOCK (cache);
handle_new_reader (cache, reader);
/* we can add the handle now */
g_hook_prepend (&cache->readers, (GHook *) reader);
cache->readers_cookie++;
CACHE_UNLOCK (cache);
return TRUE;
/* errors */
wrong_limits:
{
GST_WARNING_OBJECT (cache,
"%s wrong values min =%" G_GUINT64_FORMAT ", max=%"
G_GUINT64_FORMAT ", unit %d specified when adding reader",
reader->debug, reader->min_value, reader->max_value,
reader->min_format);
return FALSE;
}
}
/* should be called with the readerslock held. */
static void
gst_burst_cache_remove_reader_link (GstBurstCache * cache,
GstBurstCacheReader * reader, gboolean remove, GError * reason)
{
if (!G_HOOK_IS_VALID (reader))
goto was_removing;
GST_DEBUG_OBJECT (cache, "%s removing reader %p: (%s)",
reader->debug, reader, reason ? reason->message : "Unknown reason");
/* set reader to invalid position while being removed */
reader->bufpos = -1;
reader->reason = reason;
reader->remove_time = g_get_real_time ();
cache->readers_cookie++;
if (remove)
g_hook_destroy_link (&cache->readers, (GHook *) reader);
return;
/* ERRORS */
was_removing:
{
GST_WARNING_OBJECT (cache, "%s reader is already being removed",
reader->debug);
if (reason)
g_error_free (reason);
return;
}
}
/**
* gst_burst_cache_remove_reader:
* @cache: a #GstBurstCache
* @reader: a #GstBurstCacheReader
* @drain: drain flag
*
* Remove @reader from @cache. When @drain is %TRUE all remaining data
* in the cache will be sent to the reader before it is removed.
*
* Returns: %TRUE on success
*/
gboolean
gst_burst_cache_remove_reader (GstBurstCache * cache,
GstBurstCacheReader * reader, gboolean drain)
{
g_return_val_if_fail (GST_IS_BURST_CACHE (cache), FALSE);
g_return_val_if_fail (reader != NULL, FALSE);
GST_DEBUG_OBJECT (cache, "%s removing reader", reader->debug);
CACHE_LOCK (cache);
if (!G_HOOK_IS_VALID (reader))
goto not_valid;
if (drain) {
if (reader->draincount == -1) {
/* take the position of the reader as the number of buffers left to drain.
* If the reader was at position -1, we drain 0 buffers, 0 == drain 1
* buffer, etc... This will mark reader as draining. We can not remove the
* reader right away because it might have some buffers to drain in its
* queue. */
reader->draincount = reader->bufpos + 1;
} else {
GST_INFO_OBJECT (cache, "%s Reader already draining", reader->debug);
}
} else {
gst_burst_cache_remove_reader_link (cache, reader, TRUE,
g_error_new (GST_BURST_CACHE_ERROR, GST_BURST_CACHE_ERROR_NONE, "User requested remove"));
}
CACHE_UNLOCK (cache);
return TRUE;
/* ERRORS */
not_valid:
{
GST_WARNING_OBJECT (cache, "reader %s not found!", reader->debug);
CACHE_UNLOCK (cache);
return FALSE;
}
}
/**
* gst_burst_cache_error_reader:
* @cache: a #GstBurstCache
* @reader: a #GstBurstCacheReader
* @error: (transfer full): a #GError
*
* Remove @reader from @cache and set the reason to @error. Ownership is taken
* of @error.
*
* Returns: %TRUE on success
*/
gboolean
gst_burst_cache_error_reader (GstBurstCache * cache,
GstBurstCacheReader * reader, GError * error)
{
g_return_val_if_fail (GST_IS_BURST_CACHE (cache), FALSE);
g_return_val_if_fail (reader != NULL, FALSE);
GST_DEBUG_OBJECT (cache, "%s error reader", reader->debug);
CACHE_LOCK (cache);
if (!G_HOOK_IS_VALID (reader))
goto not_valid;
if (error == NULL)
error = g_error_new (GST_BURST_CACHE_ERROR, GST_BURST_CACHE_ERROR_ERROR, "Unknown error");
GST_WARNING_OBJECT (cache, "%s reader %p error, removing: %s",
reader->debug, reader, error->message);
gst_burst_cache_remove_reader_link (cache, reader, TRUE, error);
CACHE_UNLOCK (cache);
return TRUE;
/* ERRORS */
not_valid:
{
GST_WARNING_OBJECT (cache, "reader %s not found!", reader->debug);
CACHE_UNLOCK (cache);
return FALSE;
}
}
static gboolean
remove_hook (GstBurstCacheReader * reader, GstBurstCache * cache)
{
gst_burst_cache_remove_reader_link (cache, reader, FALSE,
g_error_new (GST_BURST_CACHE_ERROR, GST_BURST_CACHE_ERROR_NONE, "User requested clear"));
/* FALSE to remove */
return FALSE;
}
/**
* gst_burst_cache_clear_readers:
* @cache: a #GstBurstCache
*
* Remove all readers from @cache.
*/
void
gst_burst_cache_clear_readers (GstBurstCache * cache)
{
g_return_if_fail (GST_IS_BURST_CACHE (cache));
GST_DEBUG_OBJECT (cache, "clearing all readers");
CACHE_LOCK (cache);
g_hook_list_marshal_check (&cache->readers, TRUE, (GHookCheckMarshaller)
remove_hook, cache);
CACHE_UNLOCK (cache);
}
/* calculate the new position for a reader after recovery. This function
* does not update the reader position but merely returns the required
* position.
*/
static gint
gst_burst_cache_recover_reader (GstBurstCache * cache,
GstBurstCacheReader * reader)
{
gint newbufpos;
GST_WARNING_OBJECT (cache,
"%s reader %p is lagging at %d, recover using policy %d",
reader->debug, reader, reader->bufpos, cache->recover);
switch (cache->recover) {
case GST_BURST_CACHE_RECOVER_NONE:
/* do nothing, reader will catch up or get kicked out when it reaches
* the hard max */
newbufpos = reader->bufpos;
break;
case GST_BURST_CACHE_RECOVER_RESYNC_LATEST:
/* move to beginning of queue */
newbufpos = -1;
break;
case GST_BURST_CACHE_RECOVER_RESYNC_SOFT_LIMIT:
/* move to beginning of soft max */
newbufpos =
get_buffers_max (cache, cache->limit_format, cache->limit_soft_max);
break;
case GST_BURST_CACHE_RECOVER_RESYNC_KEYFRAME:
/* find keyframe in buffers, we search backwards to find the
* closest keyframe relative to what this reader already received. */
newbufpos = MIN ((gint) (cache->bufqueue->len - 1),
get_buffers_max (cache, cache->limit_format,
cache->limit_soft_max) - 1);
while (newbufpos >= 0) {
GstBuffer *buf;
buf = g_ptr_array_index (cache->bufqueue, newbufpos);
if (is_keyframe (cache, buf)) {
/* found a buffer that is not a delta unit */
break;
}
newbufpos--;
}
break;
default:
/* unknown recovery procedure */
newbufpos =
get_buffers_max (cache, cache->limit_format, cache->limit_soft_max);
break;
}
return newbufpos;
}
typedef struct
{
GstBurstCache *cache;
GstBurstCacheClass *klass;
GstClockTime now;
gint max_buffer_usage;
gint max_buffers;
gint soft_max_buffers;
} QueueHookData;
static gboolean
queue_hook (GstBurstCacheReader * reader, QueueHookData * data)
{
GstBurstCache *cache = data->cache;
/* move reader forwards */
reader->bufpos++;
GST_LOG_OBJECT (cache, "%s reader %p at position %d",
reader->debug, reader, reader->bufpos);
/* check soft max if needed, recover reader */
if (data->soft_max_buffers > 0 && reader->bufpos >= data->soft_max_buffers) {
gint newpos;
newpos = gst_burst_cache_recover_reader (cache, reader);
if (newpos != reader->bufpos) {
reader->dropped_buffers += reader->bufpos - newpos;
reader->bufpos = newpos;
reader->discont = TRUE;
GST_INFO_OBJECT (cache, "%s reader %p position reset to %d",
reader->debug, reader, reader->bufpos);
} else {
GST_INFO_OBJECT (cache,
"%s reader %p not recovering position", reader->debug, reader);
}
}
/* check hard max */
if (data->max_buffers > 0 && reader->bufpos >= data->max_buffers)
goto hit_limit;
/* check timeout */
if (reader->timeout > 0 && data->now - reader->last_activity_time >
reader->timeout)
goto timeout;
if (reader->new_reader) {
handle_new_reader (cache, reader);
} else if (reader->bufpos == 0) {
/* reader changed from -1 to 0, we can send data to this reader now. */
reader->callback (cache, reader, reader->user_data);
}
/* keep track of maximum buffer usage */
if (reader->bufpos > data->max_buffer_usage) {
data->max_buffer_usage = reader->bufpos;
}
return TRUE;
/* ERRORS */
hit_limit:
{
GST_WARNING_OBJECT (cache, "%s reader %p is too slow, removing",
reader->debug, reader);
gst_burst_cache_remove_reader_link (cache, reader, FALSE,
g_error_new (GST_BURST_CACHE_ERROR, GST_BURST_CACHE_ERROR_SLOW, "Reader is too slow"));
/* remove reader */
return FALSE;
}
timeout:
{
GST_WARNING_OBJECT (cache, "%s reader %p timeout, removing",
reader->debug, reader);
gst_burst_cache_remove_reader_link (cache, reader, FALSE,
g_error_new (GST_BURST_CACHE_ERROR, GST_BURST_CACHE_ERROR_SLOW, "Reader timed out"));
/* remove reader */
return FALSE;
}
}
/**
* gst_burst_cache_queue_buffer:
* @cache: a #GstBurstCache
* @buffer: a #GstBuffer
*
* Queue @buffer in @cache. Older and unused buffers will be removed from
* @cache.
*/
void
gst_burst_cache_queue_buffer (GstBurstCache * cache, GstBuffer * buffer)
{
gint queuelen;
gint i;
QueueHookData data;
g_return_if_fail (GST_IS_BURST_CACHE (cache));
g_return_if_fail (buffer != NULL);
data.now = g_get_real_time ();
data.klass = GST_BURST_CACHE_GET_CLASS (cache);
CACHE_LOCK (cache);
/* add buffer to queue */
g_ptr_array_insert (cache->bufqueue, 0, buffer);
queuelen = cache->bufqueue->len;
data.cache = cache;
if (cache->limit_max > 0)
data.max_buffers =
get_buffers_max (cache, cache->limit_format, cache->limit_max);
else
data.max_buffers = -1;
if (cache->limit_soft_max > 0)
data.soft_max_buffers =
get_buffers_max (cache, cache->limit_format, cache->limit_soft_max);
else
data.soft_max_buffers = -1;
GST_LOG_OBJECT (cache, "Using max %d, softmax %d", data.max_buffers,
data.soft_max_buffers);
/* After adding the buffer, we update all reader positions in the queue. If
* a reader moves over the soft max, we start the recovery procedure for this
* slow reader. If it goes over the hard max, it is put into the slow list
* and removed. */
data.max_buffer_usage = 0;
g_hook_list_marshal_check (&cache->readers, TRUE, (GHookCheckMarshaller)
queue_hook, &data);
/* make sure we respect bytes-min, buffers-min and time-min when they are set */
{
gint usage, max;
GST_LOG_OBJECT (cache,
"extending queue %d to respect time_min %" GST_TIME_FORMAT
", bytes_min %d, buffers_min %d", data.max_buffer_usage,
GST_TIME_ARGS (cache->time_min), cache->bytes_min, cache->buffers_min);
/* get index where the limits are ok, we don't really care if all limits
* are ok, we just queue as much as we need. We also don't compare against
* the max limits. */
find_limits (cache, &usage, cache->bytes_min, cache->buffers_min,
cache->time_min, &max, -1, -1, -1);
data.max_buffer_usage = MAX (data.max_buffer_usage, usage + 1);
GST_LOG_OBJECT (cache, "extended queue to %d", data.max_buffer_usage);
}
/* now look for start points and make sure there is at least one
* keyframe point in the queue. */
{
/* no point in searching beyond the queue length */
gint limit = queuelen;
/* no point in searching beyond the soft-max if any. */
if (data.soft_max_buffers > 0) {
limit = MIN (limit, data.soft_max_buffers);
}
GST_LOG_OBJECT (cache,
"extending queue to include start point, now at %d, limit is %d",
data.max_buffer_usage, limit);
for (i = 0; i < limit; i++) {
GstBuffer *buf;
buf = g_ptr_array_index (cache->bufqueue, i);
if (is_keyframe (cache, buf)) {
/* found a sync frame, now extend the buffer usage to
* include at least this frame. */
data.max_buffer_usage = MAX (data.max_buffer_usage, i);
break;
}
}
GST_LOG_OBJECT (cache, "max buffer usage is now %d", data.max_buffer_usage);
}
GST_LOG_OBJECT (cache, "len %d, usage %d", queuelen, data.max_buffer_usage);
/* nobody is referencing units after max_buffer_usage so we can
* remove them from the queue. We remove them in reverse order as
* this is the most optimal for GArray. */
for (i = queuelen - 1; i > data.max_buffer_usage; i--) {
GstBuffer *old;
/* queue exceeded max size */
queuelen--;
old = g_ptr_array_remove_index (cache->bufqueue, i);
/* unref tail buffer */
gst_buffer_unref (old);
}
/* save for stats */
cache->buffers_queued = data.max_buffer_usage;
CACHE_UNLOCK (cache);
}
/**
* gst_burst_cache_get_buffer:
* @cache: a #GstBurstCache
* @reader: a #GstBurstCacheReader
* @buffer: a #GstBuffer
*
* Get the next buffer for @reader in @cache.
*
* Returns: #GST_BURST_CACHE_RESULT_OK when a buffer is available.
* #GST_BURST_CACHE_RESULT_WAIT is returned when no buffers are available, the
* caller should wait for the callback signal before attempting to get a
* buffer again. #GST_BURST_CACHE_RESULT_EOS is returned when the client has
* received all buffers and is ready to be removed.
*/
GstBurstCacheResult
gst_burst_cache_get_buffer (GstBurstCache * cache, GstBurstCacheReader * reader,
GstBuffer ** buffer)
{
GstBuffer *buf;
GstClockTime timestamp;
g_return_val_if_fail (GST_IS_BURST_CACHE (cache),
GST_BURST_CACHE_RESULT_ERROR);
g_return_val_if_fail (reader != NULL, GST_BURST_CACHE_RESULT_ERROR);
g_return_val_if_fail (buffer != NULL, GST_BURST_CACHE_RESULT_ERROR);
CACHE_LOCK (cache);
if (reader->bufpos == -1)
goto no_data_yet;
/* we drained all remaining buffers, no need to get a new one */
if (reader->draincount == 0)
goto drained;
/* grab buffer */
buf = g_ptr_array_index (cache->bufqueue, reader->bufpos);
reader->bufpos--;
/* update stats */
timestamp = GST_BUFFER_TIMESTAMP (buf);
if (reader->first_buffer_ts == GST_CLOCK_TIME_NONE)
reader->first_buffer_ts = timestamp;
if (timestamp != GST_CLOCK_TIME_NONE)
reader->last_buffer_ts = timestamp;
/* decrease draincount */
if (reader->draincount != -1)
reader->draincount--;
GST_LOG_OBJECT (cache, "%s reader %p at position %d",
reader->debug, reader, reader->bufpos);
*buffer = gst_buffer_ref (buf);
CACHE_UNLOCK (cache);
return GST_BURST_CACHE_RESULT_OK;
/* ERRORS */
no_data_yet:
{
GST_DEBUG_OBJECT (cache, "%s no data available", reader->debug);
CACHE_UNLOCK (cache);
return GST_BURST_CACHE_RESULT_WAIT;
}
drained:
{
GST_DEBUG_OBJECT (cache, "%s drained", reader->debug);
CACHE_UNLOCK (cache);
return GST_BURST_CACHE_RESULT_EOS;
}
}