| /* GStreamer |
| * Copyright (C) <2007> 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. |
| */ |
| #include <string.h> |
| #include <stdlib.h> |
| |
| #include <gst/rtp/gstrtpbuffer.h> |
| #include <gst/rtp/gstrtcpbuffer.h> |
| |
| #include "rtpjitterbuffer.h" |
| |
| GST_DEBUG_CATEGORY_STATIC (rtp_jitter_buffer_debug); |
| #define GST_CAT_DEFAULT rtp_jitter_buffer_debug |
| |
| #define MAX_WINDOW RTP_JITTER_BUFFER_MAX_WINDOW |
| #define MAX_TIME (2 * GST_SECOND) |
| |
| /* signals and args */ |
| enum |
| { |
| LAST_SIGNAL |
| }; |
| |
| enum |
| { |
| PROP_0 |
| }; |
| |
| /* GObject vmethods */ |
| static void rtp_jitter_buffer_finalize (GObject * object); |
| |
| GType |
| rtp_jitter_buffer_mode_get_type (void) |
| { |
| static GType jitter_buffer_mode_type = 0; |
| static const GEnumValue jitter_buffer_modes[] = { |
| {RTP_JITTER_BUFFER_MODE_NONE, "Only use RTP timestamps", "none"}, |
| {RTP_JITTER_BUFFER_MODE_SLAVE, "Slave receiver to sender clock", "slave"}, |
| {RTP_JITTER_BUFFER_MODE_BUFFER, "Do low/high watermark buffering", |
| "buffer"}, |
| {RTP_JITTER_BUFFER_MODE_SYNCED, "Synchronized sender and receiver clocks", |
| "synced"}, |
| {0, NULL, NULL}, |
| }; |
| |
| if (!jitter_buffer_mode_type) { |
| jitter_buffer_mode_type = |
| g_enum_register_static ("RTPJitterBufferMode", jitter_buffer_modes); |
| } |
| return jitter_buffer_mode_type; |
| } |
| |
| /* static guint rtp_jitter_buffer_signals[LAST_SIGNAL] = { 0 }; */ |
| |
| G_DEFINE_TYPE (RTPJitterBuffer, rtp_jitter_buffer, G_TYPE_OBJECT); |
| |
| static void |
| rtp_jitter_buffer_class_init (RTPJitterBufferClass * klass) |
| { |
| GObjectClass *gobject_class; |
| |
| gobject_class = (GObjectClass *) klass; |
| |
| gobject_class->finalize = rtp_jitter_buffer_finalize; |
| |
| GST_DEBUG_CATEGORY_INIT (rtp_jitter_buffer_debug, "rtpjitterbuffer", 0, |
| "RTP Jitter Buffer"); |
| } |
| |
| static void |
| rtp_jitter_buffer_init (RTPJitterBuffer * jbuf) |
| { |
| g_mutex_init (&jbuf->clock_lock); |
| |
| jbuf->packets = g_queue_new (); |
| jbuf->mode = RTP_JITTER_BUFFER_MODE_SLAVE; |
| |
| rtp_jitter_buffer_reset_skew (jbuf); |
| } |
| |
| static void |
| rtp_jitter_buffer_finalize (GObject * object) |
| { |
| RTPJitterBuffer *jbuf; |
| |
| jbuf = RTP_JITTER_BUFFER_CAST (object); |
| |
| if (jbuf->media_clock_synced_id) |
| g_signal_handler_disconnect (jbuf->media_clock, |
| jbuf->media_clock_synced_id); |
| if (jbuf->media_clock) |
| gst_object_unref (jbuf->media_clock); |
| |
| if (jbuf->pipeline_clock) |
| gst_object_unref (jbuf->pipeline_clock); |
| |
| g_queue_free (jbuf->packets); |
| |
| g_mutex_clear (&jbuf->clock_lock); |
| |
| G_OBJECT_CLASS (rtp_jitter_buffer_parent_class)->finalize (object); |
| } |
| |
| /** |
| * rtp_jitter_buffer_new: |
| * |
| * Create an #RTPJitterBuffer. |
| * |
| * Returns: a new #RTPJitterBuffer. Use g_object_unref() after usage. |
| */ |
| RTPJitterBuffer * |
| rtp_jitter_buffer_new (void) |
| { |
| RTPJitterBuffer *jbuf; |
| |
| jbuf = g_object_new (RTP_TYPE_JITTER_BUFFER, NULL); |
| |
| return jbuf; |
| } |
| |
| /** |
| * rtp_jitter_buffer_get_mode: |
| * @jbuf: an #RTPJitterBuffer |
| * |
| * Get the current jitterbuffer mode. |
| * |
| * Returns: the current jitterbuffer mode. |
| */ |
| RTPJitterBufferMode |
| rtp_jitter_buffer_get_mode (RTPJitterBuffer * jbuf) |
| { |
| return jbuf->mode; |
| } |
| |
| /** |
| * rtp_jitter_buffer_set_mode: |
| * @jbuf: an #RTPJitterBuffer |
| * @mode: a #RTPJitterBufferMode |
| * |
| * Set the buffering and clock slaving algorithm used in the @jbuf. |
| */ |
| void |
| rtp_jitter_buffer_set_mode (RTPJitterBuffer * jbuf, RTPJitterBufferMode mode) |
| { |
| jbuf->mode = mode; |
| } |
| |
| GstClockTime |
| rtp_jitter_buffer_get_delay (RTPJitterBuffer * jbuf) |
| { |
| return jbuf->delay; |
| } |
| |
| void |
| rtp_jitter_buffer_set_delay (RTPJitterBuffer * jbuf, GstClockTime delay) |
| { |
| jbuf->delay = delay; |
| jbuf->low_level = (delay * 15) / 100; |
| /* the high level is at 90% in order to release packets before we fill up the |
| * buffer up to the latency */ |
| jbuf->high_level = (delay * 90) / 100; |
| |
| GST_DEBUG ("delay %" GST_TIME_FORMAT ", min %" GST_TIME_FORMAT ", max %" |
| GST_TIME_FORMAT, GST_TIME_ARGS (jbuf->delay), |
| GST_TIME_ARGS (jbuf->low_level), GST_TIME_ARGS (jbuf->high_level)); |
| } |
| |
| /** |
| * rtp_jitter_buffer_set_clock_rate: |
| * @jbuf: an #RTPJitterBuffer |
| * @clock_rate: the new clock rate |
| * |
| * Set the clock rate in the jitterbuffer. |
| */ |
| void |
| rtp_jitter_buffer_set_clock_rate (RTPJitterBuffer * jbuf, guint32 clock_rate) |
| { |
| if (jbuf->clock_rate != clock_rate) { |
| GST_DEBUG ("Clock rate changed from %" G_GUINT32_FORMAT " to %" |
| G_GUINT32_FORMAT, jbuf->clock_rate, clock_rate); |
| jbuf->clock_rate = clock_rate; |
| rtp_jitter_buffer_reset_skew (jbuf); |
| } |
| } |
| |
| /** |
| * rtp_jitter_buffer_get_clock_rate: |
| * @jbuf: an #RTPJitterBuffer |
| * |
| * Get the currently configure clock rate in @jbuf. |
| * |
| * Returns: the current clock-rate |
| */ |
| guint32 |
| rtp_jitter_buffer_get_clock_rate (RTPJitterBuffer * jbuf) |
| { |
| return jbuf->clock_rate; |
| } |
| |
| static void |
| media_clock_synced_cb (GstClock * clock, gboolean synced, |
| RTPJitterBuffer * jbuf) |
| { |
| GstClockTime internal, external; |
| |
| g_mutex_lock (&jbuf->clock_lock); |
| if (jbuf->pipeline_clock) { |
| internal = gst_clock_get_internal_time (jbuf->media_clock); |
| external = gst_clock_get_time (jbuf->pipeline_clock); |
| |
| gst_clock_set_calibration (jbuf->media_clock, internal, external, 1, 1); |
| } |
| g_mutex_unlock (&jbuf->clock_lock); |
| } |
| |
| /** |
| * rtp_jitter_buffer_set_media_clock: |
| * @jbuf: an #RTPJitterBuffer |
| * @clock: (transfer full): media #GstClock |
| * @clock_offset: RTP time at clock epoch or -1 |
| * |
| * Sets the media clock for the media and the clock offset |
| * |
| */ |
| void |
| rtp_jitter_buffer_set_media_clock (RTPJitterBuffer * jbuf, GstClock * clock, |
| guint64 clock_offset) |
| { |
| g_mutex_lock (&jbuf->clock_lock); |
| if (jbuf->media_clock) { |
| if (jbuf->media_clock_synced_id) |
| g_signal_handler_disconnect (jbuf->media_clock, |
| jbuf->media_clock_synced_id); |
| jbuf->media_clock_synced_id = 0; |
| gst_object_unref (jbuf->media_clock); |
| } |
| jbuf->media_clock = clock; |
| jbuf->media_clock_offset = clock_offset; |
| |
| if (jbuf->pipeline_clock && jbuf->media_clock && |
| jbuf->pipeline_clock != jbuf->media_clock) { |
| jbuf->media_clock_synced_id = |
| g_signal_connect (jbuf->media_clock, "synced", |
| G_CALLBACK (media_clock_synced_cb), jbuf); |
| if (gst_clock_is_synced (jbuf->media_clock)) { |
| GstClockTime internal, external; |
| |
| internal = gst_clock_get_internal_time (jbuf->media_clock); |
| external = gst_clock_get_time (jbuf->pipeline_clock); |
| |
| gst_clock_set_calibration (jbuf->media_clock, internal, external, 1, 1); |
| } |
| |
| gst_clock_set_master (jbuf->media_clock, jbuf->pipeline_clock); |
| } |
| g_mutex_unlock (&jbuf->clock_lock); |
| } |
| |
| /** |
| * rtp_jitter_buffer_set_pipeline_clock: |
| * @jbuf: an #RTPJitterBuffer |
| * @clock: pipeline #GstClock |
| * |
| * Sets the pipeline clock |
| * |
| */ |
| void |
| rtp_jitter_buffer_set_pipeline_clock (RTPJitterBuffer * jbuf, GstClock * clock) |
| { |
| g_mutex_lock (&jbuf->clock_lock); |
| if (jbuf->pipeline_clock) |
| gst_object_unref (jbuf->pipeline_clock); |
| jbuf->pipeline_clock = clock ? gst_object_ref (clock) : NULL; |
| |
| if (jbuf->pipeline_clock && jbuf->media_clock && |
| jbuf->pipeline_clock != jbuf->media_clock) { |
| if (gst_clock_is_synced (jbuf->media_clock)) { |
| GstClockTime internal, external; |
| |
| internal = gst_clock_get_internal_time (jbuf->media_clock); |
| external = gst_clock_get_time (jbuf->pipeline_clock); |
| |
| gst_clock_set_calibration (jbuf->media_clock, internal, external, 1, 1); |
| } |
| |
| gst_clock_set_master (jbuf->media_clock, jbuf->pipeline_clock); |
| } |
| g_mutex_unlock (&jbuf->clock_lock); |
| } |
| |
| gboolean |
| rtp_jitter_buffer_get_rfc7273_sync (RTPJitterBuffer * jbuf) |
| { |
| return jbuf->rfc7273_sync; |
| } |
| |
| void |
| rtp_jitter_buffer_set_rfc7273_sync (RTPJitterBuffer * jbuf, |
| gboolean rfc7273_sync) |
| { |
| jbuf->rfc7273_sync = rfc7273_sync; |
| } |
| |
| /** |
| * rtp_jitter_buffer_reset_skew: |
| * @jbuf: an #RTPJitterBuffer |
| * |
| * Reset the skew calculations in @jbuf. |
| */ |
| void |
| rtp_jitter_buffer_reset_skew (RTPJitterBuffer * jbuf) |
| { |
| jbuf->base_time = -1; |
| jbuf->base_rtptime = -1; |
| jbuf->base_extrtp = -1; |
| jbuf->media_clock_base_time = -1; |
| jbuf->ext_rtptime = -1; |
| jbuf->last_rtptime = -1; |
| jbuf->window_pos = 0; |
| jbuf->window_filling = TRUE; |
| jbuf->window_min = 0; |
| jbuf->skew = 0; |
| jbuf->prev_send_diff = -1; |
| jbuf->prev_out_time = -1; |
| jbuf->need_resync = TRUE; |
| |
| GST_DEBUG ("reset skew correction"); |
| } |
| |
| /** |
| * rtp_jitter_buffer_disable_buffering: |
| * @jbuf: an #RTPJitterBuffer |
| * @disabled: the new state |
| * |
| * Enable or disable buffering on @jbuf. |
| */ |
| void |
| rtp_jitter_buffer_disable_buffering (RTPJitterBuffer * jbuf, gboolean disabled) |
| { |
| jbuf->buffering_disabled = disabled; |
| } |
| |
| static void |
| rtp_jitter_buffer_resync (RTPJitterBuffer * jbuf, GstClockTime time, |
| GstClockTime gstrtptime, guint64 ext_rtptime, gboolean reset_skew) |
| { |
| jbuf->base_time = time; |
| jbuf->media_clock_base_time = -1; |
| jbuf->base_rtptime = gstrtptime; |
| jbuf->base_extrtp = ext_rtptime; |
| jbuf->prev_out_time = -1; |
| jbuf->prev_send_diff = -1; |
| if (reset_skew) { |
| jbuf->window_filling = TRUE; |
| jbuf->window_pos = 0; |
| jbuf->window_min = 0; |
| jbuf->window_size = 0; |
| jbuf->skew = 0; |
| } |
| jbuf->need_resync = FALSE; |
| } |
| |
| static guint64 |
| get_buffer_level (RTPJitterBuffer * jbuf) |
| { |
| RTPJitterBufferItem *high_buf = NULL, *low_buf = NULL; |
| guint64 level; |
| |
| /* first buffer with timestamp */ |
| high_buf = (RTPJitterBufferItem *) g_queue_peek_tail_link (jbuf->packets); |
| while (high_buf) { |
| if (high_buf->dts != -1 || high_buf->pts != -1) |
| break; |
| |
| high_buf = (RTPJitterBufferItem *) g_list_previous (high_buf); |
| } |
| |
| low_buf = (RTPJitterBufferItem *) g_queue_peek_head_link (jbuf->packets); |
| while (low_buf) { |
| if (low_buf->dts != -1 || low_buf->pts != -1) |
| break; |
| |
| low_buf = (RTPJitterBufferItem *) g_list_next (low_buf); |
| } |
| |
| if (!high_buf || !low_buf || high_buf == low_buf) { |
| level = 0; |
| } else { |
| guint64 high_ts, low_ts; |
| |
| high_ts = high_buf->dts != -1 ? high_buf->dts : high_buf->pts; |
| low_ts = low_buf->dts != -1 ? low_buf->dts : low_buf->pts; |
| |
| if (high_ts > low_ts) |
| level = high_ts - low_ts; |
| else |
| level = 0; |
| |
| GST_LOG_OBJECT (jbuf, |
| "low %" GST_TIME_FORMAT " high %" GST_TIME_FORMAT " level %" |
| G_GUINT64_FORMAT, GST_TIME_ARGS (low_ts), GST_TIME_ARGS (high_ts), |
| level); |
| } |
| return level; |
| } |
| |
| static void |
| update_buffer_level (RTPJitterBuffer * jbuf, gint * percent) |
| { |
| gboolean post = FALSE; |
| guint64 level; |
| |
| level = get_buffer_level (jbuf); |
| GST_DEBUG ("buffer level %" GST_TIME_FORMAT, GST_TIME_ARGS (level)); |
| |
| if (jbuf->buffering_disabled) { |
| GST_DEBUG ("buffering is disabled"); |
| level = jbuf->high_level; |
| } |
| |
| if (jbuf->buffering) { |
| post = TRUE; |
| if (level >= jbuf->high_level) { |
| GST_DEBUG ("buffering finished"); |
| jbuf->buffering = FALSE; |
| } |
| } else { |
| if (level < jbuf->low_level) { |
| GST_DEBUG ("buffering started"); |
| jbuf->buffering = TRUE; |
| post = TRUE; |
| } |
| } |
| if (post) { |
| gint perc; |
| |
| if (jbuf->buffering && (jbuf->high_level != 0)) { |
| perc = (level * 100 / jbuf->high_level); |
| perc = MIN (perc, 100); |
| } else { |
| perc = 100; |
| } |
| |
| if (percent) |
| *percent = perc; |
| |
| GST_DEBUG ("buffering %d", perc); |
| } |
| } |
| |
| /* For the clock skew we use a windowed low point averaging algorithm as can be |
| * found in Fober, Orlarey and Letz, 2005, "Real Time Clock Skew Estimation |
| * over Network Delays": |
| * http://www.grame.fr/Ressources/pub/TR-050601.pdf |
| * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.102.1546 |
| * |
| * The idea is that the jitter is composed of: |
| * |
| * J = N + n |
| * |
| * N : a constant network delay. |
| * n : random added noise. The noise is concentrated around 0 |
| * |
| * In the receiver we can track the elapsed time at the sender with: |
| * |
| * send_diff(i) = (Tsi - Ts0); |
| * |
| * Tsi : The time at the sender at packet i |
| * Ts0 : The time at the sender at the first packet |
| * |
| * This is the difference between the RTP timestamp in the first received packet |
| * and the current packet. |
| * |
| * At the receiver we have to deal with the jitter introduced by the network. |
| * |
| * recv_diff(i) = (Tri - Tr0) |
| * |
| * Tri : The time at the receiver at packet i |
| * Tr0 : The time at the receiver at the first packet |
| * |
| * Both of these values contain a jitter Ji, a jitter for packet i, so we can |
| * write: |
| * |
| * recv_diff(i) = (Cri + D + ni) - (Cr0 + D + n0)) |
| * |
| * Cri : The time of the clock at the receiver for packet i |
| * D + ni : The jitter when receiving packet i |
| * |
| * We see that the network delay is irrelevant here as we can elliminate D: |
| * |
| * recv_diff(i) = (Cri + ni) - (Cr0 + n0)) |
| * |
| * The drift is now expressed as: |
| * |
| * Drift(i) = recv_diff(i) - send_diff(i); |
| * |
| * We now keep the W latest values of Drift and find the minimum (this is the |
| * one with the lowest network jitter and thus the one which is least affected |
| * by it). We average this lowest value to smooth out the resulting network skew. |
| * |
| * Both the window and the weighting used for averaging influence the accuracy |
| * of the drift estimation. Finding the correct parameters turns out to be a |
| * compromise between accuracy and inertia. |
| * |
| * We use a 2 second window or up to 512 data points, which is statistically big |
| * enough to catch spikes (FIXME, detect spikes). |
| * We also use a rather large weighting factor (125) to smoothly adapt. During |
| * startup, when filling the window, we use a parabolic weighting factor, the |
| * more the window is filled, the faster we move to the detected possible skew. |
| * |
| * Returns: @time adjusted with the clock skew. |
| */ |
| static GstClockTime |
| calculate_skew (RTPJitterBuffer * jbuf, guint64 ext_rtptime, |
| GstClockTime gstrtptime, GstClockTime time) |
| { |
| guint64 send_diff, recv_diff; |
| gint64 delta; |
| gint64 old; |
| gint pos, i; |
| GstClockTime out_time; |
| guint64 slope; |
| |
| /* elapsed time at sender */ |
| send_diff = gstrtptime - jbuf->base_rtptime; |
| |
| /* we don't have an arrival timestamp so we can't do skew detection. we |
| * should still apply a timestamp based on RTP timestamp and base_time */ |
| if (time == -1 || jbuf->base_time == -1) |
| goto no_skew; |
| |
| /* elapsed time at receiver, includes the jitter */ |
| recv_diff = time - jbuf->base_time; |
| |
| /* measure the diff */ |
| delta = ((gint64) recv_diff) - ((gint64) send_diff); |
| |
| /* measure the slope, this gives a rought estimate between the sender speed |
| * and the receiver speed. This should be approximately 8, higher values |
| * indicate a burst (especially when the connection starts) */ |
| if (recv_diff > 0) |
| slope = (send_diff * 8) / recv_diff; |
| else |
| slope = 8; |
| |
| GST_DEBUG ("time %" GST_TIME_FORMAT ", base %" GST_TIME_FORMAT ", recv_diff %" |
| GST_TIME_FORMAT ", slope %" G_GUINT64_FORMAT, GST_TIME_ARGS (time), |
| GST_TIME_ARGS (jbuf->base_time), GST_TIME_ARGS (recv_diff), slope); |
| |
| /* if the difference between the sender timeline and the receiver timeline |
| * changed too quickly we have to resync because the server likely restarted |
| * its timestamps. */ |
| if (ABS (delta - jbuf->skew) > GST_SECOND) { |
| GST_WARNING ("delta - skew: %" GST_TIME_FORMAT " too big, reset skew", |
| GST_TIME_ARGS (ABS (delta - jbuf->skew))); |
| rtp_jitter_buffer_resync (jbuf, time, gstrtptime, ext_rtptime, TRUE); |
| send_diff = 0; |
| delta = 0; |
| } |
| |
| pos = jbuf->window_pos; |
| |
| if (G_UNLIKELY (jbuf->window_filling)) { |
| /* we are filling the window */ |
| GST_DEBUG ("filling %d, delta %" G_GINT64_FORMAT, pos, delta); |
| jbuf->window[pos++] = delta; |
| /* calc the min delta we observed */ |
| if (G_UNLIKELY (pos == 1 || delta < jbuf->window_min)) |
| jbuf->window_min = delta; |
| |
| if (G_UNLIKELY (send_diff >= MAX_TIME || pos >= MAX_WINDOW)) { |
| jbuf->window_size = pos; |
| |
| /* window filled */ |
| GST_DEBUG ("min %" G_GINT64_FORMAT, jbuf->window_min); |
| |
| /* the skew is now the min */ |
| jbuf->skew = jbuf->window_min; |
| jbuf->window_filling = FALSE; |
| } else { |
| gint perc_time, perc_window, perc; |
| |
| /* figure out how much we filled the window, this depends on the amount of |
| * time we have or the max number of points we keep. */ |
| perc_time = send_diff * 100 / MAX_TIME; |
| perc_window = pos * 100 / MAX_WINDOW; |
| perc = MAX (perc_time, perc_window); |
| |
| /* make a parabolic function, the closer we get to the MAX, the more value |
| * we give to the scaling factor of the new value */ |
| perc = perc * perc; |
| |
| /* quickly go to the min value when we are filling up, slowly when we are |
| * just starting because we're not sure it's a good value yet. */ |
| jbuf->skew = |
| (perc * jbuf->window_min + ((10000 - perc) * jbuf->skew)) / 10000; |
| jbuf->window_size = pos + 1; |
| } |
| } else { |
| /* pick old value and store new value. We keep the previous value in order |
| * to quickly check if the min of the window changed */ |
| old = jbuf->window[pos]; |
| jbuf->window[pos++] = delta; |
| |
| if (G_UNLIKELY (delta <= jbuf->window_min)) { |
| /* if the new value we inserted is smaller or equal to the current min, |
| * it becomes the new min */ |
| jbuf->window_min = delta; |
| } else if (G_UNLIKELY (old == jbuf->window_min)) { |
| gint64 min = G_MAXINT64; |
| |
| /* if we removed the old min, we have to find a new min */ |
| for (i = 0; i < jbuf->window_size; i++) { |
| /* we found another value equal to the old min, we can stop searching now */ |
| if (jbuf->window[i] == old) { |
| min = old; |
| break; |
| } |
| if (jbuf->window[i] < min) |
| min = jbuf->window[i]; |
| } |
| jbuf->window_min = min; |
| } |
| /* average the min values */ |
| jbuf->skew = (jbuf->window_min + (124 * jbuf->skew)) / 125; |
| GST_DEBUG ("delta %" G_GINT64_FORMAT ", new min: %" G_GINT64_FORMAT, |
| delta, jbuf->window_min); |
| } |
| /* wrap around in the window */ |
| if (G_UNLIKELY (pos >= jbuf->window_size)) |
| pos = 0; |
| jbuf->window_pos = pos; |
| |
| no_skew: |
| /* the output time is defined as the base timestamp plus the RTP time |
| * adjusted for the clock skew .*/ |
| if (jbuf->base_time != -1) { |
| out_time = jbuf->base_time + send_diff; |
| /* skew can be negative and we don't want to make invalid timestamps */ |
| if (jbuf->skew < 0 && out_time < -jbuf->skew) { |
| out_time = 0; |
| } else { |
| out_time += jbuf->skew; |
| } |
| } else |
| out_time = -1; |
| |
| GST_DEBUG ("skew %" G_GINT64_FORMAT ", out %" GST_TIME_FORMAT, |
| jbuf->skew, GST_TIME_ARGS (out_time)); |
| |
| return out_time; |
| } |
| |
| static void |
| queue_do_insert (RTPJitterBuffer * jbuf, GList * list, GList * item) |
| { |
| GQueue *queue = jbuf->packets; |
| |
| /* It's more likely that the packet was inserted at the tail of the queue */ |
| if (G_LIKELY (list)) { |
| item->prev = list; |
| item->next = list->next; |
| list->next = item; |
| } else { |
| item->prev = NULL; |
| item->next = queue->head; |
| queue->head = item; |
| } |
| if (item->next) |
| item->next->prev = item; |
| else |
| queue->tail = item; |
| queue->length++; |
| } |
| |
| GstClockTime |
| rtp_jitter_buffer_calculate_pts (RTPJitterBuffer * jbuf, GstClockTime dts, |
| gboolean estimated_dts, guint32 rtptime, GstClockTime base_time) |
| { |
| guint64 ext_rtptime; |
| GstClockTime gstrtptime, pts; |
| GstClock *media_clock, *pipeline_clock; |
| guint64 media_clock_offset; |
| gboolean rfc7273_mode; |
| |
| /* rtp time jumps are checked for during skew calculation, but bypassed |
| * in other mode, so mind those here and reset jb if needed. |
| * Only reset if valid input time, which is likely for UDP input |
| * where we expect this might happen due to async thread effects |
| * (in seek and state change cycles), but not so much for TCP input */ |
| if (GST_CLOCK_TIME_IS_VALID (dts) && !estimated_dts && |
| jbuf->mode != RTP_JITTER_BUFFER_MODE_SLAVE && |
| jbuf->base_time != -1 && jbuf->last_rtptime != -1) { |
| GstClockTime ext_rtptime = jbuf->ext_rtptime; |
| |
| ext_rtptime = gst_rtp_buffer_ext_timestamp (&ext_rtptime, rtptime); |
| if (ext_rtptime > jbuf->last_rtptime + 3 * jbuf->clock_rate || |
| ext_rtptime + 3 * jbuf->clock_rate < jbuf->last_rtptime) { |
| /* reset even if we don't have valid incoming time; |
| * still better than producing possibly very bogus output timestamp */ |
| GST_WARNING ("rtp delta too big, reset skew"); |
| rtp_jitter_buffer_reset_skew (jbuf); |
| } |
| } |
| |
| /* Return the last time if we got the same RTP timestamp again */ |
| ext_rtptime = gst_rtp_buffer_ext_timestamp (&jbuf->ext_rtptime, rtptime); |
| if (jbuf->last_rtptime != -1 && ext_rtptime == jbuf->last_rtptime) { |
| return jbuf->prev_out_time; |
| } |
| |
| /* keep track of the last extended rtptime */ |
| jbuf->last_rtptime = ext_rtptime; |
| |
| g_mutex_lock (&jbuf->clock_lock); |
| media_clock = jbuf->media_clock ? gst_object_ref (jbuf->media_clock) : NULL; |
| pipeline_clock = |
| jbuf->pipeline_clock ? gst_object_ref (jbuf->pipeline_clock) : NULL; |
| media_clock_offset = jbuf->media_clock_offset; |
| g_mutex_unlock (&jbuf->clock_lock); |
| |
| gstrtptime = |
| gst_util_uint64_scale_int (ext_rtptime, GST_SECOND, jbuf->clock_rate); |
| |
| if (G_LIKELY (jbuf->base_rtptime != -1)) { |
| /* check elapsed time in RTP units */ |
| if (gstrtptime < jbuf->base_rtptime) { |
| /* elapsed time at sender, timestamps can go backwards and thus be |
| * smaller than our base time, schedule to take a new base time in |
| * that case. */ |
| GST_WARNING ("backward timestamps at server, schedule resync"); |
| jbuf->need_resync = TRUE; |
| } |
| } |
| |
| switch (jbuf->mode) { |
| case RTP_JITTER_BUFFER_MODE_NONE: |
| case RTP_JITTER_BUFFER_MODE_BUFFER: |
| /* send 0 as the first timestamp and -1 for the other ones. This will |
| * interpolate them from the RTP timestamps with a 0 origin. In buffering |
| * mode we will adjust the outgoing timestamps according to the amount of |
| * time we spent buffering. */ |
| if (jbuf->base_time == -1) |
| dts = 0; |
| else |
| dts = -1; |
| break; |
| case RTP_JITTER_BUFFER_MODE_SYNCED: |
| /* synchronized clocks, take first timestamp as base, use RTP timestamps |
| * to interpolate */ |
| if (jbuf->base_time != -1 && !jbuf->need_resync) |
| dts = -1; |
| break; |
| case RTP_JITTER_BUFFER_MODE_SLAVE: |
| default: |
| break; |
| } |
| |
| /* need resync, lock on to time and gstrtptime if we can, otherwise we |
| * do with the previous values */ |
| if (G_UNLIKELY (jbuf->need_resync && dts != -1)) { |
| GST_INFO ("resync to time %" GST_TIME_FORMAT ", rtptime %" |
| GST_TIME_FORMAT, GST_TIME_ARGS (dts), GST_TIME_ARGS (gstrtptime)); |
| rtp_jitter_buffer_resync (jbuf, dts, gstrtptime, ext_rtptime, FALSE); |
| } |
| |
| GST_DEBUG ("extrtp %" G_GUINT64_FORMAT ", gstrtp %" GST_TIME_FORMAT ", base %" |
| GST_TIME_FORMAT ", send_diff %" GST_TIME_FORMAT, ext_rtptime, |
| GST_TIME_ARGS (gstrtptime), GST_TIME_ARGS (jbuf->base_rtptime), |
| GST_TIME_ARGS (gstrtptime - jbuf->base_rtptime)); |
| |
| rfc7273_mode = media_clock && pipeline_clock |
| && gst_clock_is_synced (media_clock); |
| |
| if (rfc7273_mode && jbuf->mode == RTP_JITTER_BUFFER_MODE_SLAVE |
| && (media_clock_offset == -1 || !jbuf->rfc7273_sync)) { |
| GstClockTime internal, external; |
| GstClockTime rate_num, rate_denom; |
| GstClockTime nsrtptimediff, rtpntptime, rtpsystime; |
| |
| gst_clock_get_calibration (media_clock, &internal, &external, &rate_num, |
| &rate_denom); |
| |
| /* Slave to the RFC7273 media clock instead of trying to estimate it |
| * based on receive times and RTP timestamps */ |
| |
| if (jbuf->media_clock_base_time == -1) { |
| if (jbuf->base_time != -1) { |
| jbuf->media_clock_base_time = |
| gst_clock_unadjust_with_calibration (media_clock, |
| jbuf->base_time + base_time, internal, external, rate_num, |
| rate_denom); |
| } else { |
| if (dts != -1) |
| jbuf->media_clock_base_time = |
| gst_clock_unadjust_with_calibration (media_clock, dts + base_time, |
| internal, external, rate_num, rate_denom); |
| else |
| jbuf->media_clock_base_time = |
| gst_clock_get_internal_time (media_clock); |
| jbuf->base_rtptime = gstrtptime; |
| } |
| } |
| |
| if (gstrtptime > jbuf->base_rtptime) |
| nsrtptimediff = gstrtptime - jbuf->base_rtptime; |
| else |
| nsrtptimediff = 0; |
| |
| rtpntptime = nsrtptimediff + jbuf->media_clock_base_time; |
| |
| rtpsystime = |
| gst_clock_adjust_with_calibration (media_clock, rtpntptime, internal, |
| external, rate_num, rate_denom); |
| |
| if (rtpsystime > base_time) |
| pts = rtpsystime - base_time; |
| else |
| pts = 0; |
| |
| GST_DEBUG ("RFC7273 clock time %" GST_TIME_FORMAT ", out %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (rtpsystime), GST_TIME_ARGS (pts)); |
| } else if (rfc7273_mode && (jbuf->mode == RTP_JITTER_BUFFER_MODE_SLAVE |
| || jbuf->mode == RTP_JITTER_BUFFER_MODE_SYNCED) |
| && media_clock_offset != -1 && jbuf->rfc7273_sync) { |
| GstClockTime ntptime, rtptime_tmp; |
| GstClockTime ntprtptime, rtpsystime; |
| GstClockTime internal, external; |
| GstClockTime rate_num, rate_denom; |
| |
| /* Don't do any of the dts related adjustments further down */ |
| dts = -1; |
| |
| /* Calculate the actual clock time on the sender side based on the |
| * RFC7273 clock and convert it to our pipeline clock |
| */ |
| |
| gst_clock_get_calibration (media_clock, &internal, &external, &rate_num, |
| &rate_denom); |
| |
| ntptime = gst_clock_get_internal_time (media_clock); |
| |
| ntprtptime = gst_util_uint64_scale (ntptime, jbuf->clock_rate, GST_SECOND); |
| ntprtptime += media_clock_offset; |
| ntprtptime &= 0xffffffff; |
| |
| rtptime_tmp = rtptime; |
| /* Check for wraparounds, we assume that the diff between current RTP |
| * timestamp and current media clock time can't be bigger than |
| * 2**31 clock units */ |
| if (ntprtptime > rtptime_tmp && ntprtptime - rtptime_tmp >= 0x80000000) |
| rtptime_tmp += G_GUINT64_CONSTANT (0x100000000); |
| else if (rtptime_tmp > ntprtptime && rtptime_tmp - ntprtptime >= 0x80000000) |
| ntprtptime += G_GUINT64_CONSTANT (0x100000000); |
| |
| if (ntprtptime > rtptime_tmp) |
| ntptime -= |
| gst_util_uint64_scale (ntprtptime - rtptime_tmp, jbuf->clock_rate, |
| GST_SECOND); |
| else |
| ntptime += |
| gst_util_uint64_scale (rtptime_tmp - ntprtptime, jbuf->clock_rate, |
| GST_SECOND); |
| |
| rtpsystime = |
| gst_clock_adjust_with_calibration (media_clock, ntptime, internal, |
| external, rate_num, rate_denom); |
| /* All this assumes that the pipeline has enough additional |
| * latency to cover for the network delay */ |
| if (rtpsystime > base_time) |
| pts = rtpsystime - base_time; |
| else |
| pts = 0; |
| |
| GST_DEBUG ("RFC7273 clock time %" GST_TIME_FORMAT ", out %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (rtpsystime), GST_TIME_ARGS (pts)); |
| } else { |
| /* If we used the RFC7273 clock before and not anymore, |
| * we need to resync it later again */ |
| jbuf->media_clock_base_time = -1; |
| |
| /* do skew calculation by measuring the difference between rtptime and the |
| * receive dts, this function will return the skew corrected rtptime. */ |
| pts = calculate_skew (jbuf, ext_rtptime, gstrtptime, dts); |
| } |
| |
| /* check if timestamps are not going backwards, we can only check this if we |
| * have a previous out time and a previous send_diff */ |
| if (G_LIKELY (pts != -1 && jbuf->prev_out_time != -1 |
| && jbuf->prev_send_diff != -1)) { |
| /* now check for backwards timestamps */ |
| if (G_UNLIKELY ( |
| /* if the server timestamps went up and the out_time backwards */ |
| (gstrtptime - jbuf->base_rtptime > jbuf->prev_send_diff |
| && pts < jbuf->prev_out_time) || |
| /* if the server timestamps went backwards and the out_time forwards */ |
| (gstrtptime - jbuf->base_rtptime < jbuf->prev_send_diff |
| && pts > jbuf->prev_out_time) || |
| /* if the server timestamps did not change */ |
| gstrtptime - jbuf->base_rtptime == jbuf->prev_send_diff)) { |
| GST_DEBUG ("backwards timestamps, using previous time"); |
| pts = jbuf->prev_out_time; |
| } |
| } |
| |
| if (dts != -1 && pts + jbuf->delay < dts) { |
| /* if we are going to produce a timestamp that is later than the input |
| * timestamp, we need to reset the jitterbuffer. Likely the server paused |
| * temporarily */ |
| GST_DEBUG ("out %" GST_TIME_FORMAT " + %" G_GUINT64_FORMAT " < time %" |
| GST_TIME_FORMAT ", reset jitterbuffer", GST_TIME_ARGS (pts), |
| jbuf->delay, GST_TIME_ARGS (dts)); |
| rtp_jitter_buffer_resync (jbuf, dts, gstrtptime, ext_rtptime, TRUE); |
| pts = dts; |
| } |
| |
| jbuf->prev_out_time = pts; |
| jbuf->prev_send_diff = gstrtptime - jbuf->base_rtptime; |
| |
| if (media_clock) |
| gst_object_unref (media_clock); |
| if (pipeline_clock) |
| gst_object_unref (pipeline_clock); |
| |
| return pts; |
| } |
| |
| |
| /** |
| * rtp_jitter_buffer_insert: |
| * @jbuf: an #RTPJitterBuffer |
| * @item: an #RTPJitterBufferItem to insert |
| * @head: TRUE when the head element changed. |
| * @percent: the buffering percent after insertion |
| * |
| * Inserts @item into the packet queue of @jbuf. The sequence number of the |
| * packet will be used to sort the packets. This function takes ownerhip of |
| * @buf when the function returns %TRUE. |
| * |
| * When @head is %TRUE, the new packet was added at the head of the queue and |
| * will be available with the next call to rtp_jitter_buffer_pop() and |
| * rtp_jitter_buffer_peek(). |
| * |
| * Returns: %FALSE if a packet with the same number already existed. |
| */ |
| gboolean |
| rtp_jitter_buffer_insert (RTPJitterBuffer * jbuf, RTPJitterBufferItem * item, |
| gboolean * head, gint * percent) |
| { |
| GList *list, *event = NULL; |
| guint16 seqnum; |
| |
| g_return_val_if_fail (jbuf != NULL, FALSE); |
| g_return_val_if_fail (item != NULL, FALSE); |
| |
| list = jbuf->packets->tail; |
| |
| /* no seqnum, simply append then */ |
| if (item->seqnum == -1) |
| goto append; |
| |
| seqnum = item->seqnum; |
| |
| /* loop the list to skip strictly larger seqnum buffers */ |
| for (; list; list = g_list_previous (list)) { |
| guint16 qseq; |
| gint gap; |
| RTPJitterBufferItem *qitem = (RTPJitterBufferItem *) list; |
| |
| if (qitem->seqnum == -1) { |
| /* keep a pointer to the first consecutive event if not already |
| * set. we will insert the packet after the event if we can't find |
| * a packet with lower sequence number before the event. */ |
| if (event == NULL) |
| event = list; |
| continue; |
| } |
| |
| qseq = qitem->seqnum; |
| |
| /* compare the new seqnum to the one in the buffer */ |
| gap = gst_rtp_buffer_compare_seqnum (seqnum, qseq); |
| |
| /* we hit a packet with the same seqnum, notify a duplicate */ |
| if (G_UNLIKELY (gap == 0)) |
| goto duplicate; |
| |
| /* seqnum > qseq, we can stop looking */ |
| if (G_LIKELY (gap < 0)) |
| break; |
| |
| /* if we've found a packet with greater sequence number, cleanup the |
| * event pointer as the packet will be inserted before the event */ |
| event = NULL; |
| } |
| |
| /* if event is set it means that packets before the event had smaller |
| * sequence number, so we will insert our packet after the event */ |
| if (event) |
| list = event; |
| |
| append: |
| queue_do_insert (jbuf, list, (GList *) item); |
| |
| /* buffering mode, update buffer stats */ |
| if (jbuf->mode == RTP_JITTER_BUFFER_MODE_BUFFER) |
| update_buffer_level (jbuf, percent); |
| else if (percent) |
| *percent = -1; |
| |
| /* head was changed when we did not find a previous packet, we set the return |
| * flag when requested. */ |
| if (G_LIKELY (head)) |
| *head = (list == NULL); |
| |
| return TRUE; |
| |
| /* ERRORS */ |
| duplicate: |
| { |
| GST_DEBUG ("duplicate packet %d found", (gint) seqnum); |
| if (G_LIKELY (head)) |
| *head = FALSE; |
| return FALSE; |
| } |
| } |
| |
| /** |
| * rtp_jitter_buffer_pop: |
| * @jbuf: an #RTPJitterBuffer |
| * @percent: the buffering percent |
| * |
| * Pops the oldest buffer from the packet queue of @jbuf. The popped buffer will |
| * have its timestamp adjusted with the incomming running_time and the detected |
| * clock skew. |
| * |
| * Returns: a #GstBuffer or %NULL when there was no packet in the queue. |
| */ |
| RTPJitterBufferItem * |
| rtp_jitter_buffer_pop (RTPJitterBuffer * jbuf, gint * percent) |
| { |
| GList *item = NULL; |
| GQueue *queue; |
| |
| g_return_val_if_fail (jbuf != NULL, NULL); |
| |
| queue = jbuf->packets; |
| |
| item = queue->head; |
| if (item) { |
| queue->head = item->next; |
| if (queue->head) |
| queue->head->prev = NULL; |
| else |
| queue->tail = NULL; |
| queue->length--; |
| } |
| |
| /* buffering mode, update buffer stats */ |
| if (jbuf->mode == RTP_JITTER_BUFFER_MODE_BUFFER) |
| update_buffer_level (jbuf, percent); |
| else if (percent) |
| *percent = -1; |
| |
| return (RTPJitterBufferItem *) item; |
| } |
| |
| /** |
| * rtp_jitter_buffer_peek: |
| * @jbuf: an #RTPJitterBuffer |
| * |
| * Peek the oldest buffer from the packet queue of @jbuf. |
| * |
| * See rtp_jitter_buffer_insert() to check when an older packet was |
| * added. |
| * |
| * Returns: a #GstBuffer or %NULL when there was no packet in the queue. |
| */ |
| RTPJitterBufferItem * |
| rtp_jitter_buffer_peek (RTPJitterBuffer * jbuf) |
| { |
| g_return_val_if_fail (jbuf != NULL, NULL); |
| |
| return (RTPJitterBufferItem *) jbuf->packets->head; |
| } |
| |
| /** |
| * rtp_jitter_buffer_flush: |
| * @jbuf: an #RTPJitterBuffer |
| * @free_func: function to free each item |
| * @user_data: user data passed to @free_func |
| * |
| * Flush all packets from the jitterbuffer. |
| */ |
| void |
| rtp_jitter_buffer_flush (RTPJitterBuffer * jbuf, GFunc free_func, |
| gpointer user_data) |
| { |
| GList *item; |
| |
| g_return_if_fail (jbuf != NULL); |
| g_return_if_fail (free_func != NULL); |
| |
| while ((item = g_queue_pop_head_link (jbuf->packets))) |
| free_func ((RTPJitterBufferItem *) item, user_data); |
| } |
| |
| /** |
| * rtp_jitter_buffer_is_buffering: |
| * @jbuf: an #RTPJitterBuffer |
| * |
| * Check if @jbuf is buffering currently. Users of the jitterbuffer should not |
| * pop packets while in buffering mode. |
| * |
| * Returns: the buffering state of @jbuf |
| */ |
| gboolean |
| rtp_jitter_buffer_is_buffering (RTPJitterBuffer * jbuf) |
| { |
| return jbuf->buffering && !jbuf->buffering_disabled; |
| } |
| |
| /** |
| * rtp_jitter_buffer_set_buffering: |
| * @jbuf: an #RTPJitterBuffer |
| * @buffering: the new buffering state |
| * |
| * Forces @jbuf to go into the buffering state. |
| */ |
| void |
| rtp_jitter_buffer_set_buffering (RTPJitterBuffer * jbuf, gboolean buffering) |
| { |
| jbuf->buffering = buffering; |
| } |
| |
| /** |
| * rtp_jitter_buffer_get_percent: |
| * @jbuf: an #RTPJitterBuffer |
| * |
| * Get the buffering percent of the jitterbuffer. |
| * |
| * Returns: the buffering percent |
| */ |
| gint |
| rtp_jitter_buffer_get_percent (RTPJitterBuffer * jbuf) |
| { |
| gint percent; |
| guint64 level; |
| |
| if (G_UNLIKELY (jbuf->high_level == 0)) |
| return 100; |
| |
| if (G_UNLIKELY (jbuf->buffering_disabled)) |
| return 100; |
| |
| level = get_buffer_level (jbuf); |
| percent = (level * 100 / jbuf->high_level); |
| percent = MIN (percent, 100); |
| |
| return percent; |
| } |
| |
| /** |
| * rtp_jitter_buffer_num_packets: |
| * @jbuf: an #RTPJitterBuffer |
| * |
| * Get the number of packets currently in "jbuf. |
| * |
| * Returns: The number of packets in @jbuf. |
| */ |
| guint |
| rtp_jitter_buffer_num_packets (RTPJitterBuffer * jbuf) |
| { |
| g_return_val_if_fail (jbuf != NULL, 0); |
| |
| return jbuf->packets->length; |
| } |
| |
| /** |
| * rtp_jitter_buffer_get_ts_diff: |
| * @jbuf: an #RTPJitterBuffer |
| * |
| * Get the difference between the timestamps of first and last packet in the |
| * jitterbuffer. |
| * |
| * Returns: The difference expressed in the timestamp units of the packets. |
| */ |
| guint32 |
| rtp_jitter_buffer_get_ts_diff (RTPJitterBuffer * jbuf) |
| { |
| guint64 high_ts, low_ts; |
| RTPJitterBufferItem *high_buf, *low_buf; |
| guint32 result; |
| |
| g_return_val_if_fail (jbuf != NULL, 0); |
| |
| high_buf = (RTPJitterBufferItem *) g_queue_peek_tail_link (jbuf->packets); |
| low_buf = (RTPJitterBufferItem *) g_queue_peek_head_link (jbuf->packets); |
| |
| if (!high_buf || !low_buf || high_buf == low_buf) |
| return 0; |
| |
| high_ts = high_buf->rtptime; |
| low_ts = low_buf->rtptime; |
| |
| /* it needs to work if ts wraps */ |
| if (high_ts >= low_ts) { |
| result = (guint32) (high_ts - low_ts); |
| } else { |
| result = (guint32) (high_ts + G_MAXUINT32 + 1 - low_ts); |
| } |
| return result; |
| } |
| |
| /** |
| * rtp_jitter_buffer_get_sync: |
| * @jbuf: an #RTPJitterBuffer |
| * @rtptime: result RTP time |
| * @timestamp: result GStreamer timestamp |
| * @clock_rate: clock-rate of @rtptime |
| * @last_rtptime: last seen rtptime. |
| * |
| * Calculates the relation between the RTP timestamp and the GStreamer timestamp |
| * used for constructing timestamps. |
| * |
| * For extended RTP timestamp @rtptime with a clock-rate of @clock_rate, |
| * the GStreamer timestamp is currently @timestamp. |
| * |
| * The last seen extended RTP timestamp with clock-rate @clock-rate is returned in |
| * @last_rtptime. |
| */ |
| void |
| rtp_jitter_buffer_get_sync (RTPJitterBuffer * jbuf, guint64 * rtptime, |
| guint64 * timestamp, guint32 * clock_rate, guint64 * last_rtptime) |
| { |
| if (rtptime) |
| *rtptime = jbuf->base_extrtp; |
| if (timestamp) |
| *timestamp = jbuf->base_time + jbuf->skew; |
| if (clock_rate) |
| *clock_rate = jbuf->clock_rate; |
| if (last_rtptime) |
| *last_rtptime = jbuf->last_rtptime; |
| } |
| |
| /** |
| * rtp_jitter_buffer_can_fast_start: |
| * @jbuf: an #RTPJitterBuffer |
| * @num_packets: Number of consecutive packets needed |
| * |
| * Check if in the queue if there is enough packets with consecutive seqnum in |
| * order to start delivering them. |
| * |
| * Returns: %TRUE if the required number of consecutive packets was found. |
| */ |
| gboolean |
| rtp_jitter_buffer_can_fast_start (RTPJitterBuffer * jbuf, gint num_packet) |
| { |
| gboolean ret = TRUE; |
| RTPJitterBufferItem *last_item = NULL, *item; |
| gint i; |
| |
| if (rtp_jitter_buffer_num_packets (jbuf) < num_packet) |
| return FALSE; |
| |
| item = rtp_jitter_buffer_peek (jbuf); |
| for (i = 0; i < num_packet; i++) { |
| if (G_LIKELY (last_item)) { |
| guint16 expected_seqnum = last_item->seqnum + 1; |
| |
| if (expected_seqnum != item->seqnum) { |
| ret = FALSE; |
| break; |
| } |
| } |
| |
| last_item = item; |
| item = (RTPJitterBufferItem *) last_item->next; |
| } |
| |
| return ret; |
| } |