PR #25158 opened by Baptiste Coudurier (bcoudurier)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25158
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25158.patch

With slice threading, the MJPEG encoder writes a restart marker after each 
macroblock row. The row of the marker was derived from the macroblock position: 
the previous row when called at the start of a new row (`mb_x == 0`), the 
current row at the end of a slice (`mb_x == mb_width - 1`).

With a single macroblock column (width <= 16) both cases have `mb_x == 0`, so 
the end of a slice wrote the marker of the previous row (RST7 for the first 
slice) and an extra marker after the last row. libjpeg rejects these files with 
"Corrupt JPEG data: found marker 0xd7 instead of RST0".

The row of the last coded macroblock is now passed explicitly from 
`write_slice_end()` to `ff_mjpeg_encode_stuffing()`.

Tested with 300 encodes (4:2:0/4:2:2/4:4:4, widths 8-48, heights 16-136, 
2/3/4/8 slices), checking the RST sequence (RST0..RST7 cycling, `mb_height - 1` 
markers) and decoding with `djpeg`: 96 broken before, all of them one 
macroblock wide, none after. FATE passes for all mpegvideo based encoders (849 
tests), no reference changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)



From 46bd529110fbada2d4f9a159b6f661905bd8fdf6 Mon Sep 17 00:00:00 2001
From: Baptiste Coudurier <[email protected]>
Date: Sat, 17 Oct 2026 11:17:16 -0700
Subject: [PATCH] avcodec/mjpegenc: fix restart markers for pictures one
 macroblock wide

The row of the restart marker was derived from the macroblock position,
as the previous row when called at the start of a new row (mb_x == 0)
and the current row at the end of a slice (mb_x == mb_width - 1).

With a single macroblock column both cases have mb_x == 0, so the end of
a slice wrote the marker of the previous row, and a marker after the
last row. Pass the row of the last coded macroblock explicitly.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 libavcodec/mjpegenc.c      |  8 ++++----
 libavcodec/mjpegenc.h      |  2 +-
 libavcodec/mpegvideo_enc.c | 11 +++++++----
 3 files changed, 12 insertions(+), 9 deletions(-)

diff --git a/libavcodec/mjpegenc.c b/libavcodec/mjpegenc.c
index 4305f11774..4bdeef8b66 100644
--- a/libavcodec/mjpegenc.c
+++ b/libavcodec/mjpegenc.c
@@ -233,13 +233,13 @@ static void mjpeg_build_optimal_huffman(MJpegContext *m)
  * Header + values + stuffing.
  *
  * @param s The MPVEncContext.
+ * @param last_mb_y Row of the last coded macroblock.
  * @return int Error code, 0 if successful.
  */
-int ff_mjpeg_encode_stuffing(MPVEncContext *const s)
+int ff_mjpeg_encode_stuffing(MPVEncContext *const s, int last_mb_y)
 {
     MJpegContext *const m = s->mjpeg_ctx;
     PutBitContext *pbc = &s->pb;
-    int mb_y = s->c.mb_y - !s->c.mb_x;
     int ret;
 
 #if CONFIG_MJPEG_ENCODER
@@ -273,8 +273,8 @@ int ff_mjpeg_encode_stuffing(MPVEncContext *const s)
 
     ff_mjpeg_escape_FF(pbc, s->esc_pos);
 
-    if (s->c.slice_context_count > 1 && mb_y < s->c.mb_height - 1)
-        put_marker(pbc, RST0 + (mb_y&7));
+    if (s->c.slice_context_count > 1 && last_mb_y < s->c.mb_height - 1)
+        put_marker(pbc, RST0 + (last_mb_y & 7));
     s->esc_pos = put_bytes_count(pbc, 0);
 
 fail:
diff --git a/libavcodec/mjpegenc.h b/libavcodec/mjpegenc.h
index 865f13f50e..0db364e648 100644
--- a/libavcodec/mjpegenc.h
+++ b/libavcodec/mjpegenc.h
@@ -97,6 +97,6 @@ static inline void put_marker(PutBitContext *p, enum 
JpegMarker code)
 
 typedef struct MPVEncContext MPVEncContext;
 
-int ff_mjpeg_encode_stuffing(MPVEncContext *s);
+int ff_mjpeg_encode_stuffing(MPVEncContext *s, int last_mb_y);
 
 #endif /* AVCODEC_MJPEGENC_H */
diff --git a/libavcodec/mpegvideo_enc.c b/libavcodec/mpegvideo_enc.c
index 08d7aa59b7..71f981b04e 100644
--- a/libavcodec/mpegvideo_enc.c
+++ b/libavcodec/mpegvideo_enc.c
@@ -2866,7 +2866,10 @@ static int mb_var_thread(AVCodecContext *c, void *arg){
     return 0;
 }
 
-static void write_slice_end(MPVEncContext *const s)
+/**
+ * @param last_mb_y row of the last coded macroblock
+ */
+static void write_slice_end(MPVEncContext *const s, int last_mb_y)
 {
     if (CONFIG_MPEG4_ENCODER && s->c.codec_id == AV_CODEC_ID_MPEG4) {
         if (s->partitioned_frame)
@@ -2875,7 +2878,7 @@ static void write_slice_end(MPVEncContext *const s)
         ff_mpeg4_stuffing(&s->pb);
     } else if ((CONFIG_MJPEG_ENCODER || CONFIG_AMV_ENCODER) &&
                s->c.out_format == FMT_MJPEG) {
-        ff_mjpeg_encode_stuffing(s);
+        ff_mjpeg_encode_stuffing(s, last_mb_y);
     } else if (CONFIG_SPEEDHQ_ENCODER && s->c.out_format == FMT_SPEEDHQ) {
         ff_speedhq_end_slice(s);
     }
@@ -3086,7 +3089,7 @@ static int encode_thread(AVCodecContext *c, void *arg){
 
                 if(is_gob_start){
                     if (s->c.start_mb_y != mb_y || mb_x != 0) {
-                        write_slice_end(s);
+                        write_slice_end(s, mb_y - !mb_x);
 
                         if (CONFIG_MPEG4_ENCODER && s->c.codec_id == 
AV_CODEC_ID_MPEG4 && s->partitioned_frame)
                             ff_mpeg4_init_partitions(s);
@@ -3590,7 +3593,7 @@ static int encode_thread(AVCodecContext *c, void *arg){
         ff_msmpeg4_encode_ext_header(s);
 #endif
 
-    write_slice_end(s);
+    write_slice_end(s, s->c.mb_y);
 
     return 0;
 }
-- 
2.54.0

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

Reply via email to