PR #24722 opened by rnissl
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24722
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24722.patch

This is 9/9 in the GoPro Max review order. It applies on current master by 
itself.

Allow RGB24/BGR24/RGB0/BGR0/RGBA/BGRA input. Convert to 
yuvj444p/yuvj422p/yuvj420p with JPEG or BT.709 full-range matrix.

Preferred review order, each request on its own:

1/9 avutil/log: flush stderr and match Windows warning yellow
2/9 avfilter: add yuvrgb_cuda
3/9 avfilter: add gopromax2_cuda GoPro Max 2 EAC stitch
4/9 avcodec: add mjpeg_nvjpeg NVIDIA JPEG encoder
5/9 avcodec/bsf: add jpegapp to inject JPEG APP segments
6/9 avfilter: add gopromax2 GoPro Max 2 EAC stitch
7/9 avfilter: add gopromax2_opencl GoPro Max 2 EAC stitch
8/9 avfilter: add gopromax_opencl GoPro Max 1 stitch
9/9 avcodec/mjpegenc: encode packed RGB via YUV conversion

After 1-5, mapillary_tools processes GoPro Max 2 files GPU-only in my setup: 
scale_cuda=yuv444p10, yuvrgb_cuda=rgb0, gopromax2_cuda, mjpeg_nvjpeg. jpegapp 
inserts the APP1 segment while encoding, so the JPEG ffmpeg writes needs no 
postprocessing.

After 6, the Max 2 CPU fallback works. After 7, Max 2 via OpenCL works. After 
8, original GoPro Max (Max 1) via OpenCL works.

9/9 is needed when 4/9 cannot be used, for example because no nvjpeg library is 
present, and encoding falls back to the software mjpeg encoder. That encoder 
then receives packed RGB (rgb24). Upstream mjpegenc does not accept it; this 
change converts it to YUV first.

The six patches I sent to ffmpeg-devel on 20 September are obsolete. Once the 
filter requests are merged, I will send one short documentation request that 
only adds the cross-references between these sections.


From 8408ef1ea790591daa12f2befb50b7401f60b49e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Reinhard=20Ni=C3=9Fl?= <[email protected]>
Date: Sun, 20 Sep 2026 21:15:23 +0200
Subject: [PATCH] avcodec/mjpegenc: encode packed RGB via YUV conversion
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Allow RGB24/BGR24/RGB0/BGR0/RGBA/BGRA input. Convert to
yuvj444p/yuvj422p/yuvj420p with JPEG or BT.709 full-range matrix.

Co-authored-by: Cursor <[email protected]>
Signed-off-by: Reinhard Nißl <[email protected]>
---
 Changelog             |   2 +
 doc/encoders.texi     |  17 ++++
 libavcodec/mjpegenc.c | 214 +++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 231 insertions(+), 2 deletions(-)

diff --git a/Changelog b/Changelog
index 1cfa9db1b6..0c6341e570 100644
--- a/Changelog
+++ b/Changelog
@@ -15,6 +15,8 @@ version <next>:
 - H.264 data partitioning support
 - DSD (dsd_msbf) encoder
 
+- jpegapp bitstream filter
+- MJPEG encoder packed RGB input
 
 version 9.0:
 - Extend AMF Color Converter (vf_vpp_amf) HDR capabilities
diff --git a/doc/encoders.texi b/doc/encoders.texi
index a3ce15fcff..b908dd8bd8 100644
--- a/doc/encoders.texi
+++ b/doc/encoders.texi
@@ -1266,8 +1266,25 @@ Use the default huffman tables. This is the default 
strategy.
 Compute and use optimal huffman tables.
 
 @end table
+
+@item quality
+JPEG quality @code{1}--@code{100}. @code{0} (default) keeps @option{-q:v}.
+
+@item matrix
+RGB to YCbCr matrix when the encoder receives packed RGB
+(@code{rgb24}/@code{rgb0}/@code{bgr0}/...). Default @samp{jpeg} is BT.601
+full range (ITU T.871). @samp{bt709} selects BT.709 full range.
+
+@item chroma
+Chroma subsampling for RGB input: @samp{444} (default), @samp{422}, or
+@samp{420}. YUV input still follows @option{-pix_fmt}.
+
 @end table
 
