| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * Copyright (c) 2026 Torbjörn Einarsson | ||
| 3 | * | ||
| 4 | * This file is part of FFmpeg. | ||
| 5 | * | ||
| 6 | * FFmpeg is free software; you can redistribute it and/or | ||
| 7 | * modify it under the terms of the GNU Lesser General Public | ||
| 8 | * License as published by the Free Software Foundation; either | ||
| 9 | * version 2.1 of the License, or (at your option) any later version. | ||
| 10 | * | ||
| 11 | * FFmpeg is distributed in the hope that it will be useful, | ||
| 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
| 14 | * Lesser General Public License for more details. | ||
| 15 | * | ||
| 16 | * You should have received a copy of the GNU Lesser General Public | ||
| 17 | * License along with FFmpeg; if not, write to the Free Software | ||
| 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | ||
| 19 | */ | ||
| 20 | |||
| 21 | /* | ||
| 22 | * Regression test for ff_rtp_parse_packet() and the RTP reordering queue, | ||
| 23 | * exercised through the real MPEG-2 TS depacketizer (rtpdec_mpegts.c) with | ||
| 24 | * realistic full-size RTP packets (12-byte RTP header + 7 * 188-byte TS | ||
| 25 | * packets, i.e. the common 1328-byte MPEG-TS-over-RTP packet). | ||
| 26 | * | ||
| 27 | * The MPEG-TS depacketizer consumes an RTP payload only up to the point where | ||
| 28 | * a payload unit start indicator (PUSI) closes the current PES. If that PUSI | ||
| 29 | * sits on a TS packet other than the first in the RTP payload, the depacketizer | ||
| 30 | * returns the finished PES, buffers the rest of the payload internally and | ||
| 31 | * returns 1, expecting the caller to fetch the rest with a follow-up | ||
| 32 | * buf == NULL call. A plain continuation RTP payload (no PUSI, the common case | ||
| 33 | * in a video stream) completes no PES and makes the depacketizer return < 0. | ||
| 34 | * | ||
| 35 | * ff_rtp_parse_packet() routes the follow-up buf == NULL call back into the | ||
| 36 | * depacketizer (to drain its buffer) only while s->prev_ret > 0. When a packet | ||
| 37 | * is delivered from the "while (rv < 0 && has_next_packet(s))" reordering loop, | ||
| 38 | * prev_ret used not to be updated, so after such a delivery the buffered tail | ||
| 39 | * (the TS packets from the mid-payload PUSI onwards) was never drained and got | ||
| 40 | * dropped. | ||
| 41 | * | ||
| 42 | * This needs nothing more exotic than a single reordered pair of packets: a | ||
| 43 | * continuation packet (returns < 0) parsed while a mid-PUSI packet (returns 1) | ||
| 44 | * is waiting in the queue is enough to enter that loop -- which is why the loss | ||
| 45 | * was observed very frequently, not only on heavy reordering. | ||
| 46 | * | ||
| 47 | * The test feeds the exact same packets twice -- once in order (the reference; | ||
| 48 | * the in-order path never enters the buggy loop) and once with one adjacent | ||
| 49 | * pair swapped -- and checks that both deliver identical PES data. No packet is | ||
| 50 | * ever lost, so a correct depacketizer must recover everything. | ||
| 51 | */ | ||
| 52 | |||
| 53 | #include <stdio.h> | ||
| 54 | #include <string.h> | ||
| 55 | |||
| 56 | #include "libavutil/mem.h" | ||
| 57 | #include "libavutil/intreadwrite.h" | ||
| 58 | #include "libavformat/avformat.h" | ||
| 59 | #include "libavformat/rtpdec.h" | ||
| 60 | #include "libavformat/rtpdec_formats.h" | ||
| 61 | |||
| 62 | #define TS_SIZE 188 | ||
| 63 | #define TS_PAYLOAD 184 /* TS_SIZE - 4-byte TS header */ | ||
| 64 | #define TS_PER_RTP 7 /* 12 + 7*188 = 1328-byte RTP packets */ | ||
| 65 | #define RTP_HEADER_SIZE 12 | ||
| 66 | #define RTP_PAYLOAD (TS_PER_RTP * TS_SIZE) | ||
| 67 | #define MP2T_PT 33 /* static RTP payload type for MPEG-2 TS*/ | ||
| 68 | #define QUEUE_SIZE 2 /* reordering enabled, small jitter buf */ | ||
| 69 | |||
| 70 | #define ES_PID 0x0100 | ||
| 71 | #define PES_STREAM_ID 0xe0 /* a video stream id */ | ||
| 72 | #define PES_HDR_SIZE 9 /* 00 00 01 id len(2) flags(2) hdrlen */ | ||
| 73 | |||
| 74 | /* PES payload sizes that map onto a whole number of TS packets: | ||
| 75 | * first packet carries PES_HDR_SIZE + (TS_PAYLOAD - PES_HDR_SIZE) ES bytes, the | ||
| 76 | * rest carry TS_PAYLOAD each. 17 TS packets -> 175 + 16*184 ES bytes. */ | ||
| 77 | #define PES_TS_PACKETS 17 | ||
| 78 | #define PES_ES_LEN ((TS_PAYLOAD - PES_HDR_SIZE) + (PES_TS_PACKETS - 1) * TS_PAYLOAD) | ||
| 79 | |||
| 80 | /* PES boundary layout (one PES per access unit), 35 TS packets = 5 RTP packets: | ||
| 81 | * PES_0 : TS 0..16 (PUSI on TS 0) | ||
| 82 | * PES_1 : TS 17..33 (PUSI on TS 17, mid RTP packet 2) | ||
| 83 | * PES_2 : TS 34 (PUSI on TS 34, closes PES_1; PES_2 left pending) | ||
| 84 | * => RTP 1 (TS 7..13) is pure continuation, RTP 2 (TS 14..20) carries the | ||
| 85 | * mid-payload PUSI. Swapping their arrival order triggers the bug. */ | ||
| 86 | #define NB_TS_PACKETS 35 | ||
| 87 | #define NB_RTP_PACKETS (NB_TS_PACKETS / TS_PER_RTP) /* 5 */ | ||
| 88 | |||
| 89 | /* Start sequence numbers away from 0: ff_rtp_parse_packet() treats s->seq == 0 | ||
| 90 | * as "no packet seen yet" and bypasses the reordering queue for it. */ | ||
| 91 | #define SEQ_BASE 100 | ||
| 92 | |||
| 93 | typedef struct Output { | ||
| 94 | int sizes[64]; | ||
| 95 | int nb; | ||
| 96 | uint8_t bytes[1 << 16]; | ||
| 97 | int total; | ||
| 98 | } Output; | ||
| 99 | |||
| 100 | /* ------------------------------------------------------------------ */ | ||
| 101 | /* MPEG-TS stream construction */ | ||
| 102 | /* ------------------------------------------------------------------ */ | ||
| 103 | |||
| 104 | 35 | static void put_ts_packet(uint8_t *dst, int pusi, int cc, | |
| 105 | const uint8_t *payload) | ||
| 106 | { | ||
| 107 | 35 | dst[0] = 0x47; /* sync byte */ | |
| 108 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 32 times.
|
35 | dst[1] = (pusi ? 0x40 : 0x00) | ((ES_PID >> 8) & 0x1f); |
| 109 | 35 | dst[2] = ES_PID & 0xff; | |
| 110 | 35 | dst[3] = 0x10 | (cc & 0x0f); /* payload only, no AF */ | |
| 111 | 35 | memcpy(dst + 4, payload, TS_PAYLOAD); | |
| 112 | 35 | } | |
| 113 | |||
| 114 | /* Append one PES packet (header + es_len ES bytes) as TS packets. */ | ||
| 115 | 3 | static void add_pes(uint8_t *ts, int *off, int *cc, int *es_counter, int es_len) | |
| 116 | { | ||
| 117 | static const uint8_t pes_hdr[PES_HDR_SIZE] = { | ||
| 118 | 0x00, 0x00, 0x01, PES_STREAM_ID, | ||
| 119 | 0x00, 0x00, /* PES_packet_length 0 == unbounded */ | ||
| 120 | 0x80, 0x00, 0x00, /* '10' marker, no PTS/DTS, header_data_len 0 */ | ||
| 121 | }; | ||
| 122 | 3 | int es_done = 0, first = 1; | |
| 123 | |||
| 124 |
2/2✓ Branch 0 taken 35 times.
✓ Branch 1 taken 3 times.
|
38 | while (es_done < es_len) { |
| 125 | uint8_t payload[TS_PAYLOAD]; | ||
| 126 | 35 | int p = 0, i, chunk; | |
| 127 | |||
| 128 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 32 times.
|
35 | if (first) { |
| 129 | 3 | memcpy(payload, pes_hdr, PES_HDR_SIZE); | |
| 130 | 3 | p = PES_HDR_SIZE; | |
| 131 | } | ||
| 132 | 35 | chunk = TS_PAYLOAD - p; | |
| 133 |
2/2✓ Branch 0 taken 6413 times.
✓ Branch 1 taken 35 times.
|
6448 | for (i = 0; i < chunk; i++) |
| 134 | 6413 | payload[p + i] = (uint8_t)((*es_counter)++ & 0xff); | |
| 135 | |||
| 136 | 35 | put_ts_packet(ts + *off, first, *cc, payload); | |
| 137 | 35 | *off += TS_SIZE; | |
| 138 | 35 | *cc = (*cc + 1) & 0x0f; | |
| 139 | 35 | es_done += chunk; | |
| 140 | 35 | first = 0; | |
| 141 | } | ||
| 142 | 3 | } | |
| 143 | |||
| 144 | 1 | static void build_ts_stream(uint8_t *ts) | |
| 145 | { | ||
| 146 | 1 | int off = 0, cc = 0, es_counter = 0; | |
| 147 | 1 | add_pes(ts, &off, &cc, &es_counter, PES_ES_LEN); /* PES_0: TS 0..16 */ | |
| 148 | 1 | add_pes(ts, &off, &cc, &es_counter, PES_ES_LEN); /* PES_1: TS 17..33 */ | |
| 149 | 1 | add_pes(ts, &off, &cc, &es_counter, TS_PAYLOAD - PES_HDR_SIZE); /* PES_2: TS 34 */ | |
| 150 | 1 | } | |
| 151 | |||
| 152 | /* ------------------------------------------------------------------ */ | ||
| 153 | /* RTP feeding */ | ||
| 154 | /* ------------------------------------------------------------------ */ | ||
| 155 | |||
| 156 | 4 | static void collect(AVPacket *pkt, Output *out) | |
| 157 | { | ||
| 158 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
|
4 | if (pkt->size > 0 && out->nb < FF_ARRAY_ELEMS(out->sizes) && |
| 159 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | out->total + pkt->size <= (int)sizeof(out->bytes)) { |
| 160 | 4 | out->sizes[out->nb++] = pkt->size; | |
| 161 | 4 | memcpy(out->bytes + out->total, pkt->data, pkt->size); | |
| 162 | 4 | out->total += pkt->size; | |
| 163 | } | ||
| 164 | 4 | av_packet_unref(pkt); | |
| 165 | 4 | } | |
| 166 | |||
| 167 | /* Feed one freshly received RTP packet (carrying the block'th group of | ||
| 168 | * TS_PER_RTP TS packets, with RTP sequence number SEQ_BASE + block) and collect | ||
| 169 | * everything handed back, exactly as rtsp.c consumes ff_rtp_parse_packet(): | ||
| 170 | * a return value of 1 means "call again with buf == NULL". */ | ||
| 171 | 10 | static void feed(RTPDemuxContext *s, AVPacket *pkt, const uint8_t *ts_stream, | |
| 172 | int block, Output *out) | ||
| 173 | { | ||
| 174 | 10 | int len = RTP_HEADER_SIZE + RTP_PAYLOAD, ret; | |
| 175 | 10 | uint8_t *buf = av_malloc(len); | |
| 176 | 10 | uint8_t *bufptr = buf; | |
| 177 | |||
| 178 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
|
10 | if (!buf) |
| 179 | ✗ | return; | |
| 180 | 10 | buf[0] = RTP_VERSION << 6; /* V=2, no padding/ext/CSRC */ | |
| 181 | 10 | buf[1] = MP2T_PT; /* no marker bit */ | |
| 182 | 10 | AV_WB16(buf + 2, SEQ_BASE + block); | |
| 183 | 10 | AV_WB32(buf + 4, 0); /* timestamp (unused) */ | |
| 184 | 10 | AV_WB32(buf + 8, 0x0badf00d); /* ssrc */ | |
| 185 | 10 | memcpy(buf + RTP_HEADER_SIZE, ts_stream + block * RTP_PAYLOAD, RTP_PAYLOAD); | |
| 186 | |||
| 187 | 10 | ret = ff_rtp_parse_packet(s, pkt, &bufptr, len); | |
| 188 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 10 times.
|
12 | while (ret == 1) { |
| 189 | 2 | collect(pkt, out); | |
| 190 | 2 | ret = ff_rtp_parse_packet(s, pkt, NULL, 0); | |
| 191 | } | ||
| 192 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 8 times.
|
10 | if (ret == 0) |
| 193 | 2 | collect(pkt, out); | |
| 194 | |||
| 195 |
2/2✓ Branch 0 taken 9 times.
✓ Branch 1 taken 1 times.
|
10 | if (bufptr) /* not taken by the queue */ |
| 196 | 9 | av_free(bufptr); | |
| 197 | } | ||
| 198 | |||
| 199 | 2 | static int run(const uint8_t *ts_stream, const int *order, Output *out) | |
| 200 | { | ||
| 201 | 2 | AVFormatContext *fmt = avformat_alloc_context(); | |
| 202 | RTPDemuxContext *s; | ||
| 203 | PayloadContext *pc; | ||
| 204 | AVPacket *pkt; | ||
| 205 | int i, ret; | ||
| 206 | |||
| 207 | 2 | memset(out, 0, sizeof(*out)); | |
| 208 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | if (!fmt) |
| 209 | ✗ | return AVERROR(ENOMEM); | |
| 210 | |||
| 211 | 2 | s = ff_rtp_parse_open(fmt, NULL, MP2T_PT, QUEUE_SIZE); | |
| 212 | 2 | pc = av_mallocz(ff_mpegts_dynamic_handler.priv_data_size); | |
| 213 | 2 | pkt = av_packet_alloc(); | |
| 214 |
3/6✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
|
2 | if (!s || !pc || !pkt) { |
| 215 | ✗ | av_packet_free(&pkt); | |
| 216 | ✗ | av_free(pc); | |
| 217 | ✗ | if (s) | |
| 218 | ✗ | ff_rtp_parse_close(s); | |
| 219 | ✗ | avformat_free_context(fmt); | |
| 220 | ✗ | return AVERROR(ENOMEM); | |
| 221 | } | ||
| 222 | 2 | ret = ff_mpegts_dynamic_handler.init(fmt, -1, pc); | |
| 223 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | if (ret < 0) { |
| 224 | ✗ | av_packet_free(&pkt); | |
| 225 | ✗ | ff_mpegts_dynamic_handler.close(pc); | |
| 226 | ✗ | av_free(pc); | |
| 227 | ✗ | ff_rtp_parse_close(s); | |
| 228 | ✗ | avformat_free_context(fmt); | |
| 229 | ✗ | return ret; | |
| 230 | } | ||
| 231 | 2 | ff_rtp_parse_set_dynamic_protocol(s, pc, &ff_mpegts_dynamic_handler); | |
| 232 | |||
| 233 |
2/2✓ Branch 0 taken 10 times.
✓ Branch 1 taken 2 times.
|
12 | for (i = 0; i < NB_RTP_PACKETS; i++) |
| 234 | 10 | feed(s, pkt, ts_stream, order[i], out); | |
| 235 | |||
| 236 | 2 | av_packet_free(&pkt); | |
| 237 | 2 | ff_mpegts_dynamic_handler.close(pc); | |
| 238 | 2 | av_free(pc); | |
| 239 | 2 | ff_rtp_parse_close(s); | |
| 240 | 2 | avformat_free_context(fmt); | |
| 241 | 2 | return 0; | |
| 242 | } | ||
| 243 | |||
| 244 | ✗ | static void dump(const char *label, const Output *o) | |
| 245 | { | ||
| 246 | int i; | ||
| 247 | ✗ | fprintf(stderr, " %-9s %d packet(s), %d bytes, sizes:", label, o->nb, o->total); | |
| 248 | ✗ | for (i = 0; i < o->nb; i++) | |
| 249 | ✗ | fprintf(stderr, " %d", o->sizes[i]); | |
| 250 | ✗ | fprintf(stderr, "\n"); | |
| 251 | ✗ | } | |
| 252 | |||
| 253 | 1 | int main(void) | |
| 254 | { | ||
| 255 | static uint8_t ts_stream[NB_TS_PACKETS * TS_SIZE]; | ||
| 256 | /* Block indices in arrival order; reordered swaps the continuation packet | ||
| 257 | * (block 1, TS 7..13) and the mid-PUSI packet (block 2, TS 14..20). No | ||
| 258 | * packet is dropped in either case. */ | ||
| 259 | static const int in_order[NB_RTP_PACKETS] = { 0, 1, 2, 3, 4 }; | ||
| 260 | static const int reordered[NB_RTP_PACKETS] = { 0, 2, 1, 3, 4 }; | ||
| 261 | static Output ref, got; | ||
| 262 | |||
| 263 | 1 | build_ts_stream(ts_stream); | |
| 264 | |||
| 265 |
2/4✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
|
2 | if (run(ts_stream, in_order, &ref) < 0 || |
| 266 | 1 | run(ts_stream, reordered, &got) < 0) { | |
| 267 | ✗ | fprintf(stderr, "test setup failed\n"); | |
| 268 | ✗ | return 2; | |
| 269 | } | ||
| 270 | |||
| 271 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
1 | if (ref.nb != got.nb || ref.total != got.total || |
| 272 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | memcmp(ref.sizes, got.sizes, ref.nb * sizeof(ref.sizes[0])) || |
| 273 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | memcmp(ref.bytes, got.bytes, ref.total)) { |
| 274 | ✗ | fprintf(stderr, | |
| 275 | "FAIL: reordering an RTP packet with a mid-payload PUSI dropped " | ||
| 276 | "MPEG-TS data.\n"); | ||
| 277 | ✗ | dump("in-order:", &ref); | |
| 278 | ✗ | dump("reordered:", &got); | |
| 279 | ✗ | return 1; | |
| 280 | } | ||
| 281 | |||
| 282 | 1 | printf("OK: %d PES packets (%d bytes) recovered identically with and " | |
| 283 | "without reordering\n", ref.nb, ref.total); | ||
| 284 | 1 | return 0; | |
| 285 | } | ||
| 286 |