This is an automated email from the git hooks/post-receive script. Git pushed a commit to branch master in repository ffmpeg.
commit 955276412d447902ac3dec1dd6a7ee08ad1128be Author: Reinhard Nißl <[email protected]> AuthorDate: Sun Sep 20 21:15:19 2026 +0200 Commit: michaelni <[email protected]> CommitDate: Wed Oct 14 20:43:13 2026 +0000 avcodec/bsf: add jpegapp to inject JPEG APP segments Inject APP0..APP15 segments from a length-prefixed sidecar into each MJPEG packet after SOI. A zero-length record passes the packet through. An APP1-only payload retains the existing APP0; a payload starting with APP0 replaces an APP0 located immediately after SOI. Consume records in packet arrival order, with a 16 MiB record limit. Missing or invalid records return an error. Remember sidecar read and validation errors so later packets cannot consume a rejected record's payload as metadata. Flushing neither rewinds the sidecar nor clears this error; closing the context does not read ahead. Count every consumed input packet in diagnostics, distinguish I/O errors from truncated records, include the open error text for a sidecar that cannot be opened, and document the sidecar and lifecycle contract. Co-authored-by: Cursor <[email protected]> Signed-off-by: Reinhard Nißl <[email protected]> Assisted-by: Fairy --- Changelog | 1 + doc/bitstream_filters.texi | 39 ++++++ libavcodec/bitstream_filters.c | 1 + libavcodec/bsf/Makefile | 1 + libavcodec/bsf/jpegapp.c | 287 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 329 insertions(+) diff --git a/Changelog b/Changelog index 01232ef212..32e7c8bfe7 100644 --- a/Changelog +++ b/Changelog @@ -18,6 +18,7 @@ version <next>: - ADPCM IMA Citrix decoder - Panasonic RW2 decoder - xHE-AAC encoding via libfdk-xhe-aac +- jpegapp bitstream filter version 9.0: diff --git a/doc/bitstream_filters.texi b/doc/bitstream_filters.texi index a00757d315..47bb75892b 100644 --- a/doc/bitstream_filters.texi +++ b/doc/bitstream_filters.texi @@ -612,6 +612,45 @@ For example, to remux 30 MB/sec NTSC IMX to MOV: ffmpeg -i input.mxf -c copy -bsf:v imxdump -tag:v mx3n output.mov @end example +@section jpegapp + +Inject JPEG APP0..APP15 segments from a length-prefixed sidecar into each +MJPEG packet, after SOI. + +The sidecar is a sequence of records, one per encoded JPEG, in encode +order: + +@example +u32be length +u8 payload[length] +@end example + +@var{length} does not include the 4-byte prefix and must not exceed +16 MiB. A zero @var{length} leaves that packet unchanged. A non-empty +payload must be a concatenation of complete APP segments (@code{FF En} +plus the 16-bit length that includes those two length bytes). + +If the payload starts with APP0 and the packet has an APP0 marker in +the two bytes immediately following SOI, that APP0 segment is dropped. +APP0 segments preceded by fill bytes or another segment are retained. +An APP1-only payload keeps the encoder APP0: SOI, APP1, APP0, DQT, ... + +The filter requires @option{filename}. Extra unused sidecar bytes are +ignored; a packet without a matching record is an error. +Records follow packets in arrival order. Flushing the filter does not +rewind the sidecar, so a reused context needs a sidecar positioned for +the packets it will see next. +A sidecar read or validation error prevents further record consumption +by that filter context. To resume, create a new context with a sidecar +matching the remaining packets. Flushing does not clear this error. + +@example +ffmpeg -i in.mp4 -c:v mjpeg -bsf:v jpegapp=filename=app.bin frame_%d.jpg +@end example + +On Windows, escape the drive colon in the BSF option: +@code{filename='E\:/path/app.bin'}. + @section mjpeg2jpeg Convert MJPEG/AVI1 packets to full JPEG/JFIF packets. diff --git a/libavcodec/bitstream_filters.c b/libavcodec/bitstream_filters.c index f57c2632a0..ed57db5281 100644 --- a/libavcodec/bitstream_filters.c +++ b/libavcodec/bitstream_filters.c @@ -49,6 +49,7 @@ extern const FFBitStreamFilter ff_hapqa_extract_bsf; extern const FFBitStreamFilter ff_hevc_metadata_bsf; extern const FFBitStreamFilter ff_hevc_mp4toannexb_bsf; extern const FFBitStreamFilter ff_imx_dump_header_bsf; +extern const FFBitStreamFilter ff_jpegapp_bsf; extern const FFBitStreamFilter ff_lcevc_merge_bsf; extern const FFBitStreamFilter ff_lcevc_metadata_bsf; extern const FFBitStreamFilter ff_media100_to_mjpegb_bsf; diff --git a/libavcodec/bsf/Makefile b/libavcodec/bsf/Makefile index ec224e8774..c0badf5ed9 100644 --- a/libavcodec/bsf/Makefile +++ b/libavcodec/bsf/Makefile @@ -30,6 +30,7 @@ OBJS-$(CONFIG_DOVI_RPU_BSF) += bsf/dovi_rpu.o OBJS-$(CONFIG_DOVI_SPLIT_BSF) += bsf/dovi_split.o OBJS-$(CONFIG_HEVC_MP4TOANNEXB_BSF) += bsf/hevc_mp4toannexb.o OBJS-$(CONFIG_IMX_DUMP_HEADER_BSF) += bsf/imx_dump_header.o +OBJS-$(CONFIG_JPEGAPP_BSF) += bsf/jpegapp.o OBJS-$(CONFIG_LCEVC_MERGE_BSF) += bsf/lcevc_merge.o OBJS-$(CONFIG_LCEVC_METADATA_BSF) += bsf/lcevc_metadata.o OBJS-$(CONFIG_MEDIA100_TO_MJPEGB_BSF) += bsf/media100_to_mjpegb.o diff --git a/libavcodec/bsf/jpegapp.c b/libavcodec/bsf/jpegapp.c new file mode 100644 index 0000000000..c75822dfb6 --- /dev/null +++ b/libavcodec/bsf/jpegapp.c @@ -0,0 +1,287 @@ +/* + * Inject per-frame JPEG APP segments from a length-prefixed sidecar. + * + * Copyright (c) 2026 Reinhard Nißl + * + * 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 + */ + +/** + * Sidecar format (big-endian), one record per encoded JPEG packet: + * + * u32be length + * u8 payload[length] + * + * Payload is zero or more complete JPEG APP segments (APP0..APP15), + * each ``FF En`` + 16-bit length (length includes those two bytes). + * A zero length leaves the packet unchanged. + * + * After SOI the payload is inserted. If it starts with APP0 and the + * two bytes immediately after SOI are an APP0 marker, that segment is + * dropped so the sidecar can replace it. An APP0 preceded by fill + * bytes or another segment is retained. An APP1-only payload keeps + * the encoder APP0: SOI, APP1, APP0, DQT, ... + * + * A sidecar read or validation error stops further record consumption + * for this filter context. Flushing does not clear that error. + */ + +#include <errno.h> +#include <inttypes.h> +#include <limits.h> +#include <stdint.h> +#include <stdio.h> +#include <string.h> + +#include "libavutil/error.h" +#include "libavutil/file_open.h" +#include "libavutil/intreadwrite.h" +#include "libavutil/log.h" +#include "libavutil/mem.h" +#include "libavutil/opt.h" + +#include "libavcodec/bsf.h" +#include "libavcodec/bsf_internal.h" +#include "libavcodec/mjpeg.h" + +#define JPEGAPP_MAX_RECORD (16 * 1024 * 1024) + +typedef struct JPEGAppContext { + const AVClass *class; + char *filename; + FILE *file; + uint8_t *buf; + int buf_size; + int read_error; + int64_t packet_index; +} JPEGAppContext; + +static int jpeg_app_segment_len(const uint8_t *data, int size) +{ + int len; + + if (size < 4 || data[0] != 0xff || data[1] < APP0 || data[1] > APP15) + return AVERROR_INVALIDDATA; + len = AV_RB16(data + 2); + if (len < 2 || len > size - 2) + return AVERROR_INVALIDDATA; + return 2 + len; +} + +static int jpegapp_validate_payload(const uint8_t *data, int size) +{ + int pos = 0, n; + + while (pos < size) { + n = jpeg_app_segment_len(data + pos, size - pos); + if (n < 0) + return n; + pos += n; + } + return 0; +} + +static int jpegapp_init(AVBSFContext *ctx) +{ + JPEGAppContext *s = ctx->priv_data; + + if (!s->filename || !s->filename[0]) { + av_log(ctx, AV_LOG_ERROR, "Option filename is required\n"); + return AVERROR(EINVAL); + } + + s->file = avpriv_fopen_utf8(s->filename, "rb"); + if (!s->file) { + int err = errno ? AVERROR(errno) : AVERROR(EIO); + av_log(ctx, AV_LOG_ERROR, "Could not open '%s': %s\n", + s->filename, av_err2str(err)); + return err; + } + return 0; +} + +static void jpegapp_close(AVBSFContext *ctx) +{ + JPEGAppContext *s = ctx->priv_data; + + if (s->file) { + fclose(s->file); + s->file = NULL; + } + av_freep(&s->buf); + s->buf_size = 0; +} + +static int jpegapp_read_record(AVBSFContext *ctx, int *out_size) +{ + JPEGAppContext *s = ctx->priv_data; + uint8_t hdr[4]; + uint32_t n; + size_t got; + int ret; + + errno = 0; + got = fread(hdr, 1, 4, s->file); + ret = errno; + if (got == 0 && feof(s->file) && !ferror(s->file)) { + av_log(ctx, AV_LOG_ERROR, + "jpegapp: no sidecar record for packet %"PRId64"\n", + s->packet_index); + return AVERROR_INVALIDDATA; + } + if (got != 4) { + if (ferror(s->file)) { + av_log(ctx, AV_LOG_ERROR, "jpegapp: sidecar read failed\n"); + return ret ? AVERROR(ret) : AVERROR(EIO); + } + av_log(ctx, AV_LOG_ERROR, "jpegapp: truncated length prefix\n"); + return AVERROR_INVALIDDATA; + } + n = AV_RB32(hdr); + if (n > JPEGAPP_MAX_RECORD) { + av_log(ctx, AV_LOG_ERROR, "jpegapp: record length %u too large\n", n); + return AVERROR_INVALIDDATA; + } + if (n > (uint32_t)s->buf_size) { + ret = av_reallocp(&s->buf, n); + if (ret < 0) { + s->buf_size = 0; + return ret; + } + s->buf_size = n; + } + if (n) { + errno = 0; + got = fread(s->buf, 1, n, s->file); + ret = errno; + if (got != n) { + if (ferror(s->file)) { + av_log(ctx, AV_LOG_ERROR, "jpegapp: sidecar read failed\n"); + return ret ? AVERROR(ret) : AVERROR(EIO); + } + av_log(ctx, AV_LOG_ERROR, "jpegapp: truncated record payload\n"); + return AVERROR_INVALIDDATA; + } + ret = jpegapp_validate_payload(s->buf, n); + if (ret < 0) { + av_log(ctx, AV_LOG_ERROR, + "jpegapp: payload is not a sequence of APP0..APP15 segments\n"); + return ret; + } + } + *out_size = n; + return 0; +} + +static int jpegapp_filter(AVBSFContext *ctx, AVPacket *out) +{ + JPEGAppContext *s = ctx->priv_data; + AVPacket *in; + int rec_size = 0, src_off, dst_size, ret; + + ret = ff_bsf_get_packet(ctx, &in); + if (ret < 0) + return ret; + + ret = s->read_error; + if (!ret) + ret = jpegapp_read_record(ctx, &rec_size); + s->packet_index++; + if (ret < 0) { + s->read_error = ret; + goto fail; + } + + if (in->size < 2 || AV_RB16(in->data) != 0xffd8) { + av_log(ctx, AV_LOG_ERROR, "jpegapp: packet is not JPEG (missing SOI)\n"); + ret = AVERROR_INVALIDDATA; + goto fail; + } + + src_off = 2; + if (rec_size >= 2 && s->buf[0] == 0xff && s->buf[1] == APP0 && + in->size >= 4 && in->data[2] == 0xff && in->data[3] == APP0) { + int skip = jpeg_app_segment_len(in->data + 2, in->size - 2); + if (skip < 0) { + av_log(ctx, AV_LOG_ERROR, + "jpegapp: existing APP0 is invalid or truncated\n"); + ret = skip; + goto fail; + } + src_off = 2 + skip; + } + + if (!rec_size) { + ret = av_packet_ref(out, in); + goto fail; + } + + if (in->size - src_off > INT_MAX - 2 - rec_size) { + ret = AVERROR(ERANGE); + goto fail; + } + dst_size = 2 + rec_size + (in->size - src_off); + ret = av_new_packet(out, dst_size); + if (ret < 0) + goto fail; + ret = av_packet_copy_props(out, in); + if (ret < 0) + goto fail; + + memcpy(out->data, in->data, 2); + memcpy(out->data + 2, s->buf, rec_size); + memcpy(out->data + 2 + rec_size, in->data + src_off, in->size - src_off); + av_packet_free(&in); + return 0; + +fail: + if (ret < 0) + av_packet_unref(out); + av_packet_free(&in); + return ret; +} + +#define OFFSET(x) offsetof(JPEGAppContext, x) +#define FLAGS (AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_BSF_PARAM) +static const AVOption jpegapp_options[] = { + { "filename", "sidecar of u32be-length + APP-segment records", OFFSET(filename), + AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, FLAGS }, + { "file", "sidecar of u32be-length + APP-segment records", OFFSET(filename), + AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, FLAGS }, + { NULL }, +}; + +static const AVClass jpegapp_class = { + .class_name = "jpegapp", + .item_name = av_default_item_name, + .option = jpegapp_options, + .version = LIBAVUTIL_VERSION_INT, +}; + +static const enum AVCodecID jpegapp_codec_ids[] = { + AV_CODEC_ID_MJPEG, AV_CODEC_ID_NONE, +}; + +const FFBitStreamFilter ff_jpegapp_bsf = { + .p.name = "jpegapp", + .p.codec_ids = jpegapp_codec_ids, + .p.priv_class = &jpegapp_class, + .priv_data_size = sizeof(JPEGAppContext), + .init = jpegapp_init, + .close = jpegapp_close, + .filter = jpegapp_filter, +}; -- To stop receiving notification emails like this one, please contact [email protected]. _______________________________________________ ffmpeg-cvslog mailing list -- [email protected] To unsubscribe send an email to [email protected]