+@example
+-c:v mjpeg -quality 95 -matrix jpeg -chroma 444
+@end example
+
 @anchor{wavpackenc}
 @section wavpack
 
diff --git a/libavcodec/mjpegenc.c b/libavcodec/mjpegenc.c
index b9ebe3d769..ead56df19a 100644
--- a/libavcodec/mjpegenc.c
+++ b/libavcodec/mjpegenc.c
@@ -32,7 +32,11 @@
 
 #include "config_components.h"
 
+#include "libavutil/common.h"
+#include "libavutil/frame.h"
+#include "libavutil/imgutils.h"
 #include "libavutil/mem.h"
+#include "libavutil/pixdesc.h"
 
 #include "avcodec.h"
 #include "codec_internal.h"
@@ -63,9 +67,25 @@ typedef struct MJpegHuffmanCode {
  * Note that when using slice threading only the main thread's
  * MPVEncContext is followed by a MjpegContext; the other threads
  * can access this shared context via MPVEncContext.mjpeg. */
+enum MJPEGColorMatrix {
+    MJPEG_MATRIX_JPEG = 0, /* ITU T.871 / BT.601 full range */
+    MJPEG_MATRIX_BT709,
+};
+
+enum MJPEGChroma {
+    MJPEG_CHROMA_444 = 0,
+    MJPEG_CHROMA_422,
+    MJPEG_CHROMA_420,
+};
+
 typedef struct MJPEGEncContext {
     MPVMainEncContext mpeg;
     MJpegContext   mjpeg;
+    int quality;
+    int color_matrix;
+    int chroma;
+    enum AVPixelFormat src_pix_fmt;
+    AVFrame *yuv_frame;
 } MJPEGEncContext;
 
 static av_cold void init_uni_ac_vlc(const uint8_t huff_size_ac[256],
@@ -303,9 +323,124 @@ static int alloc_huffman(MJPEGEncContext *const m2)
     return 0;
 }
 
+static int mjpeg_pix_fmt_is_rgb(enum AVPixelFormat fmt)
+{
+    switch (fmt) {
+    case AV_PIX_FMT_RGB24:
+    case AV_PIX_FMT_BGR24:
+    case AV_PIX_FMT_RGB0:
+    case AV_PIX_FMT_BGR0:
+    case AV_PIX_FMT_RGBA:
+    case AV_PIX_FMT_BGRA:
+        return 1;
+    default:
+        return 0;
+    }
+}
+
+static enum AVPixelFormat mjpeg_yuv_pix_fmt(int chroma)
+{
+    switch (chroma) {
+    case MJPEG_CHROMA_420:
+        return AV_PIX_FMT_YUVJ420P;
+    case MJPEG_CHROMA_422:
+        return AV_PIX_FMT_YUVJ422P;
+    default:
+        return AV_PIX_FMT_YUVJ444P;
+    }
+}
+
+static void mjpeg_unpack_rgb(enum AVPixelFormat fmt, const uint8_t *p,
+                             int *r, int *g, int *b)
+{
+    switch (fmt) {
+    case AV_PIX_FMT_BGR24:
+    case AV_PIX_FMT_BGR0:
+    case AV_PIX_FMT_BGRA:
+        *b = p[0];
+        *g = p[1];
+        *r = p[2];
+        break;
+    default:
+        *r = p[0];
+        *g = p[1];
+        *b = p[2];
+        break;
+    }
+}
+
+static void mjpeg_rgb_to_ycbcr(int matrix, int r, int g, int b,
+                               int *y, int *cb, int *cr)
+{
+    if (matrix == MJPEG_MATRIX_BT709) {
+        *y  = ( 54 * r + 183 * g +  18 * b + 128) >> 8;
+        *cb = ((-30 * r -  98 * g + 128 * b + 128) >> 8) + 128;
+        *cr = ((128 * r - 116 * g -  12 * b + 128) >> 8) + 128;
+    } else {
+        /* JPEG / BT.601 full range (ITU T.871) */
+        *y  = ( 77 * r + 150 * g +  29 * b + 128) >> 8;
+        *cb = ((-43 * r -  85 * g + 128 * b + 128) >> 8) + 128;
+        *cr = ((128 * r - 107 * g -  21 * b + 128) >> 8) + 128;
+    }
+    *y  = av_clip_uint8(*y);
+    *cb = av_clip_uint8(*cb);
+    *cr = av_clip_uint8(*cr);
+}
+
+static int mjpeg_convert_rgb_frame(AVCodecContext *avctx, AVFrame *dst,
+                                   const AVFrame *src)
+{
+    MJPEGEncContext *const m2 = avctx->priv_data;
+    const int width  = src->width;
+    const int height = src->height;
+    const int step   = (src->format == AV_PIX_FMT_RGB24 ||
+                        src->format == AV_PIX_FMT_BGR24) ? 3 : 4;
+    const int chroma = m2->chroma;
+    int x, y;
+
+    for (y = 0; y < height; y++) {
+        const uint8_t *sp = src->data[0] + y * src->linesize[0];
+        uint8_t *yp = dst->data[0] + y * dst->linesize[0];
+        uint8_t *up, *vp;
+        int uv_y = (chroma == MJPEG_CHROMA_420) ? (y >> 1) : y;
+
+        up = dst->data[1] + uv_y * dst->linesize[1];
+        vp = dst->data[2] + uv_y * dst->linesize[2];
+
+        for (x = 0; x < width; x++) {
+            int r, g, b, Y, Cb, Cr;
+            mjpeg_unpack_rgb(src->format, sp + x * step, &r, &g, &b);
+            mjpeg_rgb_to_ycbcr(m2->color_matrix, r, g, b, &Y, &Cb, &Cr);
+            yp[x] = Y;
+
+            if (chroma == MJPEG_CHROMA_444) {
+                up[x] = Cb;
+                vp[x] = Cr;
+            } else if (chroma == MJPEG_CHROMA_422) {
+                if (!(x & 1)) {
+                    up[x >> 1] = Cb;
+                    vp[x >> 1] = Cr;
+                } else {
+                    up[x >> 1] = (up[x >> 1] + Cb + 1) >> 1;
+                    vp[x >> 1] = (vp[x >> 1] + Cr + 1) >> 1;
+                }
+            } else if (!((x | y) & 1)) {
+                up[x >> 1] = Cb;
+                vp[x >> 1] = Cr;
+            } else if (!(y & 1)) {
+                up[x >> 1] = (up[x >> 1] + Cb + 1) >> 1;
+                vp[x >> 1] = (vp[x >> 1] + Cr + 1) >> 1;
+            }
+        }
+    }
+
+    return 0;
+}
+
 static av_cold int mjpeg_encode_close(AVCodecContext *avctx)
 {
     MJPEGEncContext *const mjpeg = avctx->priv_data;
+    av_frame_free(&mjpeg->yuv_frame);
     av_freep(&mjpeg->mjpeg.huff_buffer);
     ff_mpv_encode_end(avctx);
     return 0;
@@ -539,6 +674,29 @@ static av_cold int mjpeg_encode_init(AVCodecContext *avctx)
     m2->mpeg.encode_picture_header = mjpeg_amv_encode_picture_header;
     // May be overridden below
     s->encode_mb                   = mjpeg_encode_mb;
+    m2->src_pix_fmt                = avctx->pix_fmt;
+
+    if (m2->quality > 0) {
+        int q = m2->quality >= 100 ? 1
+                                   : av_clip((100 - m2->quality + 2) / 3, 1, 
31);
+        avctx->flags |= AV_CODEC_FLAG_QSCALE;
+        avctx->global_quality = q * FF_QP2LAMBDA;
+    }
+
+    if (avctx->color_range == AVCOL_RANGE_UNSPECIFIED)
+        avctx->color_range = AVCOL_RANGE_JPEG;
+
+    if (mjpeg_pix_fmt_is_rgb(avctx->pix_fmt)) {
+        avctx->pix_fmt = mjpeg_yuv_pix_fmt(m2->chroma);
+        if (avctx->colorspace == AVCOL_SPC_UNSPECIFIED)
+            avctx->colorspace = m2->color_matrix == MJPEG_MATRIX_BT709
+                                ? AVCOL_SPC_BT709 : AVCOL_SPC_BT470BG;
+        av_log(avctx, AV_LOG_VERBOSE,
+               "mjpeg RGB %s -> %s matrix=%s\n",
+               av_get_pix_fmt_name(m2->src_pix_fmt),
+               av_get_pix_fmt_name(avctx->pix_fmt),
+               m2->color_matrix == MJPEG_MATRIX_BT709 ? "bt709" : "jpeg");
+    }
 
     if (s->mpv_flags & FF_MPV_FLAG_QP_RD) {
         // Used to produce garbage with MJPEG.
@@ -593,6 +751,20 @@ static av_cold int mjpeg_encode_init(AVCodecContext *avctx)
     if (ret < 0)
         return ret;
 
+    if (mjpeg_pix_fmt_is_rgb(m2->src_pix_fmt)) {
+        m2->yuv_frame = av_frame_alloc();
+        if (!m2->yuv_frame)
+            return AVERROR(ENOMEM);
+        m2->yuv_frame->format = avctx->pix_fmt;
+        m2->yuv_frame->width  = avctx->width;
+        m2->yuv_frame->height = avctx->height;
+        m2->yuv_frame->color_range = AVCOL_RANGE_JPEG;
+        m2->yuv_frame->colorspace  = avctx->colorspace;
+        ret = av_frame_get_buffer(m2->yuv_frame, 32);
+        if (ret < 0)
+            return ret;
+    }
+
     // Buffers start out empty.
     m->huff_ncode = 0;
 
@@ -608,6 +780,31 @@ static av_cold int mjpeg_encode_init(AVCodecContext *avctx)
     return 0;
 }
 
+#if CONFIG_MJPEG_ENCODER
+static int mjpeg_encode_frame(AVCodecContext *avctx, AVPacket *pkt,
+                              const AVFrame *frame, int *got_packet)
+{
+    MJPEGEncContext *const m2 = avctx->priv_data;
+    const AVFrame *src = frame;
+    int ret;
+
+    if (frame && m2->yuv_frame) {
+        ret = av_frame_copy_props(m2->yuv_frame, frame);
+        if (ret < 0)
+            return ret;
+        m2->yuv_frame->color_range = AVCOL_RANGE_JPEG;
+        m2->yuv_frame->colorspace  = m2->color_matrix == MJPEG_MATRIX_BT709
+                                     ? AVCOL_SPC_BT709 : AVCOL_SPC_BT470BG;
+        ret = mjpeg_convert_rgb_frame(avctx, m2->yuv_frame, frame);
+        if (ret < 0)
+            return ret;
+        src = m2->yuv_frame;
+    }
+
+    return ff_mpv_encode_picture(avctx, pkt, src, got_packet);
+}
+#endif
+
 #if CONFIG_AMV_ENCODER
 static int amv_encode_picture(AVCodecContext *avctx, AVPacket *pkt,
                               const AVFrame *pic_arg, int *got_packet)
@@ -642,6 +839,7 @@ static int amv_encode_picture(AVCodecContext *avctx, 
AVPacket *pkt,
 #endif
 
 #define OFFSET(x) offsetof(MJPEGEncContext, mjpeg.x)
+#define OFFSET_PRIV(x) offsetof(MJPEGEncContext, x)
 #define VE AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_ENCODING_PARAM
 static const AVOption options[] = {
 #define AMV_OPTIONS_OFFSET 4
@@ -649,6 +847,15 @@ static const AVOption options[] = {
     { "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 },
+{ "quality", "JPEG quality 1-100 (0 = use -q:v). 95 ≈ -q:v 2", 
OFFSET_PRIV(quality), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 100, VE },
+{ "matrix", "RGB→YCbCr matrix (RGB input only)", OFFSET_PRIV(color_matrix), 
AV_OPT_TYPE_INT, { .i64 = MJPEG_MATRIX_JPEG }, 0, MJPEG_MATRIX_BT709, VE, .unit 
= "matrix" },
+    { "jpeg",  "BT.601 full range (ITU T.871)", 0, AV_OPT_TYPE_CONST, { .i64 = 
MJPEG_MATRIX_JPEG },  0, 0, VE, .unit = "matrix" },
+    { "bt601", "Same as jpeg",                   0, AV_OPT_TYPE_CONST, { .i64 
= MJPEG_MATRIX_JPEG },  0, 0, VE, .unit = "matrix" },
+    { "bt709", "BT.709 full range",              0, AV_OPT_TYPE_CONST, { .i64 
= MJPEG_MATRIX_BT709 }, 0, 0, VE, .unit = "matrix" },
+{ "chroma", "Chroma subsampling for RGB input", OFFSET_PRIV(chroma), 
AV_OPT_TYPE_INT, { .i64 = MJPEG_CHROMA_444 }, 0, MJPEG_CHROMA_420, VE, .unit = 
"chroma" },
+    { "444", "4:4:4 (no chroma subsample)", 0, AV_OPT_TYPE_CONST, { .i64 = 
MJPEG_CHROMA_444 }, 0, 0, VE, .unit = "chroma" },
+    { "422", "4:2:2",                       0, AV_OPT_TYPE_CONST, { .i64 = 
MJPEG_CHROMA_422 }, 0, 0, VE, .unit = "chroma" },
+    { "420", "4:2:0",                       0, AV_OPT_TYPE_CONST, { .i64 = 
MJPEG_CHROMA_420 }, 0, 0, VE, .unit = "chroma" },
 FF_MPV_COMMON_OPTS
 { NULL},
 };
@@ -688,14 +895,17 @@ const FFCodec ff_mjpeg_encoder = {
     .p.id           = AV_CODEC_ID_MJPEG,
     .priv_data_size = sizeof(MJPEGEncContext),
     .init           = mjpeg_encode_init,
-    FF_CODEC_ENCODE_CB(ff_mpv_encode_picture),
+    FF_CODEC_ENCODE_CB(mjpeg_encode_frame),
     .close          = mjpeg_encode_close,
     .p.capabilities = AV_CODEC_CAP_DR1 |
                       AV_CODEC_CAP_SLICE_THREADS | AV_CODEC_CAP_FRAME_THREADS |
                       AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE,
     .caps_internal  = FF_CODEC_CAP_INIT_CLEANUP | FF_CODEC_CAP_ICC_PROFILES,
     CODEC_PIXFMTS(AV_PIX_FMT_YUVJ420P, AV_PIX_FMT_YUVJ422P, 
AV_PIX_FMT_YUVJ444P,
-                  AV_PIX_FMT_YUV420P,  AV_PIX_FMT_YUV422P,  
AV_PIX_FMT_YUV444P),
+                  AV_PIX_FMT_YUV420P,  AV_PIX_FMT_YUV422P,  AV_PIX_FMT_YUV444P,
+                  AV_PIX_FMT_RGB24, AV_PIX_FMT_BGR24,
+                  AV_PIX_FMT_RGB0,  AV_PIX_FMT_BGR0,
+                  AV_PIX_FMT_RGBA,  AV_PIX_FMT_BGRA),
     .p.priv_class   = &mjpeg_class,
     .p.profiles     = NULL_IF_CONFIG_SMALL(ff_mjpeg_profiles),
     .get_supported_config = mjpeg_get_supported_config,
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to