../../../ffmpeg/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
72 / 93
77.4%
Branches:
43 / 64
67.2%
Functions:
5 / 5
100.0%

libavcodec/bsf/jpegapp.c
Line Branch Exec Source
1 /*
2 * Inject per-frame JPEG APP segments from a length-prefixed sidecar.
3 *
4 * Copyright (c) 2026 Reinhard Nißl
5 *
6 * This file is part of FFmpeg.
7 *
8 * FFmpeg is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Lesser General Public
10 * License as published by the Free Software Foundation; either
11 * version 2.1 of the License, or (at your option) any later version.
12 *
13 * FFmpeg is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * Lesser General Public License for more details.
17 *
18 * You should have received a copy of the GNU Lesser General Public
19 * License along with FFmpeg; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 */
22
23 /**
24 * Sidecar format (big-endian), one record per encoded JPEG packet:
25 *
26 * u32be length
27 * u8 payload[length]
28 *
29 * Payload is zero or more complete JPEG APP segments (APP0..APP15),
30 * each ``FF En`` + 16-bit length (length includes those two bytes).
31 * A zero length leaves the packet unchanged.
32 *
33 * After SOI the payload is inserted. If it starts with APP0 and the
34 * two bytes immediately after SOI are an APP0 marker, that segment is
35 * dropped so the sidecar can replace it. An APP0 preceded by fill
36 * bytes or another segment is retained. An APP1-only payload keeps
37 * the encoder APP0: SOI, APP1, APP0, DQT, ...
38 *
39 * A sidecar read or validation error stops further record consumption
40 * for this filter context. Flushing does not clear that error.
41 */
42
43 #include <errno.h>
44 #include <inttypes.h>
45 #include <limits.h>
46 #include <stdint.h>
47 #include <stdio.h>
48 #include <string.h>
49
50 #include "libavutil/error.h"
51 #include "libavutil/file_open.h"
52 #include "libavutil/intreadwrite.h"
53 #include "libavutil/log.h"
54 #include "libavutil/mem.h"
55 #include "libavutil/opt.h"
56
57 #include "libavcodec/bsf.h"
58 #include "libavcodec/bsf_internal.h"
59 #include "libavcodec/bytestream.h"
60 #include "libavcodec/mjpeg.h"
61
62 #define JPEGAPP_MAX_RECORD (16 * 1024 * 1024)
63
64 typedef struct JPEGAppContext {
65 const AVClass *class;
66 GetByteContext gb;
67 uint8_t *sidecar;
68 int sidecar_len;
69 int read_error;
70 int64_t packet_index;
71 } JPEGAppContext;
72
73 3 static int jpeg_app_segment_len(const uint8_t *data, int size)
74 {
75 int len;
76
77
4/8
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 6 not taken.
✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 3 times.
✗ Branch 4 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 3 times.
3 if (size < 4 || data[0] != 0xff || data[1] < APP0 || data[1] > APP15)
78 ✗ return AVERROR_INVALIDDATA;
79 3 len = AV_RB16(data + 2);
80
2/4
✓ Branch 7 → 8 taken 3 times.
✗ Branch 7 → 9 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 3 times.
3 if (len < 2 || len > size - 2)
81 ✗ return AVERROR_INVALIDDATA;
82 3 return 2 + len;
83 }
84
85 2 static int jpegapp_validate_payload(const uint8_t *data, int size)
86 {
87 2 int pos = 0, n;
88
89
2/2
✓ Branch 7 → 3 taken 2 times.
✓ Branch 7 → 8 taken 2 times.
4 while (pos < size) {
90 2 n = jpeg_app_segment_len(data + pos, size - pos);
91
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
2 if (n < 0)
92 ✗ return n;
93 2 pos += n;
94 }
95 2 return 0;
96 }
97
98 4 static int jpegapp_init(AVBSFContext *ctx)
99 {
100 4 JPEGAppContext *s = ctx->priv_data;
101
102
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 4 times.
4 if (s->sidecar_len <= 0) {
103 ✗ av_log(ctx, AV_LOG_ERROR, "Option sidecar is required\n");
104 ✗ return AVERROR(EINVAL);
105 }
106
107 4 bytestream2_init(&s->gb, s->sidecar, s->sidecar_len);
108
109 4 return 0;
110 }
111
112 8 static int jpegapp_read_record(AVBSFContext *ctx, int *out_size)
113 {
114 8 JPEGAppContext *s = ctx->priv_data;
115 uint32_t n;
116 int ret;
117
118
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 6 taken 7 times.
8 if (!bytestream2_get_bytes_left(&s->gb)) {
119 1 av_log(ctx, AV_LOG_ERROR,
120 "jpegapp: no sidecar record for packet %"PRId64"\n",
121 s->packet_index);
122 1 return AVERROR_INVALIDDATA;
123 }
124
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 7 times.
7 if (bytestream2_get_bytes_left(&s->gb) < 4) {
125 ✗ av_log(ctx, AV_LOG_ERROR, "jpegapp: truncated length prefix\n");
126 ✗ return AVERROR_INVALIDDATA;
127 }
128 7 n = bytestream2_get_be32(&s->gb);
129
2/2
✓ Branch 11 → 12 taken 2 times.
✓ Branch 11 → 14 taken 5 times.
7 if (n > JPEGAPP_MAX_RECORD) {
130 2 av_log(ctx, AV_LOG_ERROR, "jpegapp: record length %u too large\n", n);
131 2 return AVERROR_INVALIDDATA;
132 }
133
2/2
✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 23 taken 3 times.
5 if (n) {
134
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 19 taken 2 times.
2 if (n > bytestream2_get_bytes_left(&s->gb)) {
135 ✗ av_log(ctx, AV_LOG_ERROR, "jpegapp: truncated record payload\n");
136 ✗ return AVERROR_INVALIDDATA;
137 }
138 2 ret = jpegapp_validate_payload(s->gb.buffer, n);
139
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 23 taken 2 times.
2 if (ret < 0) {
140 ✗ av_log(ctx, AV_LOG_ERROR,
141 "jpegapp: payload is not a sequence of APP0..APP15 segments\n");
142 ✗ return ret;
143 }
144 }
145 5 *out_size = n;
146 5 return 0;
147 }
148
149 156 static int jpegapp_filter(AVBSFContext *ctx, AVPacket *out)
150 {
151 156 JPEGAppContext *s = ctx->priv_data;
152 AVPacket *in;
153 156 int rec_size = 0, src_off, dst_size, ret;
154
155 156 ret = ff_bsf_get_packet(ctx, &in);
156
2/2
✓ Branch 3 → 4 taken 77 times.
✓ Branch 3 → 5 taken 79 times.
156 if (ret < 0)
157 77 return ret;
158
159 79 ret = s->read_error;
160
2/2
✓ Branch 5 → 6 taken 8 times.
✓ Branch 5 → 7 taken 71 times.
79 if (!ret)
161 8 ret = jpegapp_read_record(ctx, &rec_size);
162 79 s->packet_index++;
163
2/2
✓ Branch 7 → 8 taken 74 times.
✓ Branch 7 → 9 taken 5 times.
79 if (ret < 0) {
164 74 s->read_error = ret;
165 74 goto fail;
166 }
167
168
2/4
✓ Branch 9 → 10 taken 5 times.
✗ Branch 9 → 11 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 13 taken 5 times.
5 if (in->size < 2 || AV_RB16(in->data) != 0xffd8) {
169 ✗ av_log(ctx, AV_LOG_ERROR, "jpegapp: packet is not JPEG (missing SOI)\n");
170 ✗ ret = AVERROR_INVALIDDATA;
171 ✗ goto fail;
172 }
173
174 5 src_off = 2;
175
5/6
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 24 taken 3 times.
✓ Branch 14 → 15 taken 2 times.
✗ Branch 14 → 24 not taken.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 24 taken 1 time.
5 if (rec_size >= 2 && s->gb.buffer[0] == 0xff && s->gb.buffer[1] == APP0 &&
176
3/6
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 24 not taken.
✓ Branch 17 → 18 taken 1 time.
✗ Branch 17 → 24 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 24 not taken.
1 in->size >= 4 && in->data[2] == 0xff && in->data[3] == APP0) {
177 1 int skip = jpeg_app_segment_len(in->data + 2, in->size - 2);
178
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 23 taken 1 time.
1 if (skip < 0) {
179 ✗ av_log(ctx, AV_LOG_ERROR,
180 "jpegapp: existing APP0 is invalid or truncated\n");
181 ✗ ret = skip;
182 ✗ goto fail;
183 }
184 1 src_off = 2 + skip;
185 }
186
187
2/2
✓ Branch 24 → 25 taken 3 times.
✓ Branch 24 → 27 taken 2 times.
5 if (!rec_size) {
188 3 ret = av_packet_ref(out, in);
189 3 goto fail;
190 }
191
192
1/2
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 2 times.
2 if (in->size - src_off > INT_MAX - 2 - rec_size) {
193 ✗ ret = AVERROR(ERANGE);
194 ✗ goto fail;
195 }
196 2 dst_size = 2 + rec_size + (in->size - src_off);
197 2 ret = av_new_packet(out, dst_size);
198
1/2
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 2 times.
2 if (ret < 0)
199 ✗ goto fail;
200 2 ret = av_packet_copy_props(out, in);
201
1/2
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 2 times.
2 if (ret < 0)
202 ✗ goto fail;
203
204 2 memcpy(out->data, in->data, 2);
205 2 bytestream2_get_buffer(&s->gb, out->data + 2, rec_size);
206 2 memcpy(out->data + 2 + rec_size, in->data + src_off, in->size - src_off);
207 2 av_packet_free(&in);
208 2 return 0;
209
210 77 fail:
211
2/2
✓ Branch 38 → 39 taken 74 times.
✓ Branch 38 → 40 taken 3 times.
77 if (ret < 0)
212 74 av_packet_unref(out);
213 77 av_packet_free(&in);
214 77 return ret;
215 }
216
217 #define OFFSET(x) offsetof(JPEGAppContext, x)
218 #define FLAGS (AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_BSF_PARAM)
219 static const AVOption jpegapp_options[] = {
220 { "sidecar", "sidecar of u32be-length + APP-segment records", OFFSET(sidecar),
221 AV_OPT_TYPE_BINARY, .flags = FLAGS },
222 { NULL },
223 };
224
225 static const AVClass jpegapp_class = {
226 .class_name = "jpegapp",
227 .item_name = av_default_item_name,
228 .option = jpegapp_options,
229 .version = LIBAVUTIL_VERSION_INT,
230 };
231
232 static const enum AVCodecID jpegapp_codec_ids[] = {
233 AV_CODEC_ID_MJPEG, AV_CODEC_ID_NONE,
234 };
235
236 const FFBitStreamFilter ff_jpegapp_bsf = {
237 .p.name = "jpegapp",
238 .p.codec_ids = jpegapp_codec_ids,
239 .p.priv_class = &jpegapp_class,
240 .priv_data_size = sizeof(JPEGAppContext),
241 .init = jpegapp_init,
242 .filter = jpegapp_filter,
243 };
244