FFmpeg coverage


Directory: ../../../ffmpeg/
File: src/libavformat/tests/rtpdec.c
Date: 2026-10-15 10:36:02
Exec Total Coverage
Lines: 84 110 76.4%
Functions: 7 8 87.5%
Branches: 31 50 62.0%

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