From 31bb82e11643c2ef686cd55e44eec9071e4a3ebd Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Fri, 19 Jun 2026 17:38:18 +0200 Subject: [PATCH] module-rtp: Add RTP jitter buffer This new data structure is useful for reordering incoming packets if they arrive out-of-order. Many audio codecs require frames and/or packets to be processed in sequence order due to inter-frame dependencies, so reordering is critical for such encoded data. It also detects lost packets and reports those in sequence with received packets (crucial for proper PLC), and detects and drops late and duplicate packets. --- src/modules/meson.build | 3 +- src/modules/module-rtp/jitter-buffer.c | 1057 +++++++++++++++ src/modules/module-rtp/jitter-buffer.h | 317 +++++ test/meson.build | 11 + test/modules/module-rtp/test-jitter-buffer.c | 1259 ++++++++++++++++++ 5 files changed, 2646 insertions(+), 1 deletion(-) create mode 100644 src/modules/module-rtp/jitter-buffer.c create mode 100644 src/modules/module-rtp/jitter-buffer.h create mode 100644 test/modules/module-rtp/test-jitter-buffer.c diff --git a/src/modules/meson.build b/src/modules/meson.build index 6bd108e95..2a2b52678 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -595,7 +595,8 @@ summary({'zeroconf-discover': build_module_zeroconf_discover}, bool_yn: true, se # (by avoiding build script code duplication), create a static library # that contains that common code. pipewire_module_rtp_common_lib = static_library('pipewire-module-rtp-common-lib', - [ 'module-rtp/stream.c' ], + [ 'module-rtp/stream.c', + 'module-rtp/jitter-buffer.c' ], include_directories : [configinc], install : false, dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, opus_dep], diff --git a/src/modules/module-rtp/jitter-buffer.c b/src/modules/module-rtp/jitter-buffer.c new file mode 100644 index 000000000..34a8fc34f --- /dev/null +++ b/src/modules/module-rtp/jitter-buffer.c @@ -0,0 +1,1057 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2026 Carlos Rafael Giani */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include + +#include +#include + +#include + +#include +#include + +PW_LOG_TOPIC_EXTERN(mod_topic); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +/* RTP jitter buffer design overview + * + * NOTE: For basic information about what the jitter buffer does and how it + * is used, read through the rtp_jitter_buffer struct documentation first. + * + * The jitter buffer consists of slots and the valid seqnum window. Slots + * are abstract entities, and exist as items in the slots array. This array + * contains num_slots items. Valid slots contain elements; these are packets + * or gaps. The slot element_type is then set accordingly either to + * RTP_JITTER_BUFFER_ELEMENT_TYPE_PACKET or RTP_JITTER_BUFFER_ELEMENT_TYPE_GAP. + * (See the rtp_jitter_buffer struct documentation for what "valid" means.) + * If a slot contains a packet, then the packet data is stored in the + * packet_buffer, as described in the rtp_jitter_buffer struct documentation, + * + * A "slot index" goes from 0 to (num_slots-1) and is used for accessing + * the slot array. Slot indices are mapped to sequence numbers through modulo + * arithmetic: + * + * slot_index = seqnum mod num_slots + * + * This works reliably because the valid seqnum window establishes the + * range of valid sequence numbers, and because that window can at most + * be of size num_slots. Thus, even if a much older or much newer packet + * (that is, a packet with much smaller or larger seqnum than the ones + * encountered thus far) arrives, there is no danger of that modulo + * arithmetic incorrectly aliasing slots, since the valid seqnum window + * will take care of validating the seqnums first. + * + * In RTP, since sequence numbers are unsigned 16-bit integers, the integer + * wrap around needs to be addressed. Packet sequence number 65535 can be + * reached in real world scenarios depending on the packet duration. For + * example, if a packet covers 1 ms, then after ~66 seconds, sequence + * number 65535 will be reached. For this reason, the valid seqnum window's + * start and length quantities use uint32_t as type - it factors out the + * wrap-around in certain calculations, which make them easier. + * + * The wrap around behavior also makes it non-trivial to calculate sequence + * number deltas correctly. For this reason, the calculate_seqnum_delta() + * utility function is used for seqnum deltas. + * + * In regular mode, incoming packets all have had the expected monotonically + * incrementing sequence number. (A sequence number wrap around technically + * means that they do not monotonically increment, but this is omitted here, + * because the wrap around is a numerical limitation, and it is handled + * as a continuation of a packet seqnum increment.) In this mode, the packet + * data, packet size, header size, timestamp are forwarded immediately to the + * output_rtp_packet() function pointer without copying the packet data. This + * improves performance during regular mode, which is the most common mode + * the jitter buffer will be in, unless the network quality is very poor. The + * slots and the valid seqnum window have no meaning in this mode. + * + * When the jitter buffer expects seqnum X, but sees seqnum X-N (that is, + * an older seqnum), the incoming packet with seqnum X-N is dropped, and + * regular mode continues. If the packet's seqnum instead is X+N, it means + * that there is a gap. For example, if the last packet had seqnum 200, + * the jitter buffer expects the next packet to have seqnum 201. If the + * next packet instead has seqnum 202, then there is a gap at 201. The + * jitter buffer switches to hold-back mode, and establishes a valid seqnum + * window that starts at 201 and is of length 2. That is, it covers the gap + * and the new packet. The gap is registered at slot (201 % num_slots), the + * packet is inserted in slot (202 % num_slots). Suppose that num_slots is 10. + * Then, the gap will be registered at slot 201 % 10 = 1, and the packet 202 + * will be stored at slot 202 % 10 = 2. To be more specific, item #1 in the + * slots array will have its type set to RTP_JITTER_BUFFER_ELEMENT_TYPE_GAP, + * and item #2 will have its type set to RTP_JITTER_BUFFER_ELEMENT_TYPE_PACKET. + * The size of packet 202 will be stored in item #2 in the slots array. + * (Item #1 in the slots array will not have a packet size set, since a gap + * has no "size" in bytes.) + * Packet 202's data will be copied into packet_buffer, at location + * (max_packet_size * 2), since the slot index is 2. That way, slot 1 is set + * to contain a gap at seqnum 201, and slot 2 is set to contain a packet 202 + * with the specified packet data, packet size, header size, timestamp. + * + * In hold-back mode, every time a packet is inserted, the jitter buffer will + * check if the oldest N valid slots contain packets (and no gaps). The "oldest" + * slot is the one whose associated sequence number equals the value of + * valid_seqnum_window_start_seqnum. The second oldest slot's associated sequence + * number equals (valid_seqnum_window_start_seqnum+1) etc. If the oldest N slots + * indeed contain purely packets and no gaps (that is, N > 0), the jitter buffer + * will output the packets from those N oldest slots, in order (the packet from + * the oldest slot is output first, then comes the packet from the second oldest + * slot etc.). Then, the valid_seqnum_window_start_seqnum value is shifted forwards + * by N, since those packets from the oldest N slots are no longer held back. + * Accordingly, the valid_seqnum_window_length value is decremented by N. If there + * are still gaps, it means there are packets behind the gaps. In such a case, + * valid_seqnum_window_length will be nonzero even after decrementing it. But if + * this output step did emit all of the remaining held back packets, it implies + * that there is nothing left in the slots, nothing is being held back, so the + * jitter buffer switches back to regular mode. + * + * When a packet comes in whose sequence number is older than the start of the + * valid seqnum window, it is dropped, just like in the regular mode. If however + * the sequence number goes past the window, it effectively extends the window. + * Suppose that the window starts at seqnum 200, and is of length 5. This means + * that currently, all seqnums from 200 to 204 (both inclusive) are valid. But + * then, a packet with seqnum 209 comes in. This requires extending the window + * from length 5 to length 10, such that it covers all seqnums from 200 to 209. + * This step of course can introduce gaps. In this example, a new gap is added + * that starts at seqnum 205 and is of length 4, since the gap extends all the + * way to seqnum 208. The four slots that are associated with seqnums 205, 206, + * 207, 208 are all set to contain gaps. + * + * However, the window cannot be extended indefinitely. If it is extended past + * num_slots, it is referred to as "overextended". Such a window cannot remain like + * this, because there are not enough slots to store all the elements it covers. + * In such a case, the jitter buffer will try to shift the window forward. If + * this shift amount is small enough, then the newest N of the currently valid + * slots remain in the window, and the oldest (num_slots-N) slots need to be + * drained. To continue with the earlier example, suppose that num_slots is 8. + * This means that packet 209 would overextend the window by 2 slots. The + * jitter buffer then shifts the window such that it starts at 202 and is of + * length 8 (that is, the num_slots amount). Slots associated with seqnums that + * go from 202 to 204 remain within the window, but slots associated with seqnums + * 200 and 201 are not, and thus need to be drained. + * + * It is possible that the window is shifted so far ahead that _none_ of the + * currently valid slots remain in the window. In such a case, the window is + * reset to contain the newly arrived packet and to be of length 1. Since this + * is a case in which (as explained above), the jitter buffer sees that the + * oldest N packets (N being 1 in this case) have no gaps in between them, this + * means that the newly arrived packet is immediately output. And since after + * that, no held-back packets remain, the jitter buffer switches back to + * regular mode. + * + * When slots are drained, it means that the jitter buffer looks at the elements + * inside them, and calls output_rtp_packet() or signal_lost_packets(). Draining + * does aggregate gaps to avoid calling signal_lost_packets() often. Only valid + * slots (that is, slots whose associated sequence numbers lie within the valid + * seqnum window) are drained. + * For example, suppose that within the window, there is a gap at seqnum 201, + * a packet at seqnum 202, another one at seqnum 203, a gap that starts at seqnum + * 204 and goes until seqnum 208, and then a final packet at 209. Draining will + * cause the following sequence of calls (in this order): + * + * 1. signal_lost_packets() , with gap at seqnum 201, or length 1 + * 2. output_rtp_packet() , with data of packet at seqnum 202 + * 3. output_rtp_packet() , with data of packet at seqnum 203 + * 4. signal_lost_packets() , with gap at seqnum 204, or length 5 + * 5. output_rtp_packet() , with data of packet at seqnum 209 + * + * A partial drain drains only the oldest N slots in the window, and held-back + * packets remain. A full drain drains all slots in the entire window - + * nothing remains. In the full drain case, the jitter buffer switches back + * to regular mode afterwards. + * + * A full drain is automatically done if the hold-back mode persists for some + * time. This is a case where there is a gap, but the associated packet does + * not arrive in time (or not at all). The timeout_timer exists for this purpose; + * if it expires, it automatically does a full drain on the jitter buffer. + * That timer is reset when the window is shifted due to overextension, and + * is disabled when switching back to regular mode. + * + * In cases where draining is done because of a window overextension, the jitter + * buffer will signal packet loss after the drain if there is a gap in between + * where the window used to be and where it now is after the shift. If for example + * the window previously starts at seqnum 200 with length 4, and now starts at + * seqnum 210 with length 10, there is a gap from 201 to 209 (both inclusive). + * The jitter buffer will announce that via signal_lost_packets(). If however + * that gap is larger than num_slots, then the jitter buffer calls that function + * pointer with open_ended set to true. Such an "open ended gap" is a gap that + * is actually larger than num_slots, but is truncated to that length, to prevent + * PLC measures from having to produce an excessive amount of data. open_ended + * being set to true allows the signal_lost_packets() calback to apply additional + * measures, such as a fadeout, to cleanly terminate its PLC for the open gap. This + * might be necessary if PLC measures otherwise never reach silence on their own. + * + * These mechanisms allow for partial outputs in cases where there are multiple + * gaps in the slots, and also switches back to regular mode immediately once + * everything has been output or the jitter buffer is fully drained. + * + * Packet reordering is done implicitly, since when inserting packets, it is done + * so according to their seqnum, but packet output is done in order of their + * storage in the slots (the "oldest" slots, that is, slots associated with the + * oldest valid seqnums within the window are output first). + * + * Note that slots that contain gaps are not explicitly marked as containing gaps. + * Instead, all items in the slots array have their element type initially set to + * RTP_JITTER_BUFFER_ELEMENT_TYPE_GAP. Later, when packets are inserted, these + * types are overwritten with RTP_JITTER_BUFFER_ELEMENT_TYPE_PACKET. And, when + * packets are output, the associated slot array items have their types set back + * to RTP_JITTER_BUFFER_ELEMENT_TYPE_GAP. This is because gaps are fundamentally + * implicit - they do not exist as their own entities (unlike packets). */ + +static int arm_timeout_timer(struct rtp_jitter_buffer *jitter_buffer); +static void disarm_timeout_timer(struct rtp_jitter_buffer *jitter_buffer); +static void on_timeout_expiration(void *data, uint64_t expirations); + +static inline size_t seqnum_to_slot_index(struct rtp_jitter_buffer *jitter_buffer, + uint16_t seqnum); + +static void store_packet(struct rtp_jitter_buffer *jitter_buffer, + const uint8_t *packet_data, size_t packet_size, size_t header_size, + uint32_t timestamp, uint16_t seqnum, bool check_for_duplicates); + +static int do_drain(struct rtp_jitter_buffer *jitter_buffer, + size_t num_oldest_slots_to_drain); + +int rtp_jitter_buffer_init(struct rtp_jitter_buffer *jitter_buffer, struct rtp_jitter_buffer_params *params) +{ + int ret = 0; + size_t idx; + + spa_assert(jitter_buffer != NULL); + spa_assert(!jitter_buffer->initialized); + spa_assert(params != NULL); + spa_assert(params->num_slots > 0); + spa_assert(params->max_packet_size > 0); + spa_assert(params->packet_duration > 0); + spa_assert(params->loop != NULL); + spa_assert(params->output_rtp_packet != NULL); + spa_assert(params->signal_lost_packets != NULL); + + spa_memzero(jitter_buffer, sizeof(struct rtp_jitter_buffer)); + memcpy(&(jitter_buffer->params), params, sizeof(struct rtp_jitter_buffer_params)); + + /* Set the flag here already to make sure that in case + * of errors, rtp_jitter_buffer_shutdown() correctly + * cleans up any partial initialization. */ + jitter_buffer->initialized = true; + + jitter_buffer->slots = calloc(params->num_slots, sizeof(struct rtp_jitter_buffer_slot)); + if (jitter_buffer->slots == NULL) { + ret = -ENOMEM; + pw_log_error("Could not allocate memory for slots array: %m"); + goto error; + } + + jitter_buffer->packet_buffer = calloc(params->num_slots, params->max_packet_size); + if (jitter_buffer->packet_buffer == NULL) { + ret = -ENOMEM; + pw_log_error("Could not allocate memory for packet buffer: %m"); + goto error; + } + + for (idx = 0; idx < jitter_buffer->params.num_slots; ++idx) + jitter_buffer->slots[idx].element_type = RTP_JITTER_BUFFER_ELEMENT_TYPE_GAP; + + jitter_buffer->hold_back_mode = false; + jitter_buffer->last_seqnum = -1; + + /* Synchronize loop access. See the spa_loop_methods + * documentation; the add_source() documentation states: + * "Must be called from the loop's own thread." By extension, + * this also applies to pw_loop_add_timer(). */ + pw_loop_lock(jitter_buffer->params.loop); + jitter_buffer->timeout_timer = pw_loop_add_timer(params->loop, on_timeout_expiration, + jitter_buffer); + pw_loop_unlock(jitter_buffer->params.loop); + + if (jitter_buffer->timeout_timer == NULL) { + ret = -errno; + pw_log_error("Could not create timeout timer: %m"); + goto error; + } + + pw_log_info("Initialized RTP jitter buffer: num slots: %zu; max packet size: %zu bytes; " + "packet duration: %" PRIu64 " ns; total capacity duration: %" PRIu64 " ns", + params->num_slots, params->max_packet_size, params->packet_duration, + params->packet_duration * params->num_slots); + +finish: + return ret; + +error: + rtp_jitter_buffer_shutdown(jitter_buffer); + goto finish; +} + +void rtp_jitter_buffer_shutdown(struct rtp_jitter_buffer *jitter_buffer) +{ + spa_assert(jitter_buffer != NULL); + + if (!jitter_buffer->initialized) + return; + + if (jitter_buffer->timeout_timer != NULL) { + /* Synchronize loop access. See the spa_loop_methods + * documentation; the remove_source() documentation states: + * "Must be called from the loop's own thread." By extension, + * this also applies to pw_loop_destroy_source(). */ + pw_loop_lock(jitter_buffer->params.loop); + /* Note that this also internally detaches the source from the + * loop, and thus, any previously queued timeout callbacks are + * no longer invoked once this call ends. + * (For more, see the detach_source(), loop_destroy_source(), + * and loop_iterate() functions in spa/plugins/support/loop.c , + * particularly how they handle ep[i].data . Also see how + * remove_from_poll() - called by loop_destroy_source() - and + * loop_iterate() handle remove_count .) */ + pw_loop_destroy_source(jitter_buffer->params.loop, jitter_buffer->timeout_timer); + pw_loop_unlock(jitter_buffer->params.loop); + } + + free(jitter_buffer->slots); + free(jitter_buffer->packet_buffer); + + jitter_buffer->initialized = false; + + pw_log_info("RTP jitter buffer shut down"); +} + +int rtp_jitter_buffer_insert_packet(struct rtp_jitter_buffer *jitter_buffer, + const uint8_t *packet_data, size_t packet_size, + size_t header_size, uint32_t timestamp, uint16_t seqnum) +{ + struct rtp_jitter_buffer_params *params; + int ret = 0; + size_t slot_index; + int16_t seqnum_delta; + + spa_assert(jitter_buffer != NULL); + spa_assert(jitter_buffer->initialized); + spa_assert(packet_data != NULL); + spa_assert(header_size <= packet_size); + + params = &(jitter_buffer->params); + + spa_assert(packet_size <= params->max_packet_size); + + pw_log_trace("Got packet with size %zu and seqnum %" PRIu16 "; valid seqnum window: " + "start seqnum / length: %" PRIu32 " / %" PRIu32, packet_size, seqnum, + jitter_buffer->valid_seqnum_window_start_seqnum, + jitter_buffer->valid_seqnum_window_length); + + if (jitter_buffer->hold_back_mode) { + size_t idx = 0; + size_t num_oldest_slots_with_packets; + + spa_assert(jitter_buffer->last_seqnum >= 0); + + seqnum_delta = calculate_seqnum_delta(jitter_buffer->valid_seqnum_window_start_seqnum, seqnum); + + if (seqnum_delta < 0) { + /* In hold-back mode, sequence numbers at or after the sequence + * number stored in valid_seqnum_window_start_seqnum are of interest. + * Sequence numbers older than that are stale and to be discarded. + * See the overview at the top of this file for details. */ + pw_log_info("Dropping packet with stale or duplicate sequence number %" + PRId32, seqnum); + goto finish; + } else if ((size_t)seqnum_delta < params->num_slots) { + /* Packet is not stale, and fits within the capacity of the jitter + * buffer, that is, its sequence number is not too far ahead of the + * valid_seqnum_window_start_seqnum. Insert it into the buffer + * according to its sequence number. + * + * In case its sequence number is further ahead than the current + * valid seqnum window (but still fits within the jitter buffer + * capacity), it means that this packet actually extends the window + * when inserted into the buffer, and it does so this way: + * + * window_length = MAX(window_length, seqnum - window_start_seqnum + 1) + * + * (seqnum - window_start_seqnum) is stored in seqnum_delta, so this + * becomes: + * + * window_length = MAX(window_length, seqnum_delta + 1) + * + * (The +1 is there because the length stores a size-like quantity, + * so it must be added to avoid an off-by-one error.) + * + * Also see the overview at the top. */ + + jitter_buffer->valid_seqnum_window_length = SPA_MAX( + jitter_buffer->valid_seqnum_window_length, ((size_t)seqnum_delta) + 1); + + store_packet(jitter_buffer, packet_data, packet_size, header_size, + timestamp, seqnum, true); + } else { + /* The packet's sequence number is beyond the valid seqnum window, + * and trying to extend the window to encompass that packet would + * overextend it, that is, its length would exceed num_slots. + * + * (See the overview at the top.) + * + * Check by how much the window would be overextended. If the + * overextension is less than the valid seqnum window length, then + * some of the currently valid slots would remain valid after shifting + * the window. Specifically, the last (valid_seqnum_window_length - + * overextension_amount) slots remain valid, while the ones before that + * need to be drained. The result is a valid seqnum window that has been + * shifted forward by valid_seqnum_window_length, with a length that + * equals the num_slots amount. + * + * seqnum_delta+1 equals the amount of slots from the start of the + * window to the new packet (+1 since the new packet itself is included). + * Since by now, we know that the window would be overextended, this + * implies that (seqnum_delta+1) > num_slots. + * + * The overextension amount therefore is: + * + * overextension_amount = (seqnum_delta+1) - num_slots + * + * This is also the number of oldest slots that need to be drained at the + * start of the valid seqnum window before shifting it. If encompassing + * the new packet increases the window by overextension_amount, draining + * those oldest slots decreases it again by overextension_amount, resulting + * in a window length equaling num_slots. + * + * If however the overextension is equal to or larger than the valid seqnum + * window length, then none of the slots within the window remain valid. + * It therefore makes no sense then to try to keep them around, so the + * jitter buffer is fully drained before inserting the new packet. Also, + * this means that afterwards, the slot that contains the new packet is + * the only remaining one, that is, the window length is 1. */ + + size_t overextension_amount; + bool window_fully_invalid; + size_t num_oldest_slots_to_drain; + bool open_ended = false; + + overextension_amount = ((size_t)seqnum_delta) + 1 - params->num_slots; + window_fully_invalid = (overextension_amount >= jitter_buffer->valid_seqnum_window_length); + num_oldest_slots_to_drain = SPA_MIN(overextension_amount, + jitter_buffer->valid_seqnum_window_length); + + pw_log_debug("Packet with seqnum %" PRIu16 " exceeds capacity of jitter buffer " + "in hold-back mode; window shift amount: %zu window fully invalid: %d " + "num oldest slots to drain: %zu", seqnum, overextension_amount, + window_fully_invalid, num_oldest_slots_to_drain); + + if (num_oldest_slots_to_drain > 0) { + ret = do_drain(jitter_buffer, num_oldest_slots_to_drain); + if (SPA_UNLIKELY(ret < 0)) { + ret = -EIO; + goto finish; + } + } + + /* If the window was fully invalidated (= drained), insert a packet loss + * signal to allow callers to append some sort of PLC / fadeout to the + * drained data. This is helpful for avoiding hard cutoffs in the output. */ + if (window_fully_invalid) { + ssize_t gap_length; + uint16_t one_past_last_valid_seqnum; + + one_past_last_valid_seqnum = jitter_buffer->valid_seqnum_window_start_seqnum + + jitter_buffer->valid_seqnum_window_length; + gap_length = calculate_seqnum_delta(one_past_last_valid_seqnum, seqnum); + if (SPA_UNLIKELY(gap_length < 0)) { + /* This would indicate a serious error in the calculations, + * so log it accordingly. */ + pw_log_error("Negative packet loss amount %zd detected; valid seqnum " + "window start seqnum / length: %" PRIu32 " / %" PRIu32 "; " + "using packet loss amount 0 instead", + gap_length, jitter_buffer->valid_seqnum_window_start_seqnum, + jitter_buffer->valid_seqnum_window_length); + gap_length = 0; + } + + /* If the gap exceeds the capacity of the jitter buffer, then limit the + * packet loss amount and consider the gap open ended to avoid cases where + * callers otherwise would have to conceal an excessive amount of packet loss. */ + if ((size_t)gap_length > params->num_slots) { + open_ended = true; + gap_length = params->num_slots; + } + + if (gap_length > 0) { + if (open_ended) { + pw_log_debug("Signaling packet loss, starting at seqnum %" + PRIu16 ", gap length %zd, open ended", + one_past_last_valid_seqnum, gap_length); + } else { + pw_log_debug("Signaling packet loss, starting at seqnum %" + PRIu16 ", gap length %zd, not open ended", + one_past_last_valid_seqnum, gap_length); + } + if ((ret = params->signal_lost_packets(params->context, + one_past_last_valid_seqnum, (size_t)gap_length, open_ended)) != 0) { + pw_log_error("Could not signal lost RTP packet: %s", spa_strerror(ret)); + goto finish; + } + } + } + + /* As explained in the overview, a shift that fully invalidates + * the window is a different case than one that retains entries + * from the old window. */ + if (window_fully_invalid) { + jitter_buffer->valid_seqnum_window_start_seqnum = seqnum; + jitter_buffer->valid_seqnum_window_length = 1; + } else { + /* If the window is not fully invalid, then figure out + * its new start_seqnum by moving backwards. A not fully + * invalid window is of length num_slots (see the overview + * for why), and ends at the sequence number of the new + * packet. Therefore, it starts at (seqnum +1 - num_slots). + * (+1 since the new packet itself also has to be factored in.) */ + jitter_buffer->valid_seqnum_window_start_seqnum = seqnum + 1 - params->num_slots; + jitter_buffer->valid_seqnum_window_length = params->num_slots; + } + pw_log_debug("Reset valid seqnum window to start at seqnum %" PRIu32 " and be of length %" + PRIu32, jitter_buffer->valid_seqnum_window_start_seqnum, + jitter_buffer->valid_seqnum_window_length); + + /* Now write this new packet into the packet buffer to hold + * it back until the gap before it is filled. */ + store_packet(jitter_buffer, packet_data, packet_size, header_size, + timestamp, seqnum, false); + + /* NOTE: This assumes that spa_system_timerfd_settime() behaves just + * like timerfd_settime() in the sense that it clears any timer + * expirations that haven't been noticed yet. In other words, even + * if the timer expired already, rearming by calling + * spa_system_timerfd_settime() is assumed to implicitly wipe + * these prior expirations. + * + * This is precisely what timerfd_settime() does. From its manpage: + * + * > If the timer has already expired one or more times ***since its + * > settings were last modified using timerfd_settime()***, or since + * > the last successful read(2), then the buffer given to read(2) + * > returns an unsigned 8-byte integer (uint64_t) containing the + * > number of expirations that have occurred. + * + * And this is important to make sure that, should the timer expire while + * we are here, it does not lead to an immediate jitter buffer draining. + */ + if (ret == 0) { + pw_log_debug("Rearming timeout timer as part of resetting hold-back mode"); + ret = arm_timeout_timer(jitter_buffer); + if (SPA_UNLIKELY(ret < 0)) { + pw_log_error("Could not arm timer: %s", spa_strerror(ret)); + ret = -EINVAL; + } + } else { + pw_log_warn("Not rearming timeout timer due to earlier error while draining"); + } + + /* Next, let the logic below check the contents of the new, + * updated valid seqnum window. */ + } + + /* After inserting the packet, go through the valid window and check if + * the oldest N slots contain packets without gaps in between them. First, + * N (= num_oldest_slots_with_packets) has to be determined. Iterate over + * the slots, starting at the valid seqnum window start, until either, + * a slot with a gap inside is encountered, or the end of the window is + * reached. That way, the number of oldest N held back packets that present + * in an uninterrupted sequence in the oldest N slots and thus can be output + * is determined. */ + for (idx = 0; idx < jitter_buffer->valid_seqnum_window_length; ++idx) { + uint16_t item_seqnum = jitter_buffer->valid_seqnum_window_start_seqnum + idx; + slot_index = seqnum_to_slot_index(jitter_buffer, item_seqnum); + if (jitter_buffer->slots[slot_index].element_type != + RTP_JITTER_BUFFER_ELEMENT_TYPE_PACKET) + break; + } + num_oldest_slots_with_packets = idx; + + if (num_oldest_slots_with_packets > 0) { + pw_log_debug("The first %zu slots in valid seqnum window all contain " + "packets - these packets can be output immediately", + num_oldest_slots_with_packets); + + /* Now output the packets. */ + for (idx = 0; idx < num_oldest_slots_with_packets; ++idx) { + struct rtp_jitter_buffer_slot *slot; + uint16_t item_seqnum; + const uint8_t *packet_data_to_output; + + /* This implicitly does the sequence number wrap-around + * by storing the value in uint16_t. */ + item_seqnum = jitter_buffer->valid_seqnum_window_start_seqnum + idx; + + slot_index = seqnum_to_slot_index(jitter_buffer, item_seqnum); + slot = &(jitter_buffer->slots[slot_index]); + + packet_data_to_output = jitter_buffer->packet_buffer + + params->max_packet_size * slot_index; + + ret = params->output_rtp_packet(params->context, packet_data_to_output, + slot->packet_size, slot->header_size, slot->timestamp, item_seqnum); + if (SPA_UNLIKELY(ret < 0)) { + pw_log_error("Could not output RTP packet: %s", spa_strerror(ret)); + ret = -EIO; + goto finish; + } + + slot->element_type = RTP_JITTER_BUFFER_ELEMENT_TYPE_GAP; + } + + /* Since num_oldest_slots_with_packets is the result of a for-loop that counts up + * to valid_seqnum_window_length, this assertion should always hold. */ + spa_assert(jitter_buffer->valid_seqnum_window_length >= num_oldest_slots_with_packets); + + /* Move the window start further to move past the slots whose packets + * were just output. This also reduces the valid seqnum window length. */ + jitter_buffer->valid_seqnum_window_start_seqnum += num_oldest_slots_with_packets; + jitter_buffer->valid_seqnum_window_length -= num_oldest_slots_with_packets; + + pw_log_trace("%zu packet(s) output in hold-back mode; valid seqnum window " + "is now: start seqnum / length: %" PRIu32 " / %" PRIu32, + num_oldest_slots_with_packets, + jitter_buffer->valid_seqnum_window_start_seqnum, + jitter_buffer->valid_seqnum_window_length); + + /* If the valid seqnum window length has become is zero, it means that + * all held-back packets have been output, and no gaps remain. + * This in turn means that we are done - all previously missing packets + * have been received and have been output in order of their sequence + * numbers. Switch back to regular mode and set the seqnum of the last + * output packet the last_seqnum. */ + if (jitter_buffer->valid_seqnum_window_length == 0) { + jitter_buffer->hold_back_mode = false; + /* Don't set this to the newly arrived packet's seqnum directly. + * Hold-back mode is active, which means that packets previously + * arrived out of order - so, the new packet too might not have + * arrived in order. */ + jitter_buffer->last_seqnum = jitter_buffer->valid_seqnum_window_start_seqnum - 1; + /* Also cancel the timeout timer to not trigger an erroneous drain. */ + disarm_timeout_timer(jitter_buffer); + pw_log_debug("No more packets held back; switched back to regular mode, " + "last seqnum set to %" PRId32, jitter_buffer->last_seqnum); + } + } + } else { + /* Here, we are in regular mode. */ + + /* If a preceding sequence number is known, use it to detect gaps. For + * example, last sequence number is 400, this packet's sequence number + * is 402 -> there is a gap at sequence number 401. This can happen due + * to packet loss and out-of-order packet transmission. + * + * (Also see the overview at the top.) */ + if (jitter_buffer->last_seqnum >= 0) { + seqnum_delta = calculate_seqnum_delta(jitter_buffer->last_seqnum, seqnum); + + if (seqnum_delta <= 0) { + /* In the regular mode, it is implied that all preceding + * packets were in order of the sequence numbers, and that + * any missing packets have been also announced in-order. + * Therefore, if at this point, the delta is <= 0, it means + * that a packet with a sequence number equal to or lower + * than the previous packet's sequence number arrived + * (taking possible wraparound into account). This packet + * may be a duplicate, and is unusable, so drop it. */ + + pw_log_info("Dropping packet with stale sequence number %" PRId32, seqnum); + goto finish; + } else if (seqnum_delta > 1) { + /* If the delta is >1, it indicates a gap. This can happen + * due to out-of-order packets or due to packet loss. + * Activate hold-back mode: Hold back the received packet + * and any further ones until there are consecutive + * sequences of packets available, which can be output. + * And, write information about that packet and the gap into + * the associated slots. + * Also arm the timeout timer in case the missing packets + * do not arrive in time. */ + + jitter_buffer->hold_back_mode = true; + /* Set the valid seqnum window start to the sequence number that + * was actually expected as the one that follows the last packet. + * Since this place was reached, it implies that the current packet's + * sequence number does not equal to that one, and thus, the packet + * we actually expected is still missing. This is the oldest packet + * we expect here, since packets with a sequence number preceding + * that packet's have been processed already at this point. */ + jitter_buffer->valid_seqnum_window_start_seqnum = + ((uint16_t)jitter_buffer->last_seqnum) + 1; + /* The current valid seqnum window length equals the seqnum_delta. This + * is because the window goes from valid_seqnum_window_start_seqnum to + * the last slot with a packet in the valid seqnum window (both inclusive). + * For example, if valid_seqnum_window_start_seqnum is initially set + * to 301 here, and the current packet has sequence number 305, it + * means that the window goes from 301 to 305, so it consists of + * 5 slots (the first 4 contain the gap from 301 to 304, and the fifth + * slot contains packet 305). + * + * seqnum_delta equals: + * + * (this current packet's seqnum - last_seqnum) + * + * Substituting in the seqnum_delta formula, we get: + * + * valid_seqnum_window_start_seqnum = last_seqnum + 1 + * -> last_seqnum = valid_seqnum_window_start_seqnum - 1 + * + * and: + * + * seqnum_delta = + * (this current packet's seqnum - last_seqnum) = + * (this current packet's seqnum - (valid_seqnum_window_start_seqnum - 1)) = + * (this current packet's seqnum - valid_seqnum_window_start_seqnum + 1) + * + * which is exactly the formula needed to compute a size or length + * from A to B (or, in this case, from valid_seqnum_window_start_seqnum + * to the packet's seqnum). */ + jitter_buffer->valid_seqnum_window_length = seqnum_delta; + + pw_log_debug("Gap detected starting at seqnum %" PRIu32 ", length %" + PRId16 "; switched to hold-back mode", + jitter_buffer->valid_seqnum_window_start_seqnum, + seqnum_delta - 1); + + if (SPA_UNLIKELY(jitter_buffer->valid_seqnum_window_length > params->num_slots)) { + /* It is possible that the sudden gap that leads to the + * activation of the hold-back mode is so big that it + * immediately overextends the valid seqnum window. + * However, unlike in the hold-back cases handled above, + * here, nothing is held back yet, so there are no slots + * that can remain valid. Therefore, this behaves just like + * in the window_fully_invalid == true case further above. + * + * In most cases, the gap is way larger than the number of + * slots. There is a corner case though, and that is when + * the window length equals (num_slots + 1). Keep in mind + * that the window includes the gap _and_ the new packet - + * hence the +1. From this it follows that if the window + * length equals num_slots+1, it means that the actual + * gap is num_slots in length. This is essentially the + * largest possible gap that still isn't open ended. For + * this reason, a check is made to identify this corner case. + * + * In either case, the correct, uninterrupted, in-order + * packets that precede this gap are followed by proper PLC. + * Should the gap be open ended, PLC can be augmented to + * include a fadeout for example. + * + * And, since an overextended window is invalid, and in this + * case, no held-back packets are presents, the valid seqnum + * window is adjusted to only contain the current packet. + * + * After signaling, the jitter buffer switches back to regular + * mode. This is because after the large gap, nothing is held + * back anymore - there is no reason for staying in the + * hold-back mode. */ + + bool open_ended = jitter_buffer->valid_seqnum_window_length > + (params->num_slots + 1); + + pw_log_debug("This gap immediately overextends the valid seqnum " + "window; signaling packet loss, starting at seqnum %" + PRIu16 ", gap length %zd, %sopen ended", + jitter_buffer->valid_seqnum_window_start_seqnum, + params->num_slots, open_ended ? "" : "not "); + + if ((ret = params->signal_lost_packets(params->context, + jitter_buffer->valid_seqnum_window_start_seqnum, + params->num_slots, open_ended)) != 0) { + pw_log_error("Could not signal lost RTP packet: %s", spa_strerror(ret)); + goto finish; + } + + pw_log_debug("Switching back to regular mode and emitting packet " + "after the large gap"); + + jitter_buffer->hold_back_mode = false; + ret = params->output_rtp_packet(params->context, packet_data, + packet_size, header_size, timestamp, seqnum); + jitter_buffer->last_seqnum = seqnum; + goto finish; + + } + + store_packet(jitter_buffer, packet_data, packet_size, header_size, + timestamp, seqnum, false); + + ret = arm_timeout_timer(jitter_buffer); + if (SPA_UNLIKELY(ret < 0)) { + pw_log_error("Could not arm timer: %s", spa_strerror(ret)); + ret = -EINVAL; + } + + goto finish; + } + } + + /* If hold-back mode is not active, just forward the packet. */ + ret = params->output_rtp_packet(params->context, packet_data, packet_size, + header_size, timestamp, seqnum); + + jitter_buffer->last_seqnum = seqnum; + } + +finish: + return ret; +} + +int rtp_jitter_buffer_drain(struct rtp_jitter_buffer *jitter_buffer) +{ + int ret; + + spa_assert(jitter_buffer != NULL); + spa_assert(jitter_buffer->initialized); + + if (!jitter_buffer->hold_back_mode) + return 0; + + ret = do_drain(jitter_buffer, jitter_buffer->valid_seqnum_window_length); + + /* Not calling signal_lost_packets with open_ended = true here, since + * that one is meant for cases where mid-stream, the hold-back mode was + * activated, and a sudden large sequence jump occurred. This function + * here however is called externally. */ + + jitter_buffer->hold_back_mode = false; + jitter_buffer->last_seqnum = -1; + pw_log_debug("Switching back to regular mode after explicit drain"); + + /* Disarm any potentially ongoing timer since the + * jitter buffer just switched back to regular mode. */ + disarm_timeout_timer(jitter_buffer); + + return ret; +} + +void rtp_jitter_buffer_flush(struct rtp_jitter_buffer *jitter_buffer) +{ + size_t idx; + + spa_assert(jitter_buffer != NULL); + spa_assert(jitter_buffer->initialized); + + jitter_buffer->hold_back_mode = false; + jitter_buffer->last_seqnum = -1; + pw_log_debug("Switching back to regular mode after flush"); + + for (idx = 0; idx < jitter_buffer->params.num_slots; ++idx) + jitter_buffer->slots[idx].element_type = RTP_JITTER_BUFFER_ELEMENT_TYPE_GAP; + + /* Disarm any potentially ongoing timer since the + * jitter buffer just switched back to regular mode. */ + disarm_timeout_timer(jitter_buffer); +} + +static int set_timeout(struct rtp_jitter_buffer *jitter_buffer, uint64_t timeout) +{ + struct itimerspec ts; + + ts.it_value.tv_sec = timeout / SPA_NSEC_PER_SEC; + ts.it_value.tv_nsec = timeout % SPA_NSEC_PER_SEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + + return spa_system_timerfd_settime(jitter_buffer->params.loop->system, + jitter_buffer->timeout_timer->fd, 0, &ts, NULL); +} + +static int arm_timeout_timer(struct rtp_jitter_buffer *jitter_buffer) +{ + struct rtp_jitter_buffer_params *params = &(jitter_buffer->params); + + /* Set the timeout to expire at the total duration + * covered by the packet buffer's capacity. */ + return set_timeout(jitter_buffer, + params->num_slots * params->packet_duration); +} + +static void disarm_timeout_timer(struct rtp_jitter_buffer *jitter_buffer) +{ + set_timeout(jitter_buffer, 0); +} + +static void on_timeout_expiration(void *data, uint64_t expirations) +{ + struct rtp_jitter_buffer *jitter_buffer = data; + + pw_log_info("Timeout timer expired; draining jitter buffer"); + do_drain(jitter_buffer, jitter_buffer->valid_seqnum_window_length); + jitter_buffer->hold_back_mode = false; + jitter_buffer->last_seqnum = -1; +} + +static void store_packet(struct rtp_jitter_buffer *jitter_buffer, const uint8_t *packet_data, + size_t packet_size, size_t header_size, uint32_t timestamp, + uint16_t seqnum, bool check_for_duplicates) +{ + size_t slot_index; + uint8_t *dest; + struct rtp_jitter_buffer_params *params = &(jitter_buffer->params); + struct rtp_jitter_buffer_slot *slot; + + slot_index = seqnum_to_slot_index(jitter_buffer, seqnum); + slot = &(jitter_buffer->slots[slot_index]); + + if (check_for_duplicates) { + if (SPA_UNLIKELY(slot->element_type == RTP_JITTER_BUFFER_ELEMENT_TYPE_PACKET)) { + pw_log_debug("Packet with seqnum %" PRIu16 " has already been inserted; " + "this is a duplicate; dropping", seqnum); + return; + } + } + + dest = jitter_buffer->packet_buffer + params->max_packet_size * slot_index; + memcpy(dest, packet_data, packet_size); + + slot->element_type = RTP_JITTER_BUFFER_ELEMENT_TYPE_PACKET; + slot->packet_size = packet_size; + slot->header_size = header_size; + slot->timestamp = timestamp; +} + +static inline size_t seqnum_to_slot_index(struct rtp_jitter_buffer *jitter_buffer, + uint16_t seqnum) +{ + return ((size_t)seqnum) % jitter_buffer->params.num_slots; +} + +static int do_drain(struct rtp_jitter_buffer *jitter_buffer, + size_t num_oldest_slots_to_drain) +{ + /* Important: This intentionally does not reset valid_seqnum_window_length + * or valid_seqnum_window_start_seqnum. That is up to the caller, since + * this function is called in several different situations that require + * different handling of these states. */ + + int ret = 0; + size_t idx; + int32_t first_drained_packet_seqnum = -1; + uint16_t first_lost_packet_seqnum = 0; + bool tracking_lost_packets = false; + struct rtp_jitter_buffer_params *params = &(jitter_buffer->params); + + spa_assert(num_oldest_slots_to_drain <= jitter_buffer->valid_seqnum_window_length); + + if (num_oldest_slots_to_drain == 0) + return 0; + + if (num_oldest_slots_to_drain == jitter_buffer->valid_seqnum_window_length) { + pw_log_debug("Draining all slots in the entire valid seqnum window"); + } else { + pw_log_debug("Draining the first (= oldest) %zu slots in the valid seqnum window; " + "window start seqnum / length: %" PRIu32 " / %" PRIu32, + num_oldest_slots_to_drain, + jitter_buffer->valid_seqnum_window_start_seqnum, + jitter_buffer->valid_seqnum_window_length); + } + + for (idx = 0; idx < num_oldest_slots_to_drain; ++idx) { + uint16_t seqnum = jitter_buffer->valid_seqnum_window_start_seqnum + idx; + size_t slot_index = seqnum_to_slot_index(jitter_buffer, seqnum); + struct rtp_jitter_buffer_slot *slot = &(jitter_buffer->slots[slot_index]); + + if (slot->element_type == RTP_JITTER_BUFFER_ELEMENT_TYPE_PACKET) { + const uint8_t *packet_data; + + if (tracking_lost_packets) { + int16_t seqnum_delta = calculate_seqnum_delta( + first_lost_packet_seqnum, seqnum); + + tracking_lost_packets = false; + + if (SPA_LIKELY(seqnum_delta > 0)) { + pw_log_debug("Signaling packet loss sequence, starting at " + "seqnum %" PRIu16 ", gap length %zu, not open ended", + first_lost_packet_seqnum, (size_t)seqnum_delta); + if ((ret = params->signal_lost_packets(params->context, + first_lost_packet_seqnum, (size_t)seqnum_delta, false)) < 0) { + goto finish; + } + } else { + pw_log_error("Negative delta %" PRId16" between first and last " + "seqnum in lost seqnum range %" PRIu16 " - %" PRIu16 + "encountered while draining", seqnum_delta, + first_lost_packet_seqnum, seqnum); + } + } + + packet_data = jitter_buffer->packet_buffer + params->max_packet_size * slot_index; + + if ((ret = params->output_rtp_packet(params->context, packet_data, + slot->packet_size, slot->header_size, slot->timestamp, seqnum)) < 0) + goto finish; + + slot->element_type = RTP_JITTER_BUFFER_ELEMENT_TYPE_GAP; + + if (first_drained_packet_seqnum < 0) + first_drained_packet_seqnum = seqnum; + } else { + /* If a packet is not available, consider it lost. + * Do not announce it right away - instead, switch + * to a tracking mode, and keep iterating over the + * table. That way, it becomes possible to announce + * entire spans of lost packets in one go, as a gap + * with multiple packets, which can be more efficient + * for PLC. For example, when 10 packets in a row are + * lost, that would then announce a gap that is 10 + * packets long instead of announcing 10 times that + * a packet was lost. */ + if (!tracking_lost_packets) { + first_lost_packet_seqnum = seqnum; + tracking_lost_packets = true; + + if (first_drained_packet_seqnum >= 0) { + uint16_t last_drained_packet_seqnum = + jitter_buffer->valid_seqnum_window_start_seqnum + (idx - 1); + pw_log_debug("Drained packet(s) with seqnums %" PRId32 " - %" PRIu16, + first_drained_packet_seqnum, last_drained_packet_seqnum); + first_drained_packet_seqnum = -1; + } + } + } + } + + if (first_drained_packet_seqnum >= 0) { + uint16_t last_drained_packet_seqnum = jitter_buffer->valid_seqnum_window_start_seqnum + (idx - 1); + pw_log_debug("Drained packet(s) with seqnums %" PRId32 " - %" PRIu16, first_drained_packet_seqnum, + last_drained_packet_seqnum); + } + + if (tracking_lost_packets) { + /* This can happen when the valid seqnum window is only partially + * drained, that is, num_oldest_slots_to_drain <= valid_seqnum_window_length. */ + + uint16_t last_lost_packet_seqnum; + int16_t seqnum_delta; + + last_lost_packet_seqnum = jitter_buffer->valid_seqnum_window_start_seqnum + (idx - 1); + seqnum_delta = calculate_seqnum_delta(first_lost_packet_seqnum, last_lost_packet_seqnum) + 1; + + spa_assert(seqnum_delta >= 0); + + pw_log_debug("Signaling final packet loss sequence, starting at " + "seqnum %" PRIu16 ", gap length %zu, not open ended", + first_lost_packet_seqnum, (size_t)seqnum_delta); + + /* This final gap is always signaled as a non-open-ended loss, + * since its size is exactly defined. Open-ended packet losses + * happen in cases where the packet loss results in a gap that + * is too large for any packet loss concealment mechanism to + * fully cover (or its end not even known, so PLC is not fully + * applicable). */ + if ((ret = params->signal_lost_packets(params->context, + first_lost_packet_seqnum, (size_t)seqnum_delta, false)) < 0) { + goto finish; + } + } + +finish: + return ret; +} + diff --git a/src/modules/module-rtp/jitter-buffer.h b/src/modules/module-rtp/jitter-buffer.h new file mode 100644 index 000000000..408eaf567 --- /dev/null +++ b/src/modules/module-rtp/jitter-buffer.h @@ -0,0 +1,317 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2026 Carlos Rafael Giani */ +/* SPDX-License-Identifier: MIT */ + +#ifndef PIPEWIRE_RTP_JITTER_BUFFER_H +#define PIPEWIRE_RTP_JITTER_BUFFER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +struct rtp_jitter_buffer_params { + /* How many slots the jitter buffer shall have. This defines + * the capacity of the jitter buffer. Must be at least 1. + * For a more detailed explanation what slots are, see + * further below. */ + size_t num_slots; + /* Maximum size of the packets, in bytes. This must include + * the RTP header bytes. Must be at least 1. */ + size_t max_packet_size; + /* Duration of each packet, in nanoseconds. + * Must be at least 1. */ + uint64_t packet_duration; + /* PipeWire loop, used for setting up a timeout timer. + * This needs to be a loop that runs in the same thread + * as this jitter buffer. + * This must be set to a valid pointer. */ + struct pw_loop *loop; + /* User-defined context that is passed to the function pointers. */ + void *context; + + /* Function pointer called when the jitter buffer decides to output + * an RTP packet. In regular mode, this is immediately invoked when + * rtp_jitter_buffer_insert_packet() is called. No data is copied + * then; this function pointer is directly passed the packet_data and + * packet_size argument values that rtp_jitter_buffer_insert_packet() + * was called with. In hold-back mode, this is called when the jitter + * buffer is able to output previously held back packets in order + * (for example, because a preceding packet just now arrived), or + * when the jitter buffer gets drained. packet_data points to the + * bytes of the RTP packet that is output, and packet_size contains + * the size of the packet in bytes. + * + * Should this function experience an error, it returns a negated + * errno, and 0 if it succeeds. In case of an error, if the overall + * logic can continue, it is recommended to fully reset the jitter + * buffer by calling rtp_jitter_buffer_flush(). + * + * This must be set to a valid pointer. */ + int (*output_rtp_packet)(void *context, const uint8_t *packet_data, size_t packet_size, + size_t header_size, uint32_t timestamp, uint16_t seqnum); + /* Function pointer called when the jitter buffer detected one or + * more lost packets in a row. For example, if the last received packet + * had sequence number 16, and then, the jitter buffer received packet + * with sequence number 26, then 27, 28, etc., it eventually will + * detect that packets 17 through 25 are lost, and call this, setting + * seqnum_of_first_lost_packet to 17, and gap_length to 8. The last + * argument is set to false if the packet loss is detected mid stream + * when the gap is small enough that measures like PLC can reasonably + * cover all lost packets. If the detected gap is too large, open_ended + * will be set to true. Implementations must then interpret the value of + * gap_length as a maximum; whatever PLC measures the implementation + * uses must not produce output larger than gap_length. This is a + * safety measure to avoid cases where PLC would have to produce an + * excessive amount of data. + * + * When open_ended is true, implementations should also apply a fade + * out at the end of the produced PLC content to avoid a potential + * hard cutoff at the end. + * + * Should this function experience an error, it returns a negated + * errno, and 0 if it succeeds. In case of an error, if the overall + * logic can continue, it is recommended to fully reset the jitter + * buffer by calling rtp_jitter_buffer_flush(). + * + * This must be set to a valid pointer. */ + int (*signal_lost_packets)(void *context, uint16_t seqnum_of_first_lost_packet, + size_t gap_length, bool open_ended); +}; + +enum rtp_jitter_buffer_element_type { + RTP_JITTER_BUFFER_ELEMENT_TYPE_PACKET, + RTP_JITTER_BUFFER_ELEMENT_TYPE_GAP +}; + +/* Slot structure. A slot stores an element (a packer or + * a gap). Slots outside of the valid seqnum window have + * undefined contents. See the "slots" array documentation + * in the rtp_jitter_buffer structure below. */ +struct rtp_jitter_buffer_slot { + enum rtp_jitter_buffer_element_type element_type; + /* This is only used when the element_type is + * RTP_JITTER_BUFFER_ELEMENT_TYPE_PACKET. See + * the packet_buffer documentation below for more. */ + size_t packet_size; + /* Size of the RTP header. This must be less than + * or equal to packet_size. The packet data that + * is stored in packet_buffer includes this header. */ + size_t header_size; + /* RTP timestamp. */ + uint32_t timestamp; +}; + +struct rtp_jitter_buffer { + /* Copy of the params passed to rtp_jitter_buffer_init(). */ + struct rtp_jitter_buffer_params params; + + /* Set to true by rtp_jitter_buffer_init(), and set to false + * by rtp_jitter_buffer_shutdown(). */ + bool initialized; + + /* Array of slots. The size of this array equals num_slots. + * Only slots that are within the valid seqnum window have + * valid values in this array. For example, if the slot at + * array index 3 corresponds to sequence number 400, but + * the valid seqnum window starts at 402, then the value + * in this array at index 3 has no meaning. Not used when + * regular mode is active. */ + struct rtp_jitter_buffer_slot *slots; + + /* Buffer for storing packets in hold-back mode. The size + * of this buffer is max_packet_size*num_slots. To access + * a packet, do it this way: + * + * packet_data = &(packet_buffer[max_packet_size * index]); + * + * Doing this is technically less space efficient if the actual + * packet size is smaller than max_packet_size, but it makes + * buffer management and packet addressing much easier, and + * avoids costly reallocations. The packet_size field in the + * slot structure specifies how many bytes at that location + * actually make up the packet. FOr example, if the slot at + * index 5 in the slots array has its element_type value set + * to RTP_JITTER_BUFFER_ELEMENT_TYPE_PACKET, then at location + * &(packet_buffer[max_packet_size * 5]), valid packet data + * can be found. And, starting from that location, the amount + * of bytes the packet there is made of equals the packet_size + * value of the slot at index 5. + * + * Only the locations that correspond to valid slots with + * a packet as element contain valid data. For example, if the + * slot at index 3 corresponds to sequence number 400, but the + * valid seqnum window starts at 402, then the data at location + * &(packet_buffer[max_packet_size * 3]) has no meaning. Also, + * if the slot does lie within the valid window, but the type + * of the contained element is RTP_JITTER_BUFFER_ELEMENT_TYPE_GAP, + * then the associated location in this packert buffer is + * meaningless. Not used when regular mode is active. */ + uint8_t *packet_buffer; + + /* If true, hold-back mode is active. */ + bool hold_back_mode; + /* Sequence number of the last packet observed while in + * regular mode. If no last packet is known (at startup for + * example), this is set to -1. + * Not used when hold-back mode is active. */ + int32_t last_seqnum; + /* Timer that, when it times out, drains all slots and switches + * back to regular mode. This is armed when hold-back mode is + * activated, and when it is reset. + * Not used when regular mode is active. */ + struct spa_source *timeout_timer; + /* These define the valid seqnum window. Packet sequence numbers + * in the [start_seqnum .. (start_seqnum + window_length - 1)] + * range (both start and end of it inclusive) are within this + * window, and are considered valid. A sequence number below the + * window's start_seqnum is considered stale. A sequence number + * beyond the end of the window extends the window; the window + * length is increased such that this new sequence number is at + * the very end of the window, that is, the length is extended + * such that new_seqnum == (start_seqnum + window_length - 1) + * + * However, if this overextends the window (meaning that the + * window length would be greater than the number of slots), + * the logic in rtp_jitter_buffer_insert_packet() will adjust + * the window's start_seqnum and length. + * + * If hold-back mode is off, these two values have no meaning. + * + * This window is necessary for the following: + * + * 1. It allows for detecting and dropping old, stale packets + * by looking at their sequence numbers. + * 2. It detects when currently held-back packets need to be + * drained, that is, the gap that precedes them is not getting + * filled in time. This happens when packets are lost. + * 3. It defines what slots are valid. Only those slots that are + * associated with sequence numbers that fit inside this + * window are valid. Slots outside of the window have no + * defined nor meaningful content (that is, packet size, + * packet buffer data, or slot element type). + * + * Note that even though valid_seqnum_window_start_seqnum is a + * sequence number, it is a uint32_t instead of a uint16_t, for + * easier arithmetics. */ + uint32_t valid_seqnum_window_start_seqnum; + uint32_t valid_seqnum_window_length; +}; + +/* Initializes an uninitialized rtp_jitter_buffer instance with the given params. + * + * See rtp_jitter_buffer_params for details about the input parameters. + * + * params must be a valid pointer. An internal copy of the rtp_jitter_buffer_params + * instance that pointer refers to will be made, so the caller does not have to + * keep that instance around for the duration of the jitter buffer's lifetime. + * + * The jitter buffer must not be already initialized. + * + * Returns 0 if initialization was successful, nonzero in case of an error. + * If an error happens, this automatically calls rtp_jitter_buffer_shutdown(). */ +int rtp_jitter_buffer_init(struct rtp_jitter_buffer *jitter_buffer, struct rtp_jitter_buffer_params *params); + +/* Shuts down a previously initialized jitter buffer. + * + * jitter_buffer must be a valid pointer. + * + * Calling this on an uninitialized jitter buffer results in a no-op. */ +void rtp_jitter_buffer_shutdown(struct rtp_jitter_buffer *jitter_buffer); + +/* Returns true if the jitter buffer is initialized. */ +static inline bool rtp_jitter_buffer_is_initialized(struct rtp_jitter_buffer *jitter_buffer) +{ + return jitter_buffer->initialized; +} + +/* Inserts an RTP packet into the jitter buffer. + * + * Depending on the sequence number of the RTP packet, the jitter buffer + * will immediately output that packet, or hold it back inside a slot + * for purposes of reordering and packet loss detection. (The packet's data + * will be copied into the packet buffer.) The packet order is defined by + * the RTP sequence number. (16-bit unsigned int RTP sequence wrap-around + * is handled properly.) + * + * If for example packets with sequence numbers 1, 4, 3, 5 arrived (in this + * incorrect order), packet 1 will have been output immediately, but packets + * 4, 3, 5 will have been held back (due to the gap at 2 and the incorrect + * order of packets). If later, packet 2 arrives, the gap at 2 is filled, + * and the jitter buffer will then output all packets from 2 to 5, in the + * correct order. If however packet 2 never arrives, but packets past + * packet 5 keep arriving, then eventually, packet 2 will be considered + * lost (causing signal_lost_packets() to be called), and the held-packets + * 4, 3, 5 will be drained (and output via output_rtp_packet(), in correct + * order 3, 4, 5). + * + * jitter_buffer and packet_data must be valid pointers. packet_size must + * be the size of the memory block pointed to by packet_data . + * + * header_size is the size of the RTP header, and must be less than or equa + * to packet_size. timestamp is the RTP timestamp. + * + * Note that while the values of header_size, timestamp, seqnum could be + * parsed from the packet header, this is not done by this function for + * efficiency reasons. Parsing those may require byte swapping to handle + * endianness, and computing the header size may depend on whether the + * header has extensions or not. These computations and proceses are done + * once, by the caller, and passed to this call via the arguments. + * + * Returns 0 if the call was successful, nonzero in case of an error. + * If an error happens, consider the jitter buffer as no longer usable + * (it can only be shut down then). */ +int rtp_jitter_buffer_insert_packet(struct rtp_jitter_buffer *jitter_buffer, + const uint8_t *packet_data, size_t packet_size, + size_t header_size, uint32_t timestamp, uint16_t seqnum); + +/* Drains all held-back packets and reports packet loss based on those packets. + * + * If the jitter buffer is currently holding back packets due to some packets + * having been added out of order previously, or because gaps were detected, this + * drains them, causing the output_rtp_packet() and signal_lost_packets() function + * pointers from the params passed to rtp_jitter_buffer_init() to be called. If + * for example packets with sequence numbers 1, 4, 3, 5 arrived (in this incorrect + * order), packet 1 will have been output immediately, but packets 4, 3, 5 will + * have been held back (due to the gap at 2 and the incorrect order of packets). + * If then, this function is called, the jitter buffer will (in this order) call + * signal_lost_packets() to signal the loss of packet 2, and then call + * output_rtp_packet() to output packets 3, 4, 5 (in that corrected order). + * + * After draining, the jitter buffer will be back in regular mode. + * + * If no packets were added, or if the jitter buffer is in regular mode, + * this does nothing. + * + * The jitter buffer also has an internal timer that is activated as soon + * as gaps and out of order packets are detected. If that timer expires, + * the jitter buffer will automatically drain itself. If however all gaps + * are filled in time, the timer is deactivated, and no automatic drain occurs. + * + * jitter_buffer must be a valid pointer. + * + * Returns 0 if the call was successful, nonzero in case of an error. + * If an error happens, consider the jitter buffer as no longer usable + * (it can only be shut down then). */ +int rtp_jitter_buffer_drain(struct rtp_jitter_buffer *jitter_buffer); + +/* Flushes all held-back packets. + * + * Unlike draining, this does not call any function pointers. Instead, it + * flushes any and all held-back packets (meaning, these packets are all + * gone, and _not_ reported as lost via signal_lost_packets()), and + * resets the jitter buffer back to the regular mode. + * + * This is useful for performing a hard reset on the jitter buffer. + * + * jitter_buffer must be a valid pointer. */ +void rtp_jitter_buffer_flush(struct rtp_jitter_buffer *jitter_buffer); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* PIPEWIRE_RTP_JITTER_BUFFER_H */ diff --git a/test/meson.build b/test/meson.build index a54e059b9..6982720dd 100644 --- a/test/meson.build +++ b/test/meson.build @@ -124,6 +124,17 @@ test('test-spa', link_with: pwtest_lib) ) +test('test-module-rtp-common-lib', + executable('test-module-rtp-common-lib', + 'modules/module-rtp/test-jitter-buffer.c', + include_directories : [ + pwtest_inc, + include_directories('../src/modules'), + ], + dependencies: [ spa_dep, pipewire_module_rtp_common_dep ], + link_with: [pwtest_lib]) +) + openal_info = find_program('openal-info', required: false) if openal_info.found() cdata.set_quoted('OPENAL_INFO_PATH', openal_info.full_path()) diff --git a/test/modules/module-rtp/test-jitter-buffer.c b/test/modules/module-rtp/test-jitter-buffer.c new file mode 100644 index 000000000..ae4943643 --- /dev/null +++ b/test/modules/module-rtp/test-jitter-buffer.c @@ -0,0 +1,1259 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2026 Carlos Rafael Giani */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include + +#include "pwtest.h" + +PW_LOG_TOPIC(mod_topic, "test.rtp-jitter-buffer"); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +enum test_event_type { + TEST_EVENT_OUTPUT_PACKET, + TEST_EVENT_LOST_PACKETS, +}; + +struct test_event { + enum test_event_type type; + union { + struct { + uint16_t seqnum; + } output; + struct { + uint16_t first_seqnum; + size_t count; + bool open_ended; + } lost; + }; +}; + +#define MAX_TEST_EVENTS 256 +#define MAX_TEST_PACKET_SIZE 2048 +#define TEST_PACKET_SIZE 128 +#define TEST_HEADER_SIZE 16 +#define TEST_TIMESTAMP 123456 +#define TEST_PACKET_DURATION (10 * SPA_NSEC_PER_MSEC) + +struct test_context { + struct pw_loop *loop; + struct pw_main_loop *main_loop; + struct rtp_jitter_buffer jitter_buffer; + + struct test_event events[MAX_TEST_EVENTS]; + size_t num_events; + + uint8_t packet_bytes[MAX_TEST_PACKET_SIZE]; +}; + +static void send_packet(struct test_context *test_context, uint16_t seqnum) +{ + /* Create a simulated RTP packet. Only write the sequence number + * into its header. The rest (SSRC, CSRC, payload type etc.) are + * of no interest to the jitter buffer - it only cares about the + * sequence number. */ + struct rtp_header *header = (struct rtp_header *)(test_context->packet_bytes); + header->sequence_number = htons(seqnum); + int ret = rtp_jitter_buffer_insert_packet(&(test_context->jitter_buffer), + test_context->packet_bytes, (TEST_PACKET_SIZE), (TEST_HEADER_SIZE), (TEST_TIMESTAMP), seqnum); + assert(ret == 0); +} + +static int test_output_rtp_packet(void *context, const uint8_t *packet_data, size_t packet_size, + size_t header_size, uint32_t timestamp, uint16_t seqnum) +{ + struct test_context *test_context = context; + struct rtp_header *header = (struct rtp_header *)packet_data; + + assert(test_context->num_events < MAX_TEST_EVENTS); + + /* Check that this function is not simply passed + * the value of params.max_packet_size, and that + * the other values (header size, timestamp) + * are correct as well. */ + pwtest_int_eq(packet_size, (size_t)(TEST_PACKET_SIZE)); + pwtest_int_eq(header_size, (size_t)(TEST_HEADER_SIZE)); + pwtest_int_eq(timestamp, (size_t)(TEST_TIMESTAMP)); + + /* Compare the seqnum that is given by the caller + * with the seqnum in the RTP header to verify that + * the packet data is correctly associated with the + * information from the function arguments. */ + pwtest_int_eq(seqnum, ntohs(header->sequence_number)); + + test_context->events[test_context->num_events].type = TEST_EVENT_OUTPUT_PACKET; + test_context->events[test_context->num_events].output.seqnum = seqnum; + pw_log_debug("Output RTP packet with seqnum %" PRIu16, test_context->events[test_context->num_events].output.seqnum); + test_context->num_events++; + + return 0; +} + +static int test_signal_lost_packets(void *context, uint16_t seq_of_first_lost_packet, + size_t num_lost_packets, bool open_ended) +{ + struct test_context *test_context = context; + + assert(test_context->num_events < MAX_TEST_EVENTS); + + test_context->events[test_context->num_events].type = TEST_EVENT_LOST_PACKETS; + test_context->events[test_context->num_events].lost.first_seqnum = seq_of_first_lost_packet; + test_context->events[test_context->num_events].lost.count = num_lost_packets; + test_context->events[test_context->num_events].lost.open_ended = open_ended; + test_context->num_events++; + + return 0; +} + +static void setup_test_context(struct test_context *test_context, size_t num_slots) +{ + struct rtp_jitter_buffer_params params; + + assert(test_context != NULL); + + spa_memzero(test_context, sizeof(struct test_context)); + + pw_init(0, NULL); + + test_context->main_loop = pw_main_loop_new(NULL); + assert(test_context->main_loop != NULL); + test_context->loop = pw_main_loop_get_loop(test_context->main_loop); + + memset(¶ms, 0, sizeof(params)); + params.num_slots = num_slots; + /* Set the maximum packet size to a value higher than TEST_PACKET_SIZE + * to be able to check in test_output_rtp_packet() that that function + * does not simply get the max_packet_size value as the packet size, + * but the _actual_ packet size. (Also see test_output_rtp_packet().) */ + params.max_packet_size = MAX_TEST_PACKET_SIZE; + params.packet_duration = TEST_PACKET_DURATION; + params.loop = test_context->loop; + params.context = test_context; + params.output_rtp_packet = test_output_rtp_packet; + params.signal_lost_packets = test_signal_lost_packets; + + int ret = rtp_jitter_buffer_init(&(test_context->jitter_buffer), ¶ms); + assert(ret == 0); +} + +static void teardown_test_context(struct test_context *test_context) +{ + assert(test_context != NULL); + + rtp_jitter_buffer_shutdown(&(test_context->jitter_buffer)); + if (test_context->main_loop != NULL) + pw_main_loop_destroy(test_context->main_loop); + pw_deinit(); +} + +#define SHIFT_TEST_EVENTS() \ + do { \ + memmove( \ + &(test_context.events[0]), \ + &(test_context.events[1]), \ + (test_context.num_events - 1) * sizeof(struct test_event)); \ + test_context.num_events--; \ + } while (0) + +#define CHECK_LOST_PACKET_EVENT(FIRST_SEQNUM, COUNT, OPEN_ENDED) \ + do { \ + pwtest_int_ge(test_context.num_events, 1u); \ + pwtest_int_eq((int)(test_context.events[0].type), TEST_EVENT_LOST_PACKETS); \ + pwtest_int_eq(test_context.events[0].lost.first_seqnum, (FIRST_SEQNUM)); \ + pwtest_int_eq(test_context.events[0].lost.count, (size_t)(COUNT)); \ + pwtest_int_eq(test_context.events[0].lost.open_ended, (OPEN_ENDED)); \ + SHIFT_TEST_EVENTS(); \ + } while (0) + +#define CHECK_OUTPUT_PACKET_EVENT(SEQNUM) \ + do { \ + pwtest_int_ge(test_context.num_events, 1u); \ + pwtest_int_eq((int)(test_context.events[0].type), TEST_EVENT_OUTPUT_PACKET); \ + pwtest_int_eq(test_context.events[0].output.seqnum, (SEQNUM)); \ + SHIFT_TEST_EVENTS(); \ + } while (0) + +PWTEST(rtp_jitter_buffer_test_consecutive_packets) +{ + /* Simple test with packets that are passed to the jitter buffer + * in order, with no gaps. Immediate output is expected, since + * the jitter buffer will be in regular mode. */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Send packets 100, 101, 102, 103, 104 in order. + * All 5 should be immediately output, and the + * hold-back mode should remain disabled. */ + for (uint16_t i = 0; i < 5; i++) { + uint16_t seqnum = 100 + i; + send_packet(&test_context, seqnum); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + } + + pwtest_int_eq(test_context.num_events, 5u); + for (uint16_t i = 0; i < 5; i++) { + uint16_t seqnum = 100 + i; + CHECK_OUTPUT_PACKET_EVENT(seqnum); + } + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_simple_reordering) +{ + /* Check that simple out-of-order packet arrival is handled properly. + * There should be no gaps signaled, and the packets should be output + * in order. */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Send 100, 101 in order. */ + send_packet(&test_context, 100); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + send_packet(&test_context, 101); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + CHECK_OUTPUT_PACKET_EVENT(100); + CHECK_OUTPUT_PACKET_EVENT(101); + + /* Send 103. A gap at 102 is produced -> jitter buffer enables hold-back mode. + * No output takes place just yet, since 103 is held back. + * The valid seqnum window starts at 102 and ends at packet 103. */ + send_packet(&test_context, 103); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_start_seqnum, 102u); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_length, 2u); + pwtest_int_eq(test_context.num_events, 0u); + + /* Send 102 to simulate out-of-order arrival. This fills the gap + * at 102 (implying that it is not signaled), and should cause + * 102 and 103 to be output (in order) and the hold-back mode + * to be disabled again. */ + send_packet(&test_context, 102); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + CHECK_OUTPUT_PACKET_EVENT(102); + CHECK_OUTPUT_PACKET_EVENT(103); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_partial_output) +{ + /* Test that partial output is done correctly when some + * gaps are filled. (Partial means that only part of the + * held-back packets are output.) */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Establish regular mode with packet 400. */ + send_packet(&test_context, 400); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(400); + + /* Send in packet 402 to produce a gap at 401 and cause the + * jitter buffer to enter hold-back mode. */ + send_packet(&test_context, 402); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_start_seqnum, 401u); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_length, 2u); + pwtest_int_eq(test_context.num_events, 0u); + + /* Send in packets 404 and 405. This keeps the gap at 401, adds + * a gap at 403, and keeps the jitter buffer in hold-back mode. */ + send_packet(&test_context, 404); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + send_packet(&test_context, 405); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 0u); + + /* Send in packet 401, which fills the gap at 401. This allows + * the jitter buffer to output packets 401 and 402. But since + * another gap exists at 403, hold-back mode remains enabled. */ + send_packet(&test_context, 401); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + CHECK_OUTPUT_PACKET_EVENT(401); + CHECK_OUTPUT_PACKET_EVENT(402); + + /* Send in packet 403, which fills the gap at 403. This allows + * the jitter buffer to output packets 403, 404, 405. Those were + * the remaining held-back packets, so hold-back mode should be + * turned off now. */ + send_packet(&test_context, 403); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 3u); + CHECK_OUTPUT_PACKET_EVENT(403); + CHECK_OUTPUT_PACKET_EVENT(404); + CHECK_OUTPUT_PACKET_EVENT(405); + + /* Verify that regular mode is working properly by sending + * in packet 406. */ + send_packet(&test_context, 406); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(406); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_explicit_drain_in_regular_mode) +{ + /* Test what happens when explicitly draining the jitter buffer + * while in regular mode. Draining should be a no-op in this mode. */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Establish regular mode with packets 200 and 201. */ + send_packet(&test_context, 200); + send_packet(&test_context, 201); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + CHECK_OUTPUT_PACKET_EVENT(200); + CHECK_OUTPUT_PACKET_EVENT(201); + + /* Drain, and then check the outcome. Check that it was a no-op. */ + int ret = rtp_jitter_buffer_drain(&(test_context.jitter_buffer)); + assert(ret == 0); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 0u); + pwtest_int_eq(test_context.jitter_buffer.last_seqnum, 201); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_explicit_drain_in_hold_back_mode) +{ + /* Test what happens when explicitly draining the jitter buffer + * while in hold-back mode. Missing packets should be signaled + * as lost packets by this. */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Establish regular mode with packet 200. */ + send_packet(&test_context, 200); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(200); + + /* Send in packets 202 and 205 to produce gap at 201, 203, 204 + * and cause the jitter buffer to enter hold-back mode. */ + send_packet(&test_context, 202); + send_packet(&test_context, 205); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 0u); + + /* Drain explicitly. This should output the following (in this order): + * + * - 1 lost packet, starting at seqnum 201, not open-ended + * - 1 packet output with seqnum 202 + * - 2 lost packets, starting at seqnum 203, not open-ended + * - 1 packet output with seqnum 205 + * + * This should also set the jitter buffer back to regular mode. + * The last_seqnum should be -1, since after explicit drain, + * the jitter buffer has no idea what packets will come next.*/ + int ret = rtp_jitter_buffer_drain(&(test_context.jitter_buffer)); + assert(ret == 0); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 4u); + pwtest_int_eq(test_context.jitter_buffer.last_seqnum, -1); + CHECK_LOST_PACKET_EVENT(201, 1u, false); + CHECK_OUTPUT_PACKET_EVENT(202); + CHECK_LOST_PACKET_EVENT(203, 2u, false); + CHECK_OUTPUT_PACKET_EVENT(205); + + /* Verify that regular mode is working properly by sending + * in packet 700. Since after draining, the last_seqnum is + * -1, a discontinuity in the sequence numbers is okay. */ + send_packet(&test_context, 700); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(700); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_explicit_drain_coalesced_loss) +{ + /* Test that a contiguous set of lost packets is coalesced + * into one signal lost packet signal. */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Establish regular mode with packet 50. */ + send_packet(&test_context, 50); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(50); + + /* Send in packet 54 to produce gap at 51, 52, 53 and + * cause the jitter buffer to enter hold-back mode. */ + send_packet(&test_context, 54); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 0u); + + /* Drain the jitter buffer. The packets 51, 52, 53 are + * now considered lost, and should be reported as such. */ + int ret = rtp_jitter_buffer_drain(&(test_context.jitter_buffer)); + assert(ret == 0); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + pwtest_int_eq(test_context.jitter_buffer.last_seqnum, -1); + CHECK_LOST_PACKET_EVENT(51, 3u, false); + CHECK_OUTPUT_PACKET_EVENT(54); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_explicit_drain_with_seqnum_wraparound) +{ + /* Test what happens when explicitly draining the jitter + * buffer while in hold-back mode and with sequence numbers + * wrapping around. Missing packets should be signaled as + * lost packets by this. */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Establish regular mode with packet 65533. */ + send_packet(&test_context, 65533); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(65533); + + /* Send in packets 65535 and 2 to produce gap at 65534, 0, 1 + * and cause the jitter buffer to enter hold-back mode. */ + send_packet(&test_context, 65535); + send_packet(&test_context, 2); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 0u); + + /* Drain explicitly. This should output the following (in this order): + * + * - 1 lost packet, starting at seqnum 65534, not open-ended + * - 1 packet output with seqnum 65535 + * - 2 lost packets, starting at seqnum 0, not open-ended + * - 1 packet output with seqnum 2 + * + * This should also set the jitter buffer back to regular mode. + * The last_seqnum should be -1, since after explicit drain, + * the jitter buffer has no idea what packets will come next.*/ + int ret = rtp_jitter_buffer_drain(&(test_context.jitter_buffer)); + assert(ret == 0); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 4u); + pwtest_int_eq(test_context.jitter_buffer.last_seqnum, -1); + CHECK_LOST_PACKET_EVENT(65534, 1u, false); + CHECK_OUTPUT_PACKET_EVENT(65535); + CHECK_LOST_PACKET_EVENT(0, 2u, false); + CHECK_OUTPUT_PACKET_EVENT(2); + + /* Verify that regular mode is working properly by sending + * in packet 700. Since after draining, the last_seqnum is + * -1, a discontinuity in the sequence numbers is okay. */ + send_packet(&test_context, 700); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(700); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_stale_packets_in_regular_mode) +{ + /* Test what happens when stale and old packets are sent into + * the jitter buffer in regular mode. They should be dropped + * without influencing the behavior of the jitter buffer. */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Establish regular mode with packets 100 and 101. */ + send_packet(&test_context, 100); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + send_packet(&test_context, 101); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + CHECK_OUTPUT_PACKET_EVENT(100); + CHECK_OUTPUT_PACKET_EVENT(101); + + /* Send in packet 101. Since a packet 101 was already seen, + * this is a stale packet, and needs to be dropped. */ + send_packet(&test_context, 101); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 0u); + + /* Send in packet 99. Since packets 100 and 101 were already seen, + * this is an old packet, and needs to be dropped. */ + send_packet(&test_context, 99); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 0u); + + /* Verify that regular mode is working properly by sending + * in packet 102. */ + send_packet(&test_context, 102); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(102); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_stale_packets_in_hold_back_mode) +{ + /* Test what happens when stale and old packets are sent into + * the jitter buffer in hold-back mode. They should be dropped + * without influencing the behavior of the jitter buffer. */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Establish hold-back mode with packets 300 and 302. + * Hold-back mode gets active because of the gap at 301. */ + send_packet(&test_context, 300); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + send_packet(&test_context, 302); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(300); + + /* Send in packet 299. Since packets 300 and 302 were already seen, + * this is an old packet, and needs to be dropped. */ + send_packet(&test_context, 299); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 0u); + + /* Send in packet 300. Since a packet 300 was already seen, + * this is a stale packet, and needs to be dropped. */ + send_packet(&test_context, 300); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 0u); + + /* Send in packet 302. This is another stale packet. The + * difference to the packet 300 check above is that the + * packet 302 that was previously observed is held back, + * and was not output thus far. */ + send_packet(&test_context, 302); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 0u); + + /* Send in packet 301 to test that switching back + * to regular mode still works properly. */ + send_packet(&test_context, 301); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + CHECK_OUTPUT_PACKET_EVENT(301); + CHECK_OUTPUT_PACKET_EVENT(302); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_flush) +{ + /* Test the flush functionality. This should discard any held-back + * packets, without emitting them, and the jitter buffer should + * be back in regular mode afterwards. */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Establish hold-back mode with packets 500 and 502. + * Hold-back mode gets active because of the gap at 501. */ + send_packet(&test_context, 500); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + send_packet(&test_context, 502); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(500); + + rtp_jitter_buffer_flush(&(test_context.jitter_buffer)); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 0u); + pwtest_int_eq(test_context.jitter_buffer.last_seqnum, -1); + + /* Verify that regular mode is working properly by sending + * in packet 700. Since after flushing, the last_seqnum is + * -1, a discontinuity in the sequence numbers is okay. */ + send_packet(&test_context, 700); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(700); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_seqnum_wraparound_regular) +{ + /* Check that in regular mode, output of in-sequence packets + * works properly even when a sequence number wrap-around occurs. */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + send_packet(&test_context, 65534); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + send_packet(&test_context, 65535); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + send_packet(&test_context, 0); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + send_packet(&test_context, 1); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 4u); + CHECK_OUTPUT_PACKET_EVENT(65534); + CHECK_OUTPUT_PACKET_EVENT(65535); + CHECK_OUTPUT_PACKET_EVENT(0); + CHECK_OUTPUT_PACKET_EVENT(1); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_seqnum_wraparound_with_reordering) +{ + /* Check that in hold-back mode, output of in-sequence packets + * works properly even when a sequence number wrap-around occurs. */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Send packets 65534 and 65535 in order. */ + send_packet(&test_context, 65534); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + send_packet(&test_context, 65535); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + CHECK_OUTPUT_PACKET_EVENT(65534); + CHECK_OUTPUT_PACKET_EVENT(65535); + + /* Send in packet 1, causing a gap at 0. */ + send_packet(&test_context, 1); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 0u); + + /* Fill the gap by sending in packet 0, then check that + * packets 0 and 1 were now output in order. */ + send_packet(&test_context, 0); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + CHECK_OUTPUT_PACKET_EVENT(0); + CHECK_OUTPUT_PACKET_EVENT(1); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_overextension_single_gap_no_end_gap) +{ + /* Check what happens when hold-back mode is active, the + * valid seqnum window's maximum length is reached, and then, + * a packet with a sequence number that is one past the window + * range is added. This new packet would overextend the window, + * so the window is shifted forwards. However, it is only + * overextended by 1, so only the oldest slot in the window + * needs to be drained. In this case, that oldest slot contains + * the gap at the very beginning of the window. Also, since + * aside from that gap, there are no other ones, and the new + * packet (the one that overextends the window) comes directly + * after the last packet in the valid seqnum window, the + * jitter buffer will have no gaps left to take care of, so + * all held back packets can be output. + * + * This simulates cases where one packet is lost among + * a string of packets that all arrive in order. */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Produce a sequence of packets with a gap in them. Start at 100, + * skip 101, then go all the way to 110. + * + * First, packet 100 will immediately be output. Then, packet 102 + * will enable hold-back mode (due to the gap at 101). The valid + * seqnum window then starts at 101, and extends all the way to 110. + * 110-101+1 = 10, which equals the max num packets of the jitter + * buffer here. In other words, after this, the jitter buffer valid + * range is as large as it can maximally be. */ + send_packet(&test_context, 100); + for (uint16_t i = 102; i <= 110; i++) { + send_packet(&test_context, i); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + } + pwtest_int_eq(test_context.num_events, 1u); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_length, 10u); + CHECK_OUTPUT_PACKET_EVENT(100); + + /* Now insert packet 111. This would overextend the window, so the + * jitter buffer has to shift the window and drain the oldest slots + * that are no longer part of the shifted window. Since packet 111 + * would overextend the window by 1, it means that the one oldest + * slot is drained. That oldest slot actually is the gap at 101. + * Since that gap was drained (resulting in a packet loss signal + * at seqnum 101 of length 1), only packets remain in the valid + * seqnum window, no gaps anymore, so the jitter buffer immediately + * outputs all of them, in order. */ + send_packet(&test_context, 111); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 11u); + CHECK_LOST_PACKET_EVENT(101, 1u, false); + for (uint16_t i = 102; i <= 111; i++) + CHECK_OUTPUT_PACKET_EVENT(i); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_overextension_multiple_gaps_no_end_gap) +{ + /* Check what happens when hold-back mode is active, the + * valid seqnum window's maximum length is reached, and then, + * a packet with a sequence number that is one past the window + * range is added. This new packet would overextend the window, + * so the window is shifted forwards. However, it is only + * overextended by 1, so only the oldest slot in the window + * needs to be drained. In this case, that oldest slot contains + * the gap at the very beginning of the window. Since there + * are more gaps present, the hold-back mode is not left. + * + * This simulates cases where more than one packet is lost + * among a string of packets that all arrive in order. */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Produce a sequence of packets with a gap in them. Start at 100, + * skip 101 and 102, and go all the way to 110. + * + * In the hold-back mode that results from this, the valid range + * then starts at 101, and extends all the way to 110. 110-101+1 = 10, + * which equals the max num packets of the jitter buffer here. In + * other words, after this, the jitter buffer valid range is as large + * as it can maximally be. */ + send_packet(&test_context, 100); + send_packet(&test_context, 103); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + for (uint16_t i = 105; i <= 110; i++) { + send_packet(&test_context, i); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + } + pwtest_int_eq(test_context.num_events, 1u); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_length, 10u); + CHECK_OUTPUT_PACKET_EVENT(100); + + /* Now insert packet 111. This would overextend the window, so the + * jitter buffer has to shift the window and drain the oldest slots + * that are no longer part of the shifted window. Since packet 111 + * would overextend the window by 1, it means that the one oldest + * slot is drained. But, at 102, there is also gap, and 102 is now + * the new start of the valid seqnum window, so the jitter buffer + * cannot output any packets yet. */ + send_packet(&test_context, 111); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_start_seqnum, 102u); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_length, 10u); + CHECK_LOST_PACKET_EVENT(101, 1u, false); + + /* To see that the behavior remains as expected, fill the gap at 102. + * Since 102 is the very beginning of the valid seqnum window, and there + * is a packet at 103, the jitter buffer can now output 102 and 103. + * Also, the valid seqnum window shrinks accordingly by 2, its length + * becoming 8 and its start seqnum becoming 104. */ + send_packet(&test_context, 102); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_start_seqnum, 104u); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_length, 8u); + CHECK_OUTPUT_PACKET_EVENT(102); + CHECK_OUTPUT_PACKET_EVENT(103); + + /* Finally, send in packet 104. By now, 104 is the start of the valid + * packet window, and a gap is there. Since this is the last gap in + * the jitter buffer, once it is filled, all packets can be output. */ + send_packet(&test_context, 104); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 8u); + for (uint16_t i = 104; i <= 111; i++) + CHECK_OUTPUT_PACKET_EVENT(i); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_overextension_after_partial_output) +{ + /* Check what happens when first, in hold-back mode, a partial + * drain happens, and then, the valid seqnum window is overextended. */ + + struct test_context test_context; + + setup_test_context(&test_context, 5); + + /* Add a packet 100, which is output immediately, since the + * jitter buffer is in regular mode. */ + send_packet(&test_context, 100); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(100); + + /* Now add packet 102. Since there is a gap at 101, hold-back + * mode is enabled. The valid seqnum window starts at 101, + * and is of length 2. */ + send_packet(&test_context, 102); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_start_seqnum, 101u); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_length, 2u); + pwtest_int_eq(test_context.num_events, 0u); + + /* Packets 103 to 105 are inserted. This fills the window to + * capacity, since now, it has been extended, and goes from + * 101 to 105. That is, it starts at 101, and is of length 5 + * which equals the jitter buffer capacity). */ + send_packet(&test_context, 103); + send_packet(&test_context, 104); + send_packet(&test_context, 105); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_start_seqnum, 101u); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_length, 5u); + pwtest_int_eq(test_context.num_events, 0u); + + /* Now add packet 101. This fills the gap. All 5 packets + * can be output, and the jitter buffer returns to the regular mode. */ + send_packet(&test_context, 101); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 5u); + CHECK_OUTPUT_PACKET_EVENT(101); + CHECK_OUTPUT_PACKET_EVENT(102); + CHECK_OUTPUT_PACKET_EVENT(103); + CHECK_OUTPUT_PACKET_EVENT(104); + CHECK_OUTPUT_PACKET_EVENT(105); + + /* Re-enter the hold-back mode by adding packet 107 and + * intentionally leaving out packet 106. The valid seqnum + * window now starts at 106, and is of length 2. */ + send_packet(&test_context, 107); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_start_seqnum, 106u); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_length, 2u); + pwtest_int_eq(test_context.num_events, 0u); + + /* Packets 108 to 110 are inserted. This fills the window to + * capacity, since now, it has been extended, and goes from + * 106 to 110. That is, it starts at 106, and is of length 5 + * which equals the jitter buffer capacity). */ + send_packet(&test_context, 108); + send_packet(&test_context, 109); + send_packet(&test_context, 110); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_start_seqnum, 106u); + pwtest_int_eq(test_context.jitter_buffer.valid_seqnum_window_length, 5u); + pwtest_int_eq(test_context.num_events, 0u); + + /* Packet 111 is added. This overextends the window, since it would + * now go from 106 to 111. That is a length of 111-106+1 = 6, which + * is beyond the capacity (5). + * + * The overextension is still low enough that most of the window + * contents can be reused. In fact, only the oldest slot (the one + * containing the gap at 106) needs to be drained by signaling it + * as a packet 106 loss. + * + * Once packet 106 is signaled as lost, and the corresponding slot + * is drained, the leftovers are all packets, no gaps, so all packets + * from 107 to 111 are output. + * + * By combining this with multiple partial drains above, it is verified + * that valid_seqnum_window_start_seqnum updates (which happen during + * partial drains) do not break the overextension handling. */ + send_packet(&test_context, 111); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 6u); + CHECK_LOST_PACKET_EVENT(106, 1u, false); + CHECK_OUTPUT_PACKET_EVENT(107); + CHECK_OUTPUT_PACKET_EVENT(108); + CHECK_OUTPUT_PACKET_EVENT(109); + CHECK_OUTPUT_PACKET_EVENT(110); + CHECK_OUTPUT_PACKET_EVENT(111); + + /* Verify regular mode recovery. */ + send_packet(&test_context, 112); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(112); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_immediate_overextension_after_regular_mode) +{ + /* Check what happens when a gap causes the jitter buffer to switch + * to the hold-back mode, but that gap is so large that it immediately + * overextends the valid seqnum window. The jitter buffer should + * instantly recognize the immediate overextension aqnd signal an open + * ended packet loss event. It does not stay in the hold-back mode, + * since there is nothing to hold back in that case. */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Send 100, 101 in order. */ + send_packet(&test_context, 100); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + send_packet(&test_context, 101); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + CHECK_OUTPUT_PACKET_EVENT(100); + CHECK_OUTPUT_PACKET_EVENT(101); + + /* Send 200. A massive gap of far more than 10 packets is produced + * -> jitter buffer signals an open ended gap, but stays in regular mode. */ + send_packet(&test_context, 200); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + CHECK_LOST_PACKET_EVENT(102, 10u, true); + CHECK_OUTPUT_PACKET_EVENT(200); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_immediate_overextension_after_regular_mode_threshold_open_closed_gap) +{ + /* This is similar to rtp_jitter_buffer_test_immediate_overextension_after_regular_mode, + * but checks for a corner case. That is: If the gap length equals + * the number of slots, then the gap should not be reported as open. + * + * Test this by producing such a gap. Then further verify by repeating + * the test, but by a gap that is 1 packet larger than the number of + * slots. The first round should report a closed gap of a size equal + * to the number of slot. The second round should report an open gap. */ + + /* First round. */ + { + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Send 10, 11 in order. */ + send_packet(&test_context, 10); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + send_packet(&test_context, 11); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + CHECK_OUTPUT_PACKET_EVENT(10); + CHECK_OUTPUT_PACKET_EVENT(11); + + /* Send 22. A gap of exactly 10 packets (= the number of slots) + * is produced -> jitter buffer signals a closed gap of size + * equal to the number of slots. */ + send_packet(&test_context, 22); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + CHECK_LOST_PACKET_EVENT(12, 10u, false); + CHECK_OUTPUT_PACKET_EVENT(22); + + teardown_test_context(&test_context); + } + + /* Second round. */ + { + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Send 10, 11 in order. */ + send_packet(&test_context, 10); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + send_packet(&test_context, 11); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + CHECK_OUTPUT_PACKET_EVENT(10); + CHECK_OUTPUT_PACKET_EVENT(11); + + /* Send 23. A gap of exactly 11 packets (= 1 past the number + * of slots) is produced -> jitter buffer signals an open + * ended gap of size equal to the number of slots. */ + send_packet(&test_context, 23); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + CHECK_LOST_PACKET_EVENT(12, 10u, true); + CHECK_OUTPUT_PACKET_EVENT(23); + + teardown_test_context(&test_context); + } + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_full_window_invalidation_non_open_ended_gap) +{ + /* Check what happens when hold-back mode is active, the + * valid seqnum window's maximum length is reached, and then, + * a packet with a sequence number that is far enough to + * overextend the window past its current length. This means + * that the shifting method (verified in earlier tests above) + * won't work - the window is shifted completely past its + * current range, so none of those slots remain valid, + * and must all be drained. Furthermore, it means that between + * the last seqnum of the old window and the first seqnum of + * the new window, there is a gap. The jitter buffer is expected + * to do the following: + * + * 1. Drain the entire current valid seqnum window + * 2. Reset the window to only contain the seqnum of the new packet + * 3. Signal the gap between the old and the new window + * + * Here, the window is shifted far enough that none of the + * original content can be retained, but not so far that + * the gap between the old and new windows becomes too large + * to fully cover via PLC. As a result, that gap is signaled + * as packet loss, but as a non-open-ended one. */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Establish hold-back mode with packets 10 and 12. + * Hold-back mode gets active because of the gap at 11. */ + send_packet(&test_context, 10); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + send_packet(&test_context, 12); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(10); + + /* Send in packet 22. This would overextend the window. Shifting + * the current window moves it past packet 12, so the jitter + * buffer must be fully drained. Since afterwards, there is + * nothing left in the jitter buffer other than the new packet, + * the valid seqnum window length becomes 1, and starts at 22. + * This means that there are no gaps left, so the contents + * (in this case, just the packet 22) can be output immediately. + * Also, the gap between the old window and the new window goes + * from seqnum 13 (one past the end of the old window) to seqnum + * 21 (one before the new packet 22). 21-13+1 = 9, which is + * less than the jitter buffer capacity (which is 10), so that + * gap is announced as non-open-ended packet loss. */ + send_packet(&test_context, 22); + pwtest_int_eq(test_context.num_events, 4u); + CHECK_LOST_PACKET_EVENT(11, 1u, false); + CHECK_OUTPUT_PACKET_EVENT(12); + CHECK_LOST_PACKET_EVENT(13, 9u, false); + CHECK_OUTPUT_PACKET_EVENT(22); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_full_window_invalidation_open_ended_gap) +{ + /* Check what happens when hold-back mode is active, the + * valid seqnum window's maximum length is reached, and then, + * a packet with a sequence number that is far enough to + * overextend the window past its current length. This means + * that the shifting method (verified in earlier tests above) + * won't work - the window is shifted completely past its + * current range, so none of those slots remain valid, + * and must all be drained. Furthermore, it means that between + * the last seqnum of the old window and the first seqnum of + * the new window, there is a gap. The jitter buffer is expected + * to do the following: + * + * 1. Drain the entire current valid seqnum window + * 2. Reset the window to only contain the seqnum of the new packet + * 3. Signal the gap between the old and the new window + * + * Here, the window is shifted far enough that none of the + * original content can be retained, and that that the gap + * between the old and new windows becomes too large + * to fully cover via PLC. As a result, that gap is signaled + * as packet loss, but as an open-ended one. */ + + struct test_context test_context; + + setup_test_context(&test_context, 10); + + /* Establish hold-back mode with packets 10 and 12. + * Hold-back mode gets active because of the gap at 11. */ + send_packet(&test_context, 10); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + send_packet(&test_context, 12); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(10); + + /* Send in packet 400. This would overextend the window. Shifting + * the current window moves it past packet 12, so the jitter + * buffer must be fully drained. Since afterwards, there is + * nothing left in the jitter buffer other than the new packet, + * the valid seqnum window length becomes 1, and starts at 400. + * This means that there are no gaps left, so the contents + * (in this case, just the packet 400) can be output immediately. + * Also, the gap between the old window and the new window goes + * from seqnum 13 (one past the end of the old window) to seqnum + * 399 (one before the new packet 400). 399-13+1 = 387, which is + * far beyond the jitter buffer capacity (which is 10). That gap + * is then signaled as an open ended packet loss with maximum + * length 10, meaning that any PLC/fadeout measure must not + * exceed the length of 10 packets. (In non-open-ended signals, + * the length instead specifies the exact length of the gap.) + * This is done to avoid excessive PLC/fadeout calculations, + * like in this case, where it otherwise would force PLC for + * 387 packets. Callers are encouraged to apply fadeout as well + * to not have a hard cutoff after the maximum (10 packets here).*/ + send_packet(&test_context, 400); + pwtest_int_eq(test_context.num_events, 4u); + CHECK_LOST_PACKET_EVENT(11, 1u, false); + CHECK_OUTPUT_PACKET_EVENT(12); + CHECK_LOST_PACKET_EVENT(13, 10u, true); + CHECK_OUTPUT_PACKET_EVENT(400); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST(rtp_jitter_buffer_test_timeout_drain) +{ + /* Check what happens when hold-back mode is enabled and + * the gaps are not filled in time. It is expected that the + * jitter buffer's timeout expires and forcibly drains + * its contents. */ + + struct test_context test_context; + struct timespec ts; + + setup_test_context(&test_context, 10); + + /* Establish hold-back mode with packets 60 and 62. + * Hold-back mode gets active because of the gap at 61. */ + send_packet(&test_context, 60); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + send_packet(&test_context, 62); + pwtest_bool_true(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(60); + + /* The jitter buffer's timeout timer is configured to expire + * when the total duration of its capacity passes after the + * hold-back mode was enabled. In this test, capacity is 10 + * packets, and each packet covers 10ms, then the total duration + * is 10*10ms = 100 ms, and that will also be the timeout of + * that timer, and the gap that was detected earlier will have + * armed that timer. Sleep for 50ms longer than its timeout + * duration to make sure it expires and thus provokes the + * draining of the jitter buffer. */ + ts.tv_sec = 0; + ts.tv_nsec = 10 * TEST_PACKET_DURATION + 50 * SPA_NSEC_PER_MSEC; + nanosleep(&ts, NULL); + + /* Iterate the loop to process the timer expiration. */ + pw_loop_enter(test_context.loop); + pw_loop_iterate(test_context.loop, 0); + pw_loop_leave(test_context.loop); + + /* After draining, the jitter buffer should be back to regular + * mode, just as if rtp_jitter_buffer_drain() had been called. */ + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 2u); + pwtest_int_eq(test_context.jitter_buffer.last_seqnum, -1); + CHECK_LOST_PACKET_EVENT(61, 1u, false); + CHECK_OUTPUT_PACKET_EVENT(62); + + /* Verify that regular mode is working properly by sending + * in packet 700. Since after draining, the last_seqnum is + * -1, a discontinuity in the sequence numbers is okay. */ + send_packet(&test_context, 700); + pwtest_bool_false(test_context.jitter_buffer.hold_back_mode); + pwtest_int_eq(test_context.num_events, 1u); + CHECK_OUTPUT_PACKET_EVENT(700); + + teardown_test_context(&test_context); + + return PWTEST_PASS; +} + +PWTEST_SUITE(pw_module_rtp_common_lib) +{ + pwtest_add(rtp_jitter_buffer_test_consecutive_packets, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_simple_reordering, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_partial_output, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_explicit_drain_in_regular_mode, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_explicit_drain_in_hold_back_mode, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_explicit_drain_coalesced_loss, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_explicit_drain_with_seqnum_wraparound, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_stale_packets_in_regular_mode, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_stale_packets_in_hold_back_mode, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_flush, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_seqnum_wraparound_regular, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_seqnum_wraparound_with_reordering, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_overextension_single_gap_no_end_gap, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_overextension_multiple_gaps_no_end_gap, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_overextension_after_partial_output, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_immediate_overextension_after_regular_mode, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_full_window_invalidation_non_open_ended_gap, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_immediate_overextension_after_regular_mode_threshold_open_closed_gap, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_full_window_invalidation_open_ended_gap, PWTEST_NOARG); + pwtest_add(rtp_jitter_buffer_test_timeout_drain, PWTEST_NOARG); + + return PWTEST_PASS; +}