Inject APP0..APP15 from a length-prefixed sidecar into each MJPEG packet, after SOI. Length 0 leaves the packet unchanged. An APP1-only payload keeps encoder APP0 (typical JFIF).
Co-authored-by: Cursor <[email protected]> Signed-off-by: Reinhard Nißl <[email protected]> --- Changelog | 1 + doc/bitstream_filters.texi | 33 ++++ libavcodec/bitstream_filters.c | 1 + libavcodec/bsf/Makefile | 1 + libavcodec/bsf/jpegapp.c | 269 +++++++++++++++++++++++++++++++++ 5 files changed, 305 insertions(+) create mode 100644 libavcodec/bsf/jpegapp.c diff --git a/Changelog b/Changelog index 1cfa9db1b6..ed98fe8580 100644 --- a/Changelog +++ b/Changelog @@ -15,6 +15,7 @@ version <next>: - H.264 data partitioning support - DSD (dsd_msbf) encoder +- jpegapp bitstream filter version 9.0: - Extend AMF Color Converter (vf_vpp_amf) HDR capabilities diff --git a/doc/bitstream_filters.texi b/doc/bitstream_filters.texi index a00757d315..15e3479786 100644 --- a/doc/bitstream_filters.texi +++ b/doc/bitstream_filters.texi @@ -612,6 +612,39 @@ 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. Payload @code{0} 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 already has an APP0 after +SOI (typical JFIF), that encoder APP0 is dropped so the sidecar can +replace it. 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. + +@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..119b3f761a --- /dev/null +++ b/libavcodec/bsf/jpegapp.c @@ -0,0 +1,269 @@ +/* + * 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). + * Length 0 leaves the packet unchanged. + * + * After SOI the payload is inserted. If it starts with APP0 and the + * packet already has an APP0 (typical JFIF), that encoder APP0 is + * dropped so the sidecar can replace it. An APP1-only payload keeps + * the encoder APP0: SOI, APP1, APP0, DQT, ... + */ + +#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; + 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'\n", s->filename); + return err; + } + return 0; +} + +static void jpegapp_close(AVBSFContext *ctx) +{ + JPEGAppContext *s = ctx->priv_data; + + if (s->file) { + if (getc(s->file) != EOF) + av_log(ctx, AV_LOG_VERBOSE, + "jpegapp: unused sidecar data after %"PRId64" packets\n", + s->packet_index); + 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; + + got = fread(hdr, 1, 4, s->file); + if (got == 0 && feof(s->file)) { + av_log(ctx, AV_LOG_ERROR, + "jpegapp: no sidecar record for packet %"PRId64"\n", + s->packet_index); + return AVERROR_EOF; + } + if (got != 4) { + 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) { + got = fread(s->buf, 1, n, s->file); + if (got != n) { + 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 = jpegapp_read_record(ctx, &rec_size); + if (ret < 0) + 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) { + 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); + s->packet_index++; + av_packet_free(&in); + return 0; + +fail: + if (ret >= 0) + s->packet_index++; + 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, +}; -- 2.43.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
