Commit ebbc9b6600 for ffmpeg
commit ebbc9b6600d36608f523d0d97fcb4c52322870e5
Author: Niklas Haas <git@haasn.dev>
Date: Fri Aug 28 16:49:27 2026 +0200
avcodec/mjpegenc: add -xmp_gain_map option
Controls whether or not to serialize the gain map as an XMP blob in
addition to an ISO blob. Defaults to -1, meaning to serialize the
gain map only if compatible.
Signed-off-by: Niklas Haas <git@haasn.dev>
diff --git a/libavcodec/mjpegenc.c b/libavcodec/mjpegenc.c
index b9ebe3d769..8dc3217acf 100644
--- a/libavcodec/mjpegenc.c
+++ b/libavcodec/mjpegenc.c
@@ -644,11 +644,12 @@ static int amv_encode_picture(AVCodecContext *avctx, AVPacket *pkt,
#define OFFSET(x) offsetof(MJPEGEncContext, mjpeg.x)
#define VE AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_ENCODING_PARAM
static const AVOption options[] = {
-#define AMV_OPTIONS_OFFSET 4
+#define AMV_OPTIONS_OFFSET 5
{ "huffman", "Huffman table strategy", OFFSET(huffman), AV_OPT_TYPE_INT, { .i64 = HUFFMAN_TABLE_OPTIMAL }, 0, NB_HUFFMAN_TABLE_OPTION - 1, VE, .unit = "huffman" },
{ "default", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = HUFFMAN_TABLE_DEFAULT }, INT_MIN, INT_MAX, VE, .unit = "huffman" },
{ "optimal", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = HUFFMAN_TABLE_OPTIMAL }, INT_MIN, INT_MAX, VE, .unit = "huffman" },
{ "force_duplicated_matrix", "Always write luma and chroma matrix for mjpeg, useful for rtp streaming.", OFFSET(force_duplicated_matrix), AV_OPT_TYPE_BOOL, {.i64 = 0 }, 0, 1, VE },
+{ "xmp_gain_map", "Write Ultra HDR gain map metadata as an XMP packet", OFFSET(xmp_gain_map), AV_OPT_TYPE_BOOL, { .i64 = -1 }, -1, 1, VE },
FF_MPV_COMMON_OPTS
{ NULL},
};
diff --git a/libavcodec/mjpegenc.h b/libavcodec/mjpegenc.h
index 92feed28b4..865f13f50e 100644
--- a/libavcodec/mjpegenc.h
+++ b/libavcodec/mjpegenc.h
@@ -45,6 +45,9 @@ typedef struct MJpegContext {
int huffman;
/* Force duplication of mjpeg matrices, useful for rtp streaming */
int force_duplicated_matrix;
+ /* Tristate controlling the Ultra HDR gain map XMP packet */
+ int xmp_gain_map;
+ int xmp_warned;
//FIXME use array [3] instead of lumi / chroma, for easier addressing
uint8_t huff_size_dc_luminance[12]; ///< DC luminance Huffman table size.
uint16_t huff_code_dc_luminance[12]; ///< DC luminance Huffman table codes.
diff --git a/libavcodec/mjpegenc_common.c b/libavcodec/mjpegenc_common.c
index a4464c3f90..235a46799a 100644
--- a/libavcodec/mjpegenc_common.c
+++ b/libavcodec/mjpegenc_common.c
@@ -160,8 +160,14 @@ int ff_mjpeg_add_icc_profile_size(AVCodecContext *avctx, const AVFrame *frame,
return 0;
}
-int ff_mjpeg_add_gain_map_size(AVCodecContext *avctx, const AVFrame *frame,
- size_t *max_pkt_size)
+static int want_gain_map_xmp(const struct MJpegContext *m,
+ const AVGainMapParams *p)
+{
+ return m && m->xmp_gain_map && ff_gain_map_params_check_xmp(p);
+}
+
+int ff_mjpeg_add_gain_map_size(AVCodecContext *avctx, struct MJpegContext *m,
+ const AVFrame *frame, size_t *max_pkt_size)
{
if (avctx->codec_id != AV_CODEC_ID_MJPEG)
return 0;
@@ -176,8 +182,20 @@ int ff_mjpeg_add_gain_map_size(AVCodecContext *avctx, const AVFrame *frame,
if (ret < 0)
return ret;
- size_t app2_size = 4 + sizeof(AV_ISO21496_IDENTIFIER) + FF_GAIN_MAP_MAX_PAYLOAD_SIZE;
- size_t new_pkt_size = *max_pkt_size + app2_size;
+ size_t extra_size = 4 + sizeof(AV_ISO21496_IDENTIFIER) + FF_GAIN_MAP_MAX_PAYLOAD_SIZE;
+ if (m && m->xmp_gain_map) {
+ if (!ff_gain_map_params_check_xmp(params)) {
+ const int level = m->xmp_gain_map > 0 ? AV_LOG_ERROR : AV_LOG_WARNING;
+ av_log_once(avctx, level, AV_LOG_VERBOSE, &m->xmp_warned,
+ "Gain map metadata is incompatible with Ultra HDR XMP\n");
+ if (m->xmp_gain_map > 0)
+ return AVERROR(ENOTSUP);
+ } else {
+ extra_size += 4 + sizeof(FF_GAIN_MAP_XMP_IDENT) + FF_GAIN_MAP_XMP_MAX_LEN;
+ }
+ }
+
+ size_t new_pkt_size = *max_pkt_size + extra_size;
if (new_pkt_size < *max_pkt_size) /* overflow */
return AVERROR_INVALIDDATA;
*max_pkt_size = new_pkt_size;
@@ -185,7 +203,7 @@ int ff_mjpeg_add_gain_map_size(AVCodecContext *avctx, const AVFrame *frame,
}
static void jpeg_put_comments(AVCodecContext *avctx, PutBitContext *p,
- const AVFrame *frame)
+ const AVFrame *frame, const struct MJpegContext *m)
{
const AVFrameSideData *sd = NULL;
int size;
@@ -246,9 +264,27 @@ static void jpeg_put_comments(AVCodecContext *avctx, PutBitContext *p,
av_assert1(!remaining);
}
- /* ISO 21496-1 gain map metadata */
+ /* Gain map metadata; the ISO 21496-1 blob is written directly after the
+ * XMP packet, as recommended by the Ultra HDR specification */
sd = avctx->codec_id == AV_CODEC_ID_MJPEG ?
av_frame_get_side_data(frame, AV_FRAME_DATA_GAIN_MAP_PARAMS) : NULL;
+
+ const AVGainMapParams *gmp = sd ? (const AVGainMapParams *) sd->data : NULL;
+ if (sd && sd->size >= sizeof(AVGainMapParams) && want_gain_map_xmp(m, gmp)) {
+ put_marker(p, APP1);
+ flush_put_bits(p);
+ ptr = put_bits_ptr(p);
+ put_bits(p, 16, 0); /* patched later */
+ ff_put_string(p, FF_GAIN_MAP_XMP_IDENT, 1);
+ flush_put_bits(p);
+
+ /* pre-validated by ff_mjpeg_add_gain_map_size() */
+ size = ff_gain_map_params_to_xmp(gmp, put_bits_ptr(p));
+ av_assert0(size >= 0);
+ skip_put_bytes(p, size);
+ AV_WB16(ptr, 2 + sizeof(FF_GAIN_MAP_XMP_IDENT) + size);
+ }
+
if (sd && sd->size >= sizeof(AVGainMapParams)) {
put_marker(p, APP2);
flush_put_bits(p);
@@ -258,8 +294,7 @@ static void jpeg_put_comments(AVCodecContext *avctx, PutBitContext *p,
flush_put_bits(p);
/* pre-validated by ff_mjpeg_add_gain_map_size() */
- const AVGainMapParams *params = (const AVGainMapParams *) sd->data;
- size = ff_gain_map_params_to_iso21496(params, put_bits_ptr(p));
+ size = ff_gain_map_params_to_iso21496(gmp, put_bits_ptr(p));
av_assert0(size >= 0);
skip_put_bytes(p, size);
AV_WB16(ptr, 2 + sizeof(AV_ISO21496_IDENTIFIER) + size);
@@ -336,7 +371,7 @@ void ff_mjpeg_encode_picture_header(AVCodecContext *avctx, PutBitContext *pb,
if (avctx->codec_id == AV_CODEC_ID_AMV)
return;
- jpeg_put_comments(avctx, pb, frame);
+ jpeg_put_comments(avctx, pb, frame, m);
chroma_matrix = !lossless && !!memcmp(luma_intra_matrix,
chroma_intra_matrix,
diff --git a/libavcodec/mjpegenc_common.h b/libavcodec/mjpegenc_common.h
index 4d1e2a5477..20e3902c03 100644
--- a/libavcodec/mjpegenc_common.h
+++ b/libavcodec/mjpegenc_common.h
@@ -30,8 +30,8 @@ struct MJpegContext;
int ff_mjpeg_add_icc_profile_size(AVCodecContext *avctx, const AVFrame *frame,
size_t *max_pkt_size);
-int ff_mjpeg_add_gain_map_size(AVCodecContext *avctx, const AVFrame *frame,
- size_t *max_pkt_size);
+int ff_mjpeg_add_gain_map_size(AVCodecContext *avctx, struct MJpegContext *m,
+ const AVFrame *frame, size_t *max_pkt_size);
void ff_mjpeg_encode_picture_header(AVCodecContext *avctx, PutBitContext *pb,
const AVFrame *frame, const struct MJpegContext *m,
const uint8_t intra_matrix_permutation[64],
diff --git a/libavcodec/mpegvideo_enc.c b/libavcodec/mpegvideo_enc.c
index 592334473a..3908628504 100644
--- a/libavcodec/mpegvideo_enc.c
+++ b/libavcodec/mpegvideo_enc.c
@@ -1939,7 +1939,7 @@ int ff_mpv_encode_picture(AVCodecContext *avctx, AVPacket *pkt,
ret = ff_mjpeg_add_icc_profile_size(avctx, s->new_pic, &pkt_size);
if (ret < 0)
return ret;
- ret = ff_mjpeg_add_gain_map_size(avctx, s->new_pic, &pkt_size);
+ ret = ff_mjpeg_add_gain_map_size(avctx, s->mjpeg_ctx, s->new_pic, &pkt_size);
if (ret < 0)
return ret;
}
diff --git a/tests/ref/fate/jpeg-mpf-gainmap b/tests/ref/fate/jpeg-mpf-gainmap
index 2fee05c244..999852b117 100644
--- a/tests/ref/fate/jpeg-mpf-gainmap
+++ b/tests/ref/fate/jpeg-mpf-gainmap
@@ -1 +1 @@
-50dc4be69672174d4ff3ca8dbdaaa9d3
+e7c134ebd17c6d040e8eeaaada4633f2