From a9916460f401f7456bc337ed071a25f43ef6e387 Mon Sep 17 00:00:00 2001 From: Mathieu Duponchelle Date: Mon, 9 Dec 2019 22:00:58 +0100 Subject: [PATCH] threadshare/jitterbuffer: Import C tests from rtpjitterbuffer --- Cargo.toml | 1 + gst-plugin-threadshare/ctests/Cargo.toml | 18 + gst-plugin-threadshare/ctests/build.rs | 35 + .../ctests/tests/run_ctests.rs | 25 + .../ctests/tests/test-jitterbuffer.c | 1324 +++++++++++++++++ 5 files changed, 1403 insertions(+) create mode 100644 gst-plugin-threadshare/ctests/Cargo.toml create mode 100644 gst-plugin-threadshare/ctests/build.rs create mode 100644 gst-plugin-threadshare/ctests/tests/run_ctests.rs create mode 100644 gst-plugin-threadshare/ctests/tests/test-jitterbuffer.c diff --git a/Cargo.toml b/Cargo.toml index a1539545..c08f4175 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "gst-plugin-audiofx", "gst-plugin-togglerecord", "gst-plugin-threadshare", + "gst-plugin-threadshare/ctests", "gst-plugin-tutorial", "gst-plugin-closedcaption", "gst-plugin-version-helper", diff --git a/gst-plugin-threadshare/ctests/Cargo.toml b/gst-plugin-threadshare/ctests/Cargo.toml new file mode 100644 index 00000000..a9c545be --- /dev/null +++ b/gst-plugin-threadshare/ctests/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "gst-plugin-threadshare-ctests" +version = "0.6.0" +authors = ["Mathieu Duponchelle "] +license = "LGPL-2.1+" +description = "Threadshare C tests" +publish = false +build = "build.rs" +repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" + +[dependencies] +gstreamer = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +gst-plugin-threadshare = { path=".." } + +[build-dependencies] +gst-plugin-version-helper = { path="../../gst-plugin-version-helper" } +cc = "1.0.38" +pkg-config = "0.3.15" diff --git a/gst-plugin-threadshare/ctests/build.rs b/gst-plugin-threadshare/ctests/build.rs new file mode 100644 index 00000000..60994efe --- /dev/null +++ b/gst-plugin-threadshare/ctests/build.rs @@ -0,0 +1,35 @@ +extern crate cc; +extern crate gst_plugin_version_helper; +extern crate pkg_config; + +pub fn main() { + let libs = [ + pkg_config::probe_library("gstreamer-1.0").unwrap(), + pkg_config::probe_library("gstreamer-check-1.0").unwrap(), + pkg_config::probe_library("gstreamer-rtp-1.0").unwrap(), + ]; + + let mut build = cc::Build::new(); + + for lib in &libs { + for path in &lib.include_paths { + build.include(path); + } + } + + build.file("tests/test-jitterbuffer.c"); + + build.extra_warnings(false); + + build.compile("threadshare_ctests"); + + /* See https://github.com/rust-lang/rust/issues/41416#issuecomment-299356387 for + * why we need to do this */ + for lib in &libs { + for l in &lib.libs { + println!("cargo:rustc-link-lib={}", l); + } + } + + gst_plugin_version_helper::get_info() +} diff --git a/gst-plugin-threadshare/ctests/tests/run_ctests.rs b/gst-plugin-threadshare/ctests/tests/run_ctests.rs new file mode 100644 index 00000000..2e34e5ba --- /dev/null +++ b/gst-plugin-threadshare/ctests/tests/run_ctests.rs @@ -0,0 +1,25 @@ +extern crate gstreamer as gst; +extern crate gstthreadshare; + +extern "C" { + fn run_ctests() -> i32; +} + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstthreadshare::plugin_register_static().expect("gstthreadshare appsrc test"); + }); +} + +#[test] +pub fn run_tests() { + init(); + + unsafe { + assert!(run_ctests() == 0); + } +} diff --git a/gst-plugin-threadshare/ctests/tests/test-jitterbuffer.c b/gst-plugin-threadshare/ctests/tests/test-jitterbuffer.c new file mode 100644 index 00000000..c88c847c --- /dev/null +++ b/gst-plugin-threadshare/ctests/tests/test-jitterbuffer.c @@ -0,0 +1,1324 @@ +/* GStreamer + * + * Copyright (C) 2009 Nokia Corporation and its subsidiary(-ies) + * contact: + * Copyright (C) 2012 Cisco Systems, Inc + * Authors: Kelley Rogers + * Havard Graff + * Copyright (C) 2013-2016 Pexip AS + * Stian Selnes + * Havard Graff + * + * 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 +#include +#include + +#include + +/* For ease of programming we use globals to keep refs for our floating + * src and sink pads we create; otherwise we always have to do get_pad, + * get_peer, and then remove references in every test function */ +static GstPad *mysrcpad, *mysinkpad; +/* we also have a list of src buffers */ +static GList *inbuffers = NULL; +static gint num_dropped = 0; + +#define RTP_CAPS_STRING \ + "application/x-rtp, " \ + "media = (string)audio, " \ + "payload = (int) 0, " \ + "clock-rate = (int) 8000, " \ + "encoding-name = (string)PCMU" + +#define RTP_FRAME_SIZE 20 + +static GstStaticPadTemplate sinktemplate = GST_STATIC_PAD_TEMPLATE ("sink", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("application/x-rtp") + ); +static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("application/x-rtp, " + "clock-rate = (int) [ 1, 2147483647 ]") + ); + +static void +buffer_dropped (G_GNUC_UNUSED gpointer data, G_GNUC_UNUSED GstMiniObject * obj) +{ + num_dropped++; +} + +static GstElement * +setup_jitterbuffer (gint num_buffers) +{ + GstElement *jitterbuffer; + GstClock *clock; + GstBuffer *buffer; + GstCaps *caps; + /* a 20 sample audio block (2,5 ms) generated with + * gst-launch audiotestsrc wave=silence blocksize=40 num-buffers=3 ! + * "audio/x-raw,channels=1,rate=8000" ! mulawenc ! rtppcmupay ! + * fakesink dump=1 + */ + guint8 in[] = { + /* first 4 bytes are rtp-header, next 4 bytes are timestamp */ + 0x80, 0x80, 0x1c, 0x24, 0x46, 0xcd, 0xb7, 0x11, 0x3c, 0x3a, 0x7c, 0x5b, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff + }; + GstClockTime ts = G_GUINT64_CONSTANT (0); + GstClockTime tso = gst_util_uint64_scale (RTP_FRAME_SIZE, GST_SECOND, 8000); + /*guint latency = GST_TIME_AS_MSECONDS (num_buffers * tso); */ + gint i; + + GST_DEBUG ("setup_jitterbuffer"); + jitterbuffer = gst_check_setup_element ("ts-jitterbuffer"); + + /* we need a clock here */ + clock = gst_system_clock_obtain (); + gst_element_set_clock (jitterbuffer, clock); + gst_object_unref (clock); + /* setup latency */ + /* latency would be 7 for 3 buffers here, default is 200 + g_object_set (G_OBJECT (jitterbuffer), "latency", latency, NULL); + GST_INFO_OBJECT (jitterbuffer, "set latency to %u ms", latency); + */ + + mysrcpad = gst_check_setup_src_pad (jitterbuffer, &srctemplate); + mysinkpad = gst_check_setup_sink_pad (jitterbuffer, &sinktemplate); + gst_pad_set_active (mysrcpad, TRUE); + gst_pad_set_active (mysinkpad, TRUE); + + /* create n buffers */ + caps = gst_caps_from_string (RTP_CAPS_STRING); + gst_check_setup_events (mysrcpad, jitterbuffer, caps, GST_FORMAT_TIME); + gst_caps_unref (caps); + + for (i = 0; i < num_buffers; i++) { + buffer = gst_buffer_new_and_alloc (sizeof (in)); + gst_buffer_fill (buffer, 0, in, sizeof (in)); + GST_BUFFER_DTS (buffer) = ts; + GST_BUFFER_PTS (buffer) = ts; + GST_BUFFER_DURATION (buffer) = tso; + gst_mini_object_weak_ref (GST_MINI_OBJECT (buffer), buffer_dropped, NULL); + GST_DEBUG ("created buffer: %p", buffer); + + if (!i) + GST_BUFFER_FLAG_SET (buffer, GST_BUFFER_FLAG_DISCONT); + + inbuffers = g_list_append (inbuffers, buffer); + + /* hackish way to update the rtp header */ + in[1] = 0x00; + in[3]++; /* seqnumber */ + in[7] += RTP_FRAME_SIZE; /* inc. timestamp with framesize */ + ts += tso; + } + num_dropped = 0; + + return jitterbuffer; +} + +static GstStateChangeReturn +start_jitterbuffer (GstElement * jitterbuffer) +{ + GstStateChangeReturn ret; + GstClockTime now; + GstClock *clock; + + clock = gst_element_get_clock (jitterbuffer); + now = gst_clock_get_time (clock); + gst_object_unref (clock); + + gst_element_set_base_time (jitterbuffer, now); + ret = gst_element_set_state (jitterbuffer, GST_STATE_PLAYING); + + return ret; +} + +static void +cleanup_jitterbuffer (GstElement * jitterbuffer) +{ + GST_DEBUG ("cleanup_jitterbuffer"); + + g_list_foreach (buffers, (GFunc) gst_mini_object_unref, NULL); + g_list_free (buffers); + buffers = NULL; + + g_list_free (inbuffers); + inbuffers = NULL; + + gst_pad_set_active (mysrcpad, FALSE); + gst_pad_set_active (mysinkpad, FALSE); + gst_element_set_state (jitterbuffer, GST_STATE_NULL); + gst_check_teardown_src_pad (jitterbuffer); + gst_check_teardown_sink_pad (jitterbuffer); + gst_check_teardown_element (jitterbuffer); +} + +static void +check_jitterbuffer_results (gint num_buffers) +{ + GstBuffer *buffer; + GList *node; + GstClockTime ts = G_GUINT64_CONSTANT (0); + GstClockTime tso = gst_util_uint64_scale (RTP_FRAME_SIZE, GST_SECOND, 8000); + GstMapInfo map; + guint16 prev_sn = 0, cur_sn; + guint32 prev_ts = 0, cur_ts; + + /* sleep for twice the latency */ + g_usleep (400 * 1000); + + GST_DEBUG ("of %d buffer %d/%d received/dropped", num_buffers, + g_list_length (buffers), num_dropped); + /* if this fails, not all buffers have been processed */ + fail_unless_equals_int ((g_list_length (buffers) + num_dropped), num_buffers); + + /* check the buffer list */ + fail_unless_equals_int (g_list_length (buffers), num_buffers); + for (node = buffers; node; node = g_list_next (node)) { + fail_if ((buffer = (GstBuffer *) node->data) == NULL); + fail_if (GST_BUFFER_PTS (buffer) != ts); + gst_buffer_map (buffer, &map, GST_MAP_READ); + cur_sn = ((guint16) map.data[2] << 8) | map.data[3]; + cur_ts = ((guint32) map.data[4] << 24) | ((guint32) map.data[5] << 16) | + ((guint32) map.data[6] << 8) | map.data[7]; + gst_buffer_unmap (buffer, &map); + + if (node != buffers) { + fail_unless (cur_sn > prev_sn); + fail_unless (cur_ts > prev_ts); + + prev_sn = cur_sn; + prev_ts = cur_ts; + } + ts += tso; + } +} + +GST_START_TEST (test_push_forward_seq) +{ + GstElement *jitterbuffer; + const guint num_buffers = 3; + GstBuffer *buffer; + GList *node; + + jitterbuffer = setup_jitterbuffer (num_buffers); + fail_unless (start_jitterbuffer (jitterbuffer) + == GST_STATE_CHANGE_SUCCESS, "could not set to playing"); + + /* push buffers: 0,1,2, */ + for (node = inbuffers; node; node = g_list_next (node)) { + buffer = (GstBuffer *) node->data; + fail_unless (gst_pad_push (mysrcpad, buffer) == GST_FLOW_OK); + } + + /* check the buffer list */ + check_jitterbuffer_results (num_buffers); + + /* cleanup */ + cleanup_jitterbuffer (jitterbuffer); +} + +GST_END_TEST; + +GST_START_TEST (test_push_backward_seq) +{ + GstElement *jitterbuffer; + const guint num_buffers = 4; + GstBuffer *buffer; + GList *node; + + jitterbuffer = setup_jitterbuffer (num_buffers); + fail_unless (start_jitterbuffer (jitterbuffer) + == GST_STATE_CHANGE_SUCCESS, "could not set to playing"); + + /* push buffers: 0,3,2,1 */ + buffer = (GstBuffer *) inbuffers->data; + fail_unless (gst_pad_push (mysrcpad, buffer) == GST_FLOW_OK); + for (node = g_list_last (inbuffers); node != inbuffers; + node = g_list_previous (node)) { + buffer = (GstBuffer *) node->data; + fail_unless (gst_pad_push (mysrcpad, buffer) == GST_FLOW_OK); + } + + /* check the buffer list */ + check_jitterbuffer_results (num_buffers); + + /* cleanup */ + cleanup_jitterbuffer (jitterbuffer); +} + +GST_END_TEST; + +GST_START_TEST (test_push_unordered) +{ + GstElement *jitterbuffer; + const guint num_buffers = 4; + GstBuffer *buffer; + + jitterbuffer = setup_jitterbuffer (num_buffers); + fail_unless (start_jitterbuffer (jitterbuffer) + == GST_STATE_CHANGE_SUCCESS, "could not set to playing"); + + /* push buffers; 0,2,1,3 */ + buffer = (GstBuffer *) inbuffers->data; + fail_unless (gst_pad_push (mysrcpad, buffer) == GST_FLOW_OK); + buffer = g_list_nth_data (inbuffers, 2); + fail_unless (gst_pad_push (mysrcpad, buffer) == GST_FLOW_OK); + buffer = g_list_nth_data (inbuffers, 1); + fail_unless (gst_pad_push (mysrcpad, buffer) == GST_FLOW_OK); + buffer = g_list_nth_data (inbuffers, 3); + fail_unless (gst_pad_push (mysrcpad, buffer) == GST_FLOW_OK); + + /* check the buffer list */ + check_jitterbuffer_results (num_buffers); + + /* cleanup */ + cleanup_jitterbuffer (jitterbuffer); +} + +GST_END_TEST; + +gboolean is_eos; + +static gboolean +eos_event_function (G_GNUC_UNUSED GstPad * pad, + G_GNUC_UNUSED GstObject * parent, GstEvent * event) +{ + if (GST_EVENT_TYPE (event) == GST_EVENT_EOS) { + g_mutex_lock (&check_mutex); + is_eos = TRUE; + g_cond_signal (&check_cond); + g_mutex_unlock (&check_mutex); + } + gst_event_unref (event); + return TRUE; +} + +GST_START_TEST (test_push_eos) +{ + GstElement *jitterbuffer; + const guint num_buffers = 5; + GList *node; + GstStructure *stats; + guint64 pushed, lost, late; + int n = 0; + + is_eos = FALSE; + + jitterbuffer = setup_jitterbuffer (num_buffers); + gst_pad_set_event_function (mysinkpad, eos_event_function); + + g_object_set (jitterbuffer, "latency", 1, NULL); + + fail_unless (start_jitterbuffer (jitterbuffer) + == GST_STATE_CHANGE_SUCCESS, "could not set to playing"); + + /* push buffers: 0,1,2, */ + for (node = inbuffers; node; node = g_list_next (node)) { + GstBuffer *buffer; + + /* steal buffer from list */ + buffer = node->data; + node->data = NULL; + + n++; + /* Skip 1 */ + if (n == 2) { + gst_buffer_unref (buffer); + continue; + } + fail_unless (gst_pad_push (mysrcpad, buffer) == GST_FLOW_OK); + } + + gst_pad_push_event (mysrcpad, gst_event_new_eos ()); + + g_mutex_lock (&check_mutex); + while (!is_eos) + g_cond_wait (&check_cond, &check_mutex); + g_mutex_unlock (&check_mutex); + + fail_unless_equals_int (g_list_length (buffers), num_buffers - 1); + + /* Verify statistics */ + g_object_get (jitterbuffer, "stats", &stats, NULL); + gst_structure_get (stats, "num-pushed", G_TYPE_UINT64, &pushed, + "num-lost", G_TYPE_UINT64, &lost, + "num-late", G_TYPE_UINT64, &late, + NULL); + fail_unless_equals_int (pushed, g_list_length (inbuffers) - 1); + fail_unless_equals_int (lost, 1); + fail_unless_equals_int (late, 0); + gst_structure_free (stats); + + /* cleanup */ + cleanup_jitterbuffer (jitterbuffer); +} + +GST_END_TEST; + +GST_START_TEST (test_basetime) +{ + GstElement *jitterbuffer; + const guint num_buffers = 3; + GstBuffer *buffer; + GList *node; + GstClockTime tso = gst_util_uint64_scale (RTP_FRAME_SIZE, GST_SECOND, 8000); + + jitterbuffer = setup_jitterbuffer (num_buffers); + fail_unless (start_jitterbuffer (jitterbuffer) + == GST_STATE_CHANGE_SUCCESS, "could not set to playing"); + + /* push buffers: 2,1,0 */ + for (node = g_list_last (inbuffers); node; node = g_list_previous (node)) { + buffer = (GstBuffer *) node->data; + fail_unless (gst_pad_push (mysrcpad, buffer) == GST_FLOW_OK); + } + + /* sleep for twice the latency */ + g_usleep (400 * 1000); + + /* if this fails, not all buffers have been processed */ + fail_unless_equals_int ((g_list_length (buffers) + num_dropped), num_buffers); + + buffer = (GstBuffer *) buffers->data; + fail_unless (GST_BUFFER_DTS (buffer) != (num_buffers * tso)); + fail_unless (GST_BUFFER_PTS (buffer) != (num_buffers * tso)); + + /* cleanup */ + cleanup_jitterbuffer (jitterbuffer); +} + +GST_END_TEST; + +static GstCaps * +request_pt_map (G_GNUC_UNUSED GstElement * jitterbuffer, guint pt) +{ + fail_unless (pt == 0); + + return gst_caps_from_string (RTP_CAPS_STRING); +} + +GST_START_TEST (test_clear_pt_map) +{ + GstElement *jitterbuffer; + const guint num_buffers = 10; + gint i; + GstBuffer *buffer; + GList *node; + + jitterbuffer = setup_jitterbuffer (num_buffers); + fail_unless (start_jitterbuffer (jitterbuffer) + == GST_STATE_CHANGE_SUCCESS, "could not set to playing"); + + g_signal_connect (jitterbuffer, "request-pt-map", (GCallback) + request_pt_map, NULL); + + /* push buffers: 0,1,2, */ + for (node = inbuffers, i = 0; node && i < 3; node = g_list_next (node), i++) { + buffer = (GstBuffer *) node->data; + fail_unless (gst_pad_push (mysrcpad, buffer) == GST_FLOW_OK); + } + + g_usleep (400 * 1000); + + g_signal_emit_by_name (jitterbuffer, "clear-pt-map", NULL); + + for (; node && i < 10; node = g_list_next (node), i++) { + buffer = (GstBuffer *) node->data; + fail_unless (gst_pad_push (mysrcpad, buffer) == GST_FLOW_OK); + } + + /* check the buffer list */ + check_jitterbuffer_results (num_buffers); + + /* cleanup */ + cleanup_jitterbuffer (jitterbuffer); +} + +GST_END_TEST; + +#define TEST_BUF_CLOCK_RATE 8000 +#define AS_TEST_BUF_RTP_TIME(gst_time) gst_util_uint64_scale_int (TEST_BUF_CLOCK_RATE, gst_time, GST_SECOND) +#define TEST_BUF_PT 0 +#define TEST_BUF_SSRC 0x01BADBAD +#define TEST_BUF_MS 20 +#define TEST_BUF_DURATION (TEST_BUF_MS * GST_MSECOND) +#define TEST_BUF_SIZE (64000 * TEST_BUF_MS / 1000) +#define TEST_RTP_TS_DURATION AS_TEST_BUF_RTP_TIME (TEST_BUF_DURATION) + +static GstCaps * +generate_caps (void) +{ + return gst_caps_new_simple ("application/x-rtp", + "media", G_TYPE_STRING, "audio", + "clock-rate", G_TYPE_INT, TEST_BUF_CLOCK_RATE, + "encoding-name", G_TYPE_STRING, "TEST", + "payload", G_TYPE_INT, TEST_BUF_PT, + "ssrc", G_TYPE_UINT, TEST_BUF_SSRC, NULL); +} + +static GstBuffer * +generate_test_buffer_full (GstClockTime dts, guint seq_num, guint32 rtp_ts) +{ + GstBuffer *buf; + guint8 *payload; + guint i; + GstRTPBuffer rtp = GST_RTP_BUFFER_INIT; + + buf = gst_rtp_buffer_new_allocate (TEST_BUF_SIZE, 0, 0); + GST_BUFFER_DTS (buf) = dts; + + gst_rtp_buffer_map (buf, GST_MAP_READWRITE, &rtp); + gst_rtp_buffer_set_payload_type (&rtp, TEST_BUF_PT); + gst_rtp_buffer_set_seq (&rtp, seq_num); + gst_rtp_buffer_set_timestamp (&rtp, rtp_ts); + gst_rtp_buffer_set_ssrc (&rtp, TEST_BUF_SSRC); + + payload = gst_rtp_buffer_get_payload (&rtp); + for (i = 0; i < TEST_BUF_SIZE; i++) + payload[i] = 0xff; + + gst_rtp_buffer_unmap (&rtp); + + return buf; +} + +static GstBuffer * +generate_test_buffer (guint seq_num) +{ + return generate_test_buffer_full (seq_num * TEST_BUF_DURATION, + seq_num, seq_num * TEST_RTP_TS_DURATION); +} + +static void +push_test_buffer (GstHarness * h, guint seq_num) +{ + //gst_harness_set_time (h, seq_num * TEST_BUF_DURATION); + fail_unless_equals_int (GST_FLOW_OK, gst_harness_push (h, + generate_test_buffer (seq_num))); +} + +static gint +get_rtp_seq_num (GstBuffer * buf) +{ + GstRTPBuffer rtp = GST_RTP_BUFFER_INIT; + gint seq; + gst_rtp_buffer_map (buf, GST_MAP_READ, &rtp); + seq = gst_rtp_buffer_get_seq (&rtp); + gst_rtp_buffer_unmap (&rtp); + return seq; +} + +static void +verify_lost_event (GstHarness * h, guint exp_seq, GstClockTime exp_ts, + GstClockTime exp_dur) +{ + GstEvent *event; + const GstStructure *s; + const GValue *value; + guint seq; + GstClockTime ts; + GstClockTime dur; + + event = gst_harness_pull_event (h); + + fail_unless (event != NULL); + + s = gst_event_get_structure (event); + fail_unless (s != NULL); + fail_unless (gst_structure_get_uint (s, "seqnum", &seq)); + + value = gst_structure_get_value (s, "timestamp"); + fail_unless (value && G_VALUE_HOLDS_UINT64 (value)); + + ts = g_value_get_uint64 (value); + value = gst_structure_get_value (s, "duration"); + fail_unless (value && G_VALUE_HOLDS_UINT64 (value)); + + dur = g_value_get_uint64 (value); + fail_unless_equals_int ((guint16) exp_seq, seq); + fail_unless_equals_uint64 (exp_ts, ts); + fail_unless_equals_uint64 (exp_dur, dur); + + gst_event_unref (event); +} + +static gboolean +verify_jb_stats (GstElement * jb, GstStructure * expected) +{ + gboolean ret; + GstStructure *actual; + g_object_get (jb, "stats", &actual, NULL); + + ret = gst_structure_is_subset (actual, expected); + + if (!ret) { + gchar *e_str = gst_structure_to_string (expected); + gchar *a_str = gst_structure_to_string (actual); + fail_unless (ret, "%s is not a subset of %s", e_str, a_str); + g_free (e_str); + g_free (a_str); + } + + gst_structure_free (expected); + gst_structure_free (actual); + + return ret; +} + +static guint +construct_deterministic_initial_state (GstHarness * h, gint latency_ms) +{ + guint next_seqnum = latency_ms / TEST_BUF_MS + 1; + guint seqnum; + gint i; + + g_assert (latency_ms % TEST_BUF_MS == 0); + + gst_harness_set_src_caps (h, generate_caps ()); + g_object_set (h->element, "latency", latency_ms, NULL); + + /* When the first packet arrives in the jitterbuffer, it will create a + * timeout for this packet equal to the latency of the jitterbuffer. + * This is known as DEADLINE internally, and is meant to allow the stream + * to buffer a bit before starting to push it out, to get some ideas about + * the nature of the stream. (packetspacing, jitter etc.) + * + * When writing tests using the test-clock, it it hence important to know + * that by simply advancing the clock to this timeout, you are basically + * describing a stream that had one initial packet, and then nothing at all + * for the duration of the latency (100ms in this test), which is not a very + * usual scenario. + * + * Instead, a pattern used throughout this test-suite, is to keep the buffers + * arriving at their optimal time, until the DEADLINE is reached, and that + * then becomes the "starting-point" for the test, because at this time + * there should now be no waiting timers (unless using rtx) and we have + * a "clean" state to craft the test from. + */ + + /* Packet 0 arrives at time 0ms, Packet 5 arrives at time 100ms */ + for (seqnum = 0; seqnum < next_seqnum; seqnum++) { + push_test_buffer (h, seqnum); + } + + for (seqnum = 0; seqnum < next_seqnum; seqnum++) { + GstBuffer *buf; + gst_harness_set_time (h, + seqnum * TEST_BUF_DURATION + latency_ms * GST_MSECOND); + buf = gst_harness_pull (h); + fail_unless_equals_uint64 (seqnum * TEST_BUF_DURATION, + GST_BUFFER_PTS (buf)); + fail_unless_equals_int (seqnum, get_rtp_seq_num (buf)); + gst_buffer_unref (buf); + } + + /* drop GstEventStreamStart & GstEventCaps & GstEventSegment */ + for (i = 0; i < 3; i++) + gst_event_unref (gst_harness_pull_event (h)); + + /* drop reconfigure event */ + gst_event_unref (gst_harness_pull_upstream_event (h)); + + /* Verify that at this point our queues are empty */ + fail_unless_equals_int (0, gst_harness_buffers_in_queue (h)); + fail_unless_equals_int (0, gst_harness_events_in_queue (h)); + + return next_seqnum; +} + +GST_START_TEST (test_lost_event) +{ + GstHarness *h; + GstBuffer *buf; + gint latency_ms = 100; + guint next_seqnum; + guint missing_seqnum; + + h = gst_harness_new ("ts-jitterbuffer"); + + g_object_set (h->element, "do-lost", TRUE, NULL); + next_seqnum = construct_deterministic_initial_state (h, latency_ms); + + /* We will now create a gap in the stream, by skipping one sequence-number, + * and push the following packet. + */ + missing_seqnum = next_seqnum; + next_seqnum += 1; + + fail_unless_equals_int (GST_FLOW_OK, gst_harness_push (h, + generate_test_buffer (next_seqnum))); + + /* This packet (@next_seqnum) will now be held back, awaiting the missing one, + * verify that this is the case: + */ + fail_unless_equals_int (0, gst_harness_buffers_in_queue (h)); + fail_unless_equals_int (0, gst_harness_events_in_queue (h)); + + /* The lost-timeout for the missing packet will now be its pts + latency, so + * now we will simply crank the clock to advance to this point in time, and + * check that we get a lost-event, as well as the last packet we pushed in. + */ + gst_harness_set_time (h, + next_seqnum * TEST_BUF_DURATION + latency_ms * GST_MSECOND); + verify_lost_event (h, missing_seqnum, + missing_seqnum * TEST_BUF_DURATION, TEST_BUF_DURATION); + + buf = gst_harness_pull (h); + fail_unless_equals_uint64 (next_seqnum * TEST_BUF_DURATION, + GST_BUFFER_PTS (buf)); + fail_unless_equals_int (next_seqnum, get_rtp_seq_num (buf)); + gst_buffer_unref (buf); + + fail_unless (verify_jb_stats (h->element, + gst_structure_new ("application/x-rtp-jitterbuffer-stats", + "num-pushed", G_TYPE_UINT64, (guint64) next_seqnum, + "num-lost", G_TYPE_UINT64, (guint64) 1, NULL))); + + gst_harness_teardown (h); +} + +GST_END_TEST; + +GST_START_TEST (test_only_one_lost_event_on_large_gaps) +{ + GstHarness *h = gst_harness_new ("ts-jitterbuffer"); + GstTestClock *testclock; + GstBuffer *out_buf; + guint next_seqnum; + gint latency_ms = 200; + gint num_lost_events = latency_ms / TEST_BUF_MS; + gint i; + + testclock = gst_harness_get_testclock (h); + /* Need to set max-misorder-time and max-dropout-time to 0 so the + * jitterbuffer does not base them on packet rate calculations. + * If it does, out gap is big enough to be considered a new stream and + * we wait for a few consecutive packets just to be sure + */ + g_object_set (h->element, "do-lost", TRUE, + "max-misorder-time", 0, "max-dropout-time", 0, NULL); + next_seqnum = construct_deterministic_initial_state (h, latency_ms); + + /* move time ahead to just before 10 seconds */ + gst_harness_set_time (h, 10 * GST_SECOND - 1); + + /* check that we have no pending waits */ + fail_unless_equals_int (0, gst_test_clock_peek_id_count (testclock)); + + /* a buffer now arrives perfectly on time */ + fail_unless_equals_int (GST_FLOW_OK, + gst_harness_push (h, generate_test_buffer (500))); + + gst_harness_set_time (h, 10 * GST_SECOND + latency_ms * GST_MSECOND); + + /* we should now receive a packet-lost-event for buffers 11 through 489 ... */ + verify_lost_event (h, next_seqnum, + next_seqnum * TEST_BUF_DURATION, TEST_BUF_DURATION * (490 - next_seqnum)); + + /* ... as well as 490 (since at 10 sec 490 is too late) */ + verify_lost_event (h, 490, 490 * TEST_BUF_DURATION, TEST_BUF_DURATION); + + /* we get as many lost events as the the number of * + * buffers the jitterbuffer is able to wait for */ + for (i = 1; i < num_lost_events; i++) { + verify_lost_event (h, 490 + i, (490 + i) * TEST_BUF_DURATION, + TEST_BUF_DURATION); + } + + /* and then the buffer is released */ + out_buf = gst_harness_pull (h); + fail_unless (GST_BUFFER_FLAG_IS_SET (out_buf, GST_BUFFER_FLAG_DISCONT)); + fail_unless_equals_int (500, get_rtp_seq_num (out_buf)); + fail_unless_equals_uint64 (10 * GST_SECOND, GST_BUFFER_PTS (out_buf)); + gst_buffer_unref (out_buf); + + fail_unless (verify_jb_stats (h->element, + gst_structure_new ("application/x-rtp-jitterbuffer-stats", + "num-lost", G_TYPE_UINT64, (guint64) 489, NULL))); + + gst_object_unref (testclock); + gst_harness_teardown (h); +} + +GST_END_TEST; + +GST_START_TEST (test_two_lost_one_arrives_in_time) +{ + GstHarness *h = gst_harness_new ("ts-jitterbuffer"); + GstTestClock *testclock; + GstBuffer *buf; + gint latency_ms = 100; + guint next_seqnum; + guint first_missing; + guint second_missing; + guint current_arrived; + + testclock = gst_harness_get_testclock (h); + g_object_set (h->element, "do-lost", TRUE, NULL); + next_seqnum = construct_deterministic_initial_state (h, latency_ms); + + /* hop over 2 packets and make another one (gap of 2) */ + first_missing = next_seqnum; + second_missing = next_seqnum + 1; + current_arrived = next_seqnum + 2; + fail_unless_equals_int (GST_FLOW_OK, + gst_harness_push (h, generate_test_buffer (current_arrived))); + + gst_harness_set_time (h, + second_missing * TEST_BUF_DURATION + latency_ms * GST_MSECOND - 1); + /* @second_missing now arrives just in time */ + fail_unless_equals_int (GST_FLOW_OK, + gst_harness_push (h, generate_test_buffer (second_missing))); + + gst_harness_set_time (h, + second_missing * TEST_BUF_DURATION + latency_ms * GST_MSECOND); + + /* we should now receive a packet-lost-event */ + verify_lost_event (h, first_missing, + first_missing * TEST_BUF_DURATION, TEST_BUF_DURATION); + + /* verify that @second_missing made it through! */ + buf = gst_harness_pull (h); + fail_unless (GST_BUFFER_FLAG_IS_SET (buf, GST_BUFFER_FLAG_DISCONT)); + fail_unless_equals_int (second_missing, get_rtp_seq_num (buf)); + gst_buffer_unref (buf); + + gst_harness_set_time (h, + current_arrived * TEST_BUF_DURATION + latency_ms * GST_MSECOND); + + /* and see that @current_arrived now also is pushed */ + buf = gst_harness_pull (h); + fail_unless (!GST_BUFFER_FLAG_IS_SET (buf, GST_BUFFER_FLAG_DISCONT)); + fail_unless_equals_int (current_arrived, get_rtp_seq_num (buf)); + gst_buffer_unref (buf); + + fail_unless (verify_jb_stats (h->element, + gst_structure_new ("application/x-rtp-jitterbuffer-stats", + "num-pushed", G_TYPE_UINT64, (guint64) next_seqnum + 2, + "num-lost", G_TYPE_UINT64, (guint64) 1, NULL))); + + gst_object_unref (testclock); + gst_harness_teardown (h); +} + +GST_END_TEST; + +GST_START_TEST (test_late_packets_still_makes_lost_events) +{ + GstHarness *h = gst_harness_new ("ts-jitterbuffer"); + GstBuffer *out_buf; + gint latency_ms = 100; + guint next_seqnum; + guint seqnum; + GstClockTime now; + + g_object_set (h->element, "do-lost", TRUE, NULL); + next_seqnum = construct_deterministic_initial_state (h, latency_ms); + + /* jump 10 seconds forward in time */ + now = 10 * GST_SECOND; + gst_harness_set_time (h, now); + + /* push a packet with a gap of 2, that now is very late */ + seqnum = next_seqnum + 2; + fail_unless_equals_int (GST_FLOW_OK, gst_harness_push (h, + generate_test_buffer_full (now, + seqnum, seqnum * TEST_RTP_TS_DURATION))); + + gst_harness_set_time (h, now + latency_ms * GST_MSECOND + 1); + + /* we should now receive packet-lost-events for the gap + * FIXME: The timeout and duration here are a bit crap... + */ + verify_lost_event (h, next_seqnum, 3400 * GST_MSECOND, 6600 * GST_MSECOND); + + /* verify that packet @seqnum made it through! */ + out_buf = gst_harness_pull (h); + fail_unless (GST_BUFFER_FLAG_IS_SET (out_buf, GST_BUFFER_FLAG_DISCONT)); + fail_unless_equals_int (seqnum, get_rtp_seq_num (out_buf)); + gst_buffer_unref (out_buf); + + fail_unless (verify_jb_stats (h->element, + gst_structure_new ("application/x-rtp-jitterbuffer-stats", + "num-pushed", G_TYPE_UINT64, (guint64) next_seqnum + 1, + "num-lost", G_TYPE_UINT64, (guint64) 2, NULL))); + + gst_harness_teardown (h); +} + +GST_END_TEST; + + +GST_START_TEST (test_num_late_when_considered_lost_arrives) +{ + GstHarness *h = gst_harness_new ("ts-jitterbuffer"); + gboolean do_lost = __i__ != 0; + gint latency_ms = 100; + guint next_seqnum; + + g_object_set (h->element, "do-lost", do_lost, NULL); + next_seqnum = construct_deterministic_initial_state (h, latency_ms); + + /* gap of 1 */ + fail_unless_equals_int (GST_FLOW_OK, + gst_harness_push (h, generate_test_buffer (next_seqnum + 1))); + + gst_harness_set_time (h, + (next_seqnum + 1) * TEST_BUF_DURATION + latency_ms * GST_MSECOND); + + if (do_lost) { + /* we should now receive packet-lost-events for the missing packet */ + verify_lost_event (h, next_seqnum, + next_seqnum * TEST_BUF_DURATION, TEST_BUF_DURATION); + } + + /* pull out the pushed packet */ + gst_buffer_unref (gst_harness_pull (h)); + + /* we have one lost packet in the stats */ + fail_unless (verify_jb_stats (h->element, + gst_structure_new ("application/x-rtp-jitterbuffer-stats", + "num-pushed", G_TYPE_UINT64, (guint64) next_seqnum + 1, + "num-lost", G_TYPE_UINT64, (guint64) 1, + "num-late", G_TYPE_UINT64, (guint64) 0, NULL))); + + /* the missing packet now arrives (too late) */ + fail_unless_equals_int (GST_FLOW_OK, + gst_harness_push (h, generate_test_buffer (next_seqnum))); + + /* and this increments num-late */ + fail_unless (verify_jb_stats (h->element, + gst_structure_new ("application/x-rtp-jitterbuffer-stats", + "num-pushed", G_TYPE_UINT64, (guint64) next_seqnum + 1, + "num-lost", G_TYPE_UINT64, (guint64) 1, + "num-late", G_TYPE_UINT64, (guint64) 1, NULL))); + + gst_harness_teardown (h); +} + +GST_END_TEST; + +GST_START_TEST (test_lost_event_uses_pts) +{ + GstHarness *h = gst_harness_new ("ts-jitterbuffer"); + GstClockTime now; + gint latency_ms = 100; + guint next_seqnum; + guint lost_seqnum; + + g_object_set (h->element, "do-lost", TRUE, NULL); + next_seqnum = construct_deterministic_initial_state (h, latency_ms); + + /* hop over 1 packets and make another one (gap of 1), but due to + network delays, this packets is also grossly late */ + lost_seqnum = next_seqnum; + next_seqnum += 1; + + /* advance the clock to the latest time packet @next_seqnum could arrive */ + now = next_seqnum * TEST_BUF_DURATION + latency_ms * GST_MSECOND; + gst_harness_set_time (h, now); + gst_harness_push (h, generate_test_buffer_full (now, next_seqnum, + next_seqnum * TEST_RTP_TS_DURATION)); + + now += 1; + gst_harness_set_time (h, now); + + /* we should now have received a packet-lost-event for buffer 3 */ + verify_lost_event (h, lost_seqnum, + lost_seqnum * TEST_BUF_DURATION, TEST_BUF_DURATION); + + /* and pull out packet 4 */ + gst_buffer_unref (gst_harness_pull (h)); + + fail_unless (verify_jb_stats (h->element, + gst_structure_new ("application/x-rtp-jitterbuffer-stats", + "num-pushed", G_TYPE_UINT64, (guint64) next_seqnum, + "num-lost", G_TYPE_UINT64, (guint64) 1, NULL))); + + gst_harness_teardown (h); +} + +GST_END_TEST; + +GST_START_TEST (test_lost_event_with_backwards_rtptime) +{ + GstHarness *h = gst_harness_new ("ts-jitterbuffer"); + gint latency_ms = 40; + + g_object_set (h->element, "do-lost", TRUE, NULL); + construct_deterministic_initial_state (h, latency_ms); + + /* + * For video using B-frames, an expected sequence + * could be like this: + * (I = I-frame, P = P-frame, B = B-frame) + * ___ ___ ___ ___ ___ + * ... | 3 | | 4 | | 5 | | 6 | | 7 | + * ––– ––– ––– ––– ––– + * rtptime: 3(I) 5(P) 5(P) 4(B) 6(P) + * arrival(dts): 3 5 5 5 6 + * + * Notice here that packet 6 (the B frame) make + * the rtptime go backwards. + * + * But we get this: + * ___ ___ _ _ ___ ___ + * ... | 3 | | 4 | | | | 6 | | 7 | + * ––– ––– - - ––– ––– + * rtptime: 3(I) 5(P) 4(B) 6(P) + * arrival(dts): 3 5 5 6 + * + */ + + /* seqnum 3 */ + fail_unless_equals_int (GST_FLOW_OK, + gst_harness_push (h, generate_test_buffer (3))); + gst_harness_set_time (h, 3 * TEST_BUF_DURATION + latency_ms * GST_MSECOND); + gst_buffer_unref (gst_harness_pull (h)); + + /* seqnum 4, arriving at time 5 with rtptime 5 */ + gst_harness_push (h, + generate_test_buffer_full (5 * TEST_BUF_DURATION, + 4, 5 * TEST_RTP_TS_DURATION)); + gst_harness_set_time (h, 4 * TEST_BUF_DURATION + latency_ms * GST_MSECOND); + gst_buffer_unref (gst_harness_pull (h)); + + /* seqnum 6, arriving at time 5 with rtptime 4, + making a gap for missing seqnum 5 */ + gst_harness_push (h, + generate_test_buffer_full (5 * TEST_BUF_DURATION, + 6, 4 * TEST_RTP_TS_DURATION)); + + /* seqnum 7, arriving at time 6 with rtptime 6 */ + gst_harness_push (h, + generate_test_buffer_full (6 * TEST_BUF_DURATION, + 7, 6 * TEST_RTP_TS_DURATION)); + + /* we should now have received a packet-lost-event for seqnum 5, + with time 5 and 0 duration */ + gst_harness_set_time (h, 5 * TEST_BUF_DURATION + latency_ms * GST_MSECOND); + verify_lost_event (h, 5, 5 * TEST_BUF_DURATION, 0); + + /* and pull out 6 and 7 */ + gst_buffer_unref (gst_harness_pull (h)); + gst_buffer_unref (gst_harness_pull (h)); + + fail_unless (verify_jb_stats (h->element, + gst_structure_new ("application/x-rtp-jitterbuffer-stats", + "num-pushed", G_TYPE_UINT64, (guint64) 7, + "num-lost", G_TYPE_UINT64, (guint64) 1, NULL))); + + gst_harness_teardown (h); +} + +GST_END_TEST; + +GST_START_TEST (test_all_packets_are_timestamped_zero) +{ + GstHarness *h = gst_harness_new ("ts-jitterbuffer"); + GstBuffer *out_buf; + gint jb_latency_ms = 100; + gint i, b; + + gst_harness_set_src_caps (h, generate_caps ()); + g_object_set (h->element, "do-lost", TRUE, "latency", jb_latency_ms, NULL); + + /* advance the clock with 10 seconds */ + gst_harness_set_time (h, 10 * GST_SECOND); + + /* push the first buffer through */ + fail_unless_equals_int (GST_FLOW_OK, + gst_harness_push (h, generate_test_buffer (0))); + gst_buffer_unref (gst_harness_pull (h)); + + /* push some buffers in, all timestamped 0 */ + for (b = 1; b < 3; b++) { + fail_unless_equals_int (GST_FLOW_OK, + gst_harness_push (h, + generate_test_buffer_full (0 * GST_MSECOND, b, 0))); + /* check for the buffer coming out that was pushed in */ + out_buf = gst_harness_pull (h); + fail_unless_equals_uint64 (0, GST_BUFFER_DTS (out_buf)); + fail_unless_equals_uint64 (0, GST_BUFFER_PTS (out_buf)); + gst_buffer_unref (out_buf); + } + + /* hop over 2 packets and make another one (gap of 2) */ + b = 5; + fail_unless_equals_int (GST_FLOW_OK, + gst_harness_push (h, generate_test_buffer_full (0 * GST_MSECOND, b, 0))); + + /* drop GstEventStreamStart & GstEventCaps & GstEventSegment */ + for (i = 0; i < 3; i++) + gst_event_unref (gst_harness_pull_event (h)); + + /* we should now receive packet-lost-events for buffer 3 and 4 */ + verify_lost_event (h, 3, 0, 0); + verify_lost_event (h, 4, 0, 0); + + /* verify that buffer 5 made it through! */ + out_buf = gst_harness_pull (h); + fail_unless (GST_BUFFER_FLAG_IS_SET (out_buf, GST_BUFFER_FLAG_DISCONT)); + fail_unless_equals_int (5, get_rtp_seq_num (out_buf)); + gst_buffer_unref (out_buf); + + fail_unless (verify_jb_stats (h->element, + gst_structure_new ("application/x-rtp-jitterbuffer-stats", + "num-pushed", G_TYPE_UINT64, (guint64) 4, + "num-lost", G_TYPE_UINT64, (guint64) 2, NULL))); + + gst_harness_teardown (h); +} + +GST_END_TEST; + +GST_START_TEST (test_reorder_of_non_equidistant_packets) +{ + GstHarness *h = gst_harness_new ("ts-jitterbuffer"); + GstTestClock *testclock; + gint latency_ms = 5; + gint seq, frame; + gint num_init_frames = 1; + const GstClockTime frame_dur = TEST_BUF_DURATION; + const guint32 frame_rtp_ts_dur = TEST_RTP_TS_DURATION; + + gst_harness_set_src_caps (h, generate_caps ()); + testclock = gst_harness_get_testclock (h); + g_object_set (h->element, "do-lost", TRUE, "latency", latency_ms, NULL); + + for (frame = 0, seq = 0; frame < num_init_frames; frame++, seq += 2) { + /* Push a couple of packets with identical timestamp, typical for a video + * stream where one frame generates multiple packets. */ + gst_harness_set_time (h, frame * frame_dur); + gst_harness_push (h, generate_test_buffer_full (frame * frame_dur, + seq, frame * frame_rtp_ts_dur)); + gst_harness_push (h, generate_test_buffer_full (frame * frame_dur, + seq + 1, frame * frame_rtp_ts_dur)); + + gst_harness_set_time (h, frame * frame_dur + latency_ms * GST_MSECOND + 1); + gst_buffer_unref (gst_harness_pull (h)); + gst_buffer_unref (gst_harness_pull (h)); + } + + /* Finally push the last frame reordered */ + gst_harness_set_time (h, frame * frame_dur); + gst_harness_push (h, generate_test_buffer_full (frame * frame_dur, + seq + 1, frame * frame_rtp_ts_dur)); + + /* And then missing packet arrives just in time */ + gst_harness_set_time (h, frame * frame_dur + latency_ms * GST_MSECOND - 1); + gst_harness_push (h, + generate_test_buffer_full (frame * frame_dur + latency_ms * GST_MSECOND - + 1, seq, frame * frame_rtp_ts_dur)); + gst_harness_set_time (h, frame * frame_dur + latency_ms * GST_MSECOND + 1); + + gst_buffer_unref (gst_harness_pull (h)); + gst_buffer_unref (gst_harness_pull (h)); + + gst_object_unref (testclock); + gst_harness_teardown (h); + +} + +GST_END_TEST; + +GST_START_TEST (test_loss_equidistant_spacing_with_parameter_packets) +{ + GstHarness *h = gst_harness_new ("ts-jitterbuffer"); + gint latency_ms = 5; + gint seq, frame; + gint num_init_frames = 10; + gint i; + + gst_harness_set_src_caps (h, generate_caps ()); + g_object_set (h->element, "do-lost", TRUE, "latency", latency_ms, NULL); + + for (frame = 0, seq = 0; frame < num_init_frames; frame++, seq++) { + gst_harness_set_time (h, frame * TEST_BUF_DURATION); + gst_harness_push (h, generate_test_buffer_full (frame * TEST_BUF_DURATION, + seq, frame * TEST_RTP_TS_DURATION)); + } + + /* Push three packets with same rtptime, simulating parameter packets + + * frame. This should not disable equidistant mode as it is common for + * certain audio codecs. */ + for (i = 0; i < 3; i++) { + gst_harness_set_time (h, frame * TEST_BUF_DURATION); + gst_harness_push (h, generate_test_buffer_full (frame * TEST_BUF_DURATION, + seq++, frame * TEST_RTP_TS_DURATION)); + } + + gst_harness_set_time (h, + frame * TEST_BUF_DURATION + latency_ms * GST_MSECOND + 1); + for (i = 0; i < 13; i++) { + gst_buffer_unref (gst_harness_pull (h)); + } + + /* drop stream-start, caps, segment */ + for (i = 0; i < 3; i++) + gst_event_unref (gst_harness_pull_event (h)); + + frame++; + + /* Finally push the last packet introducing a gap */ + gst_harness_set_time (h, + frame * TEST_BUF_DURATION + latency_ms * GST_MSECOND - 1); + gst_harness_push (h, generate_test_buffer_full (frame * TEST_BUF_DURATION, + seq + 1, frame * TEST_RTP_TS_DURATION)); + gst_harness_set_time (h, + frame * TEST_BUF_DURATION + latency_ms * GST_MSECOND + 1); + + /* Check that the lost event has been generated assuming equidistant + * spacing. */ + verify_lost_event (h, seq, + frame * TEST_BUF_DURATION - TEST_BUF_DURATION / 2, TEST_BUF_DURATION / 2); + + gst_buffer_unref (gst_harness_pull (h)); + + gst_harness_teardown (h); +} + +GST_END_TEST; + +typedef struct +{ + guint seqnum_offset; + guint late_buffer; +} TestLateArrivalInput; + +static const TestLateArrivalInput + test_considered_lost_packet_in_large_gap_arrives_input[] = { + {0, 1}, {0, 2}, {65535, 1}, {65535, 2}, {65534, 1}, {65534, 2} +}; + +GST_START_TEST (test_considered_lost_packet_in_large_gap_arrives) +{ + GstHarness *h = gst_harness_new ("ts-jitterbuffer"); + GstTestClock *testclock; + GstBuffer *buffer; + gint jb_latency_ms = 20; + const TestLateArrivalInput *test_input = + &test_considered_lost_packet_in_large_gap_arrives_input[__i__]; + guint seq_offset = test_input->seqnum_offset; + guint late_buffer = test_input->late_buffer; + gint i; + + gst_harness_set_src_caps (h, generate_caps ()); + testclock = gst_harness_get_testclock (h); + g_object_set (h->element, "do-lost", TRUE, "latency", jb_latency_ms, NULL); + + /* first push buffer 0 */ + fail_unless_equals_int (GST_FLOW_OK, + gst_harness_push (h, generate_test_buffer_full (0 * TEST_BUF_DURATION, + 0 + seq_offset, 0 * TEST_RTP_TS_DURATION))); + gst_harness_set_time (h, jb_latency_ms * GST_MSECOND + 1); + gst_buffer_unref (gst_harness_pull (h)); + + /* drop GstEventStreamStart & GstEventCaps & GstEventSegment */ + for (i = 0; i < 3; i++) + gst_event_unref (gst_harness_pull_event (h)); + + /* hop over 3 packets, and push buffer 4 (gap of 3) */ + fail_unless_equals_int (GST_FLOW_OK, + gst_harness_push (h, generate_test_buffer_full (4 * TEST_BUF_DURATION, + 4 + seq_offset, 4 * TEST_RTP_TS_DURATION))); + + gst_harness_set_time (h, + 4 * TEST_BUF_DURATION + jb_latency_ms * GST_MSECOND + 1); + + /* buffer 4 is pushed as normal */ + buffer = gst_harness_pull (h); + fail_unless_equals_int ((4 + seq_offset) & 0xffff, get_rtp_seq_num (buffer)); + gst_buffer_unref (buffer); + + /* now buffer 1 sneaks in before the lost event for buffer 1 and 2 is + * processed */ + fail_unless_equals_int (GST_FLOW_OK, + gst_harness_push (h, + generate_test_buffer_full (late_buffer * TEST_BUF_DURATION, + late_buffer + seq_offset, late_buffer * TEST_RTP_TS_DURATION))); + + verify_lost_event (h, 1 + seq_offset, 1 * TEST_BUF_DURATION, + 2 * TEST_BUF_DURATION); + + verify_lost_event (h, 3 + seq_offset, 3 * TEST_BUF_DURATION, + 1 * TEST_BUF_DURATION); + + /* we have lost 3, and one of them arrived eventually, but too late */ + fail_unless (verify_jb_stats (h->element, + gst_structure_new ("application/x-rtp-jitterbuffer-stats", + "num-pushed", G_TYPE_UINT64, (guint64) 2, + "num-lost", G_TYPE_UINT64, (guint64) 3, + "num-late", G_TYPE_UINT64, (guint64) 1, NULL))); + + gst_object_unref (testclock); + gst_harness_teardown (h); +} + +GST_END_TEST; + +static Suite * +rtpjitterbuffer_suite (void) +{ + Suite *s = suite_create ("rtpjitterbuffer"); + TCase *tc_chain = tcase_create ("general"); + + suite_add_tcase (s, tc_chain); + + tcase_add_test (tc_chain, test_push_forward_seq); + tcase_add_test (tc_chain, test_push_backward_seq); + tcase_add_test (tc_chain, test_push_unordered); + tcase_add_test (tc_chain, test_push_eos); + tcase_add_test (tc_chain, test_basetime); + tcase_add_test (tc_chain, test_clear_pt_map); + + tcase_add_test (tc_chain, test_lost_event); + tcase_add_test (tc_chain, test_only_one_lost_event_on_large_gaps); + tcase_add_test (tc_chain, test_two_lost_one_arrives_in_time); + tcase_add_test (tc_chain, test_late_packets_still_makes_lost_events); + tcase_add_test (tc_chain, test_lost_event_uses_pts); + tcase_add_test (tc_chain, test_lost_event_with_backwards_rtptime); + + tcase_add_test (tc_chain, test_all_packets_are_timestamped_zero); + tcase_add_loop_test (tc_chain, test_num_late_when_considered_lost_arrives, 0, + 2); + tcase_add_test (tc_chain, test_reorder_of_non_equidistant_packets); + tcase_add_test (tc_chain, + test_loss_equidistant_spacing_with_parameter_packets); + + tcase_add_loop_test (tc_chain, + test_considered_lost_packet_in_large_gap_arrives, 0, + G_N_ELEMENTS (test_considered_lost_packet_in_large_gap_arrives_input)); + + return s; +} + +#define main ctests_main + +GST_CHECK_MAIN (rtpjitterbuffer); + +int run_ctests(void) { + return ctests_main(0, NULL); +} -- GitLab