PR #23640 opened by Torbjörn Einarsson (tobbee)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23640
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23640.patch

# Summary of changes

When receiving RTP with multiple MPEG-2 TS packets in the payload, some TS 
packets were
silently dropped whenever an RTP packet carried a PES boundary
(a PUSI (Payload Unit Start Indicator) on a TS packet other than the first in 
the payload)
and that packet was delivered out of the RTP reordering queue.
In practice this happened for a simple packet reordering.

## Fix

Update s->prev_ret inside the loop so that the first packets after a PUSI are 
not dropped.

## A New Test

To reproduce the issue, there is a standalone test program,  
`libavformat/tests/rtpdec.c`, 
that may be dropped if deemed unnecessary. It drives the real MPEG-2 TS
depacketizer with full-size RTP packets (12-byte header + 7x188-byte TS 
packets) by driving
the same packets twice, once in order and once with one adjacent pair 
reordered, without
dropping any packet. A correct depacketizer delivers identical PES data both 
ways; before
the fix the reordered run is short by the dropped tail.

Build and run:

    make libavformat/tests/rtpdec && ./libavformat/tests/rtpdec



From d4bf590cc018af2a04e00d6695ba83ad58d33a09 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torbjo=CC=88rn=20Einarsson?= <[email protected]>
Date: Mon, 29 Jun 2026 14:23:50 +0200
Subject: [PATCH] avformat/rtpdec: fix dropped payload after reordering-queue
 delivery
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

ff_rtp_parse_packet() drains a depacketizer's internally buffered data on
the follow-up buf==NULL call only while s->prev_ret > 0. The
"while (rv < 0 && has_next_packet(s))" reordering loop delivered queued
packets without updating s->prev_ret, so when a packet was returned from that
loop, prev_ret kept its stale negative value and the next buf==NULL call was
routed into the (now empty) reordering queue instead of back into the
depacketizer.

For MPEG-2 TS this drops data: when an RTP payload carries a payload unit
start indicator (PUSI) on a TS packet other than the first, the depacketizer
returns the finished PES, buffers the rest of the payload and returns 1,
expecting a buf==NULL call to fetch the remainder. A plain continuation
payload (no PUSI, the common case) completes no PES and returns < 0, which is
enough to enter the loop above; if the next queued packet carries a
mid-payload PUSI, its buffered tail (the TS packets from the PUSI onwards) is
never drained and is lost. A single reordered pair of packets triggers this,
so the loss occurs rather frequently, not only under heavy reordering.

Update s->prev_ret inside the loop and add libavformat/tests/rtpdec.c, which
drives the real MPEG-TS depacketizer with full 7x188-byte RTP packets and
checks that an in-order and a reordered delivery of the same packets yield
identical PES data.

Signed-off-by: Torbjörn Einarsson <[email protected]>
---
 libavformat/Makefile         |   1 +
 libavformat/rtpdec.c         |   4 +-
 libavformat/tests/.gitignore |   1 +
 libavformat/tests/rtpdec.c   | 277 +++++++++++++++++++++++++++++++++++
 4 files changed, 282 insertions(+), 1 deletion(-)
 create mode 100644 libavformat/tests/rtpdec.c

diff --git a/libavformat/Makefile b/libavformat/Makefile
index 752436cf5f..cd1ca0fbfc 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -781,6 +781,7 @@ TESTPROGS = id3v2                                           
            \
             seek                                                        \
             url                                                         \
             seek_utils
+TESTPROGS-$(CONFIG_RTPDEC)               += rtpdec
 #           async                                                       \
 
 FIFO-MUXER-TESTPROGS-$(CONFIG_NETWORK)   += fifo_muxer
diff --git a/libavformat/rtpdec.c b/libavformat/rtpdec.c
index 3fd817f93c..47ecb46d91 100644
--- a/libavformat/rtpdec.c
+++ b/libavformat/rtpdec.c
@@ -930,8 +930,10 @@ int ff_rtp_parse_packet(RTPDemuxContext *s, AVPacket *pkt,
         return -1;
     rv = rtp_parse_one_packet(s, pkt, bufptr, len);
     s->prev_ret = rv;
-    while (rv < 0 && has_next_packet(s))
+    while (rv < 0 && has_next_packet(s)) {
         rv = rtp_parse_queued_packet(s, pkt);
+        s->prev_ret = rv;
+    }
     return rv ? rv : has_next_packet(s);
 }
 
diff --git a/libavformat/tests/.gitignore b/libavformat/tests/.gitignore
index f9aac1f9e0..b7a4c70014 100644
--- a/libavformat/tests/.gitignore
+++ b/libavformat/tests/.gitignore
@@ -4,6 +4,7 @@
 /movenc
 /noproxy
 /rtmpdh
+/rtpdec
 /seek
 /srtp
 /url
diff --git a/libavformat/tests/rtpdec.c b/libavformat/tests/rtpdec.c
new file mode 100644
index 0000000000..1480337bfe
--- /dev/null
+++ b/libavformat/tests/rtpdec.c
@@ -0,0 +1,277 @@
+/*
+ * Copyright (c) 2026 Torbjörn Einarsson
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/*
+ * Regression test for ff_rtp_parse_packet() and the RTP reordering queue,
+ * exercised through the real MPEG-2 TS depacketizer (rtpdec_mpegts.c) with
+ * realistic full-size RTP packets (12-byte RTP header + 7 * 188-byte TS
+ * packets, i.e. the common 1328-byte MPEG-TS-over-RTP packet).
+ *
+ * The MPEG-TS depacketizer consumes an RTP payload only up to the point where
+ * a payload unit start indicator (PUSI) closes the current PES. If that PUSI
+ * sits on a TS packet other than the first in the RTP payload, the 
depacketizer
+ * returns the finished PES, buffers the rest of the payload internally and
+ * returns 1, expecting the caller to fetch the rest with a follow-up
+ * buf == NULL call. A plain continuation RTP payload (no PUSI, the common case
+ * in a video stream) completes no PES and makes the depacketizer return < 0.
+ *
+ * ff_rtp_parse_packet() routes the follow-up buf == NULL call back into the
+ * depacketizer (to drain its buffer) only while s->prev_ret > 0. When a packet
+ * is delivered from the "while (rv < 0 && has_next_packet(s))" reordering 
loop,
+ * prev_ret used not to be updated, so after such a delivery the buffered tail
+ * (the TS packets from the mid-payload PUSI onwards) was never drained and got
+ * dropped.
+ *
+ * This needs nothing more exotic than a single reordered pair of packets: a
+ * continuation packet (returns < 0) parsed while a mid-PUSI packet (returns 1)
+ * is waiting in the queue is enough to enter that loop -- which is why the 
loss
+ * was observed very frequently, not only on heavy reordering.
+ *
+ * The test feeds the exact same packets twice -- once in order (the reference;
+ * the in-order path never enters the buggy loop) and once with one adjacent
+ * pair swapped -- and checks that both deliver identical PES data. No packet 
is
+ * ever lost, so a correct depacketizer must recover everything.
+ */
+
+#include <stdio.h>
+#include <string.h>
+
+#include "libavutil/mem.h"
+#include "libavutil/intreadwrite.h"
+#include "libavformat/avformat.h"
+#include "libavformat/rtpdec.h"
+#include "libavformat/rtpdec_formats.h"
+
+#define TS_SIZE          188
+#define TS_PAYLOAD       184            /* TS_SIZE - 4-byte TS header          
 */
+#define TS_PER_RTP       7              /* 12 + 7*188 = 1328-byte RTP packets  
 */
+#define RTP_HEADER_SIZE  12
+#define RTP_PAYLOAD      (TS_PER_RTP * TS_SIZE)
+#define MP2T_PT          33             /* static RTP payload type for MPEG-2 
TS*/
+#define QUEUE_SIZE       2              /* reordering enabled, small jitter 
buf */
+
+#define ES_PID           0x0100
+#define PES_STREAM_ID    0xe0           /* a video stream id                   
 */
+#define PES_HDR_SIZE     9              /* 00 00 01 id len(2) flags(2) hdrlen  
  */
+
+/* PES payload sizes that map onto a whole number of TS packets:
+ * first packet carries PES_HDR_SIZE + (TS_PAYLOAD - PES_HDR_SIZE) ES bytes, 
the
+ * rest carry TS_PAYLOAD each. 17 TS packets -> 175 + 16*184 ES bytes. */
+#define PES_TS_PACKETS   17
+#define PES_ES_LEN       ((TS_PAYLOAD - PES_HDR_SIZE) + (PES_TS_PACKETS - 1) * 
TS_PAYLOAD)
+
+/* PES boundary layout (one PES per access unit), 35 TS packets = 5 RTP 
packets:
+ *   PES_0 : TS 0..16   (PUSI on TS 0)
+ *   PES_1 : TS 17..33  (PUSI on TS 17, mid RTP packet 2)
+ *   PES_2 : TS 34      (PUSI on TS 34, closes PES_1; PES_2 left pending)
+ * => RTP 1 (TS 7..13) is pure continuation, RTP 2 (TS 14..20) carries the
+ *    mid-payload PUSI. Swapping their arrival order triggers the bug. */
+#define NB_TS_PACKETS    35
+#define NB_RTP_PACKETS   (NB_TS_PACKETS / TS_PER_RTP)   /* 5 */
+
+/* Start sequence numbers away from 0: ff_rtp_parse_packet() treats s->seq == 0
+ * as "no packet seen yet" and bypasses the reordering queue for it. */
+#define SEQ_BASE         100
+
+typedef struct Output {
+    int       sizes[64];
+    int       nb;
+    uint8_t   bytes[1 << 16];
+    int       total;
+} Output;
+
+/* ------------------------------------------------------------------ */
+/* MPEG-TS stream construction                                        */
+/* ------------------------------------------------------------------ */
+
+static void put_ts_packet(uint8_t *dst, int pusi, int cc,
+                          const uint8_t *payload)
+{
+    dst[0] = 0x47;                                   /* sync byte            */
+    dst[1] = (pusi ? 0x40 : 0x00) | ((ES_PID >> 8) & 0x1f);
+    dst[2] = ES_PID & 0xff;
+    dst[3] = 0x10 | (cc & 0x0f);                     /* payload only, no AF  */
+    memcpy(dst + 4, payload, TS_PAYLOAD);
+}
+
+/* Append one PES packet (header + es_len ES bytes) as TS packets. */
+static void add_pes(uint8_t *ts, int *off, int *cc, int *es_counter, int 
es_len)
+{
+    static const uint8_t pes_hdr[PES_HDR_SIZE] = {
+        0x00, 0x00, 0x01, PES_STREAM_ID,
+        0x00, 0x00,             /* PES_packet_length 0 == unbounded           
*/
+        0x80, 0x00, 0x00,       /* '10' marker, no PTS/DTS, header_data_len 0 
*/
+    };
+    int es_done = 0, first = 1;
+
+    while (es_done < es_len) {
+        uint8_t payload[TS_PAYLOAD];
+        int p = 0, i, chunk;
+
+        if (first) {
+            memcpy(payload, pes_hdr, PES_HDR_SIZE);
+            p = PES_HDR_SIZE;
+        }
+        chunk = TS_PAYLOAD - p;
+        for (i = 0; i < chunk; i++)
+            payload[p + i] = (uint8_t)((*es_counter)++ & 0xff);
+
+        put_ts_packet(ts + *off, first, *cc, payload);
+        *off += TS_SIZE;
+        *cc   = (*cc + 1) & 0x0f;
+        es_done += chunk;
+        first = 0;
+    }
+}
+
+static void build_ts_stream(uint8_t *ts)
+{
+    int off = 0, cc = 0, es_counter = 0;
+    add_pes(ts, &off, &cc, &es_counter, PES_ES_LEN);   /* PES_0: TS 0..16  */
+    add_pes(ts, &off, &cc, &es_counter, PES_ES_LEN);   /* PES_1: TS 17..33 */
+    add_pes(ts, &off, &cc, &es_counter, TS_PAYLOAD - PES_HDR_SIZE); /* PES_2: 
TS 34 */
+}
+
+/* ------------------------------------------------------------------ */
+/* RTP feeding                                                        */
+/* ------------------------------------------------------------------ */
+
+static void collect(AVPacket *pkt, Output *out)
+{
+    if (pkt->size > 0 && out->nb < FF_ARRAY_ELEMS(out->sizes) &&
+        out->total + pkt->size <= (int)sizeof(out->bytes)) {
+        out->sizes[out->nb++] = pkt->size;
+        memcpy(out->bytes + out->total, pkt->data, pkt->size);
+        out->total += pkt->size;
+    }
+    av_packet_unref(pkt);
+}
+
+/* Feed one freshly received RTP packet (carrying the block'th group of
+ * TS_PER_RTP TS packets, with RTP sequence number SEQ_BASE + block) and 
collect
+ * everything handed back, exactly as rtsp.c consumes ff_rtp_parse_packet():
+ * a return value of 1 means "call again with buf == NULL". */
+static void feed(RTPDemuxContext *s, AVPacket *pkt, const uint8_t *ts_stream,
+                 int block, Output *out)
+{
+    int len = RTP_HEADER_SIZE + RTP_PAYLOAD, ret;
+    uint8_t *buf = av_malloc(len);
+    uint8_t *bufptr = buf;
+
+    if (!buf)
+        return;
+    buf[0] = RTP_VERSION << 6;                 /* V=2, no padding/ext/CSRC    
*/
+    buf[1] = MP2T_PT;                           /* no marker bit               
*/
+    AV_WB16(buf + 2, SEQ_BASE + block);
+    AV_WB32(buf + 4, 0);                        /* timestamp (unused)          
*/
+    AV_WB32(buf + 8, 0x0badf00d);               /* ssrc                        
*/
+    memcpy(buf + RTP_HEADER_SIZE, ts_stream + block * RTP_PAYLOAD, 
RTP_PAYLOAD);
+
+    ret = ff_rtp_parse_packet(s, pkt, &bufptr, len);
+    while (ret == 1) {
+        collect(pkt, out);
+        ret = ff_rtp_parse_packet(s, pkt, NULL, 0);
+    }
+    if (ret == 0)
+        collect(pkt, out);
+
+    if (bufptr)                                 /* not taken by the queue      
*/
+        av_free(bufptr);
+}
+
+static int run(const uint8_t *ts_stream, const int *order, Output *out)
+{
+    AVFormatContext *fmt = avformat_alloc_context();
+    RTPDemuxContext *s;
+    PayloadContext *pc;
+    AVPacket *pkt;
+    int i;
+
+    memset(out, 0, sizeof(*out));
+    if (!fmt)
+        return AVERROR(ENOMEM);
+
+    s = ff_rtp_parse_open(fmt, NULL, MP2T_PT, QUEUE_SIZE);
+    pc = av_mallocz(ff_mpegts_dynamic_handler.priv_data_size);
+    pkt = av_packet_alloc();
+    if (!s || !pc || !pkt) {
+        av_packet_free(&pkt);
+        av_free(pc);
+        if (s)
+            ff_rtp_parse_close(s);
+        avformat_free_context(fmt);
+        return AVERROR(ENOMEM);
+    }
+    ff_mpegts_dynamic_handler.init(fmt, -1, pc);
+    ff_rtp_parse_set_dynamic_protocol(s, pc, &ff_mpegts_dynamic_handler);
+
+    for (i = 0; i < NB_RTP_PACKETS; i++)
+        feed(s, pkt, ts_stream, order[i], out);
+
+    av_packet_free(&pkt);
+    ff_mpegts_dynamic_handler.close(pc);
+    av_free(pc);
+    ff_rtp_parse_close(s);
+    avformat_free_context(fmt);
+    return 0;
+}
+
+static void dump(const char *label, const Output *o)
+{
+    int i;
+    fprintf(stderr, "  %-9s %d packet(s), %d bytes, sizes:", label, o->nb, 
o->total);
+    for (i = 0; i < o->nb; i++)
+        fprintf(stderr, " %d", o->sizes[i]);
+    fprintf(stderr, "\n");
+}
+
+int main(void)
+{
+    static uint8_t ts_stream[NB_TS_PACKETS * TS_SIZE];
+    /* Block indices in arrival order; reordered swaps the continuation packet
+     * (block 1, TS 7..13) and the mid-PUSI packet (block 2, TS 14..20). No
+     * packet is dropped in either case. */
+    static const int in_order[NB_RTP_PACKETS]  = { 0, 1, 2, 3, 4 };
+    static const int reordered[NB_RTP_PACKETS] = { 0, 2, 1, 3, 4 };
+    Output ref, got;
+
+    build_ts_stream(ts_stream);
+
+    if (run(ts_stream, in_order,  &ref) < 0 ||
+        run(ts_stream, reordered, &got) < 0) {
+        fprintf(stderr, "test setup failed\n");
+        return 2;
+    }
+
+    if (ref.nb != got.nb || ref.total != got.total ||
+        memcmp(ref.sizes, got.sizes, ref.nb * sizeof(ref.sizes[0])) ||
+        memcmp(ref.bytes, got.bytes, ref.total)) {
+        fprintf(stderr,
+                "FAIL: reordering an RTP packet with a mid-payload PUSI 
dropped "
+                "MPEG-TS data.\n");
+        dump("in-order:",  &ref);
+        dump("reordered:", &got);
+        return 1;
+    }
+
+    printf("OK: %d PES packets (%d bytes) recovered identically with and "
+           "without reordering\n", ref.nb, ref.total);
+    return 0;
+}
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to