[vlc-commits] [Git][videolan/vlc][master] 2 commits: transcode: fix the encoded end-of-stream picture order

Steve Lhomme (@robUx4) gitlab at videolan.org
Mon Aug 17 06:00:50 UTC 2026



Steve Lhomme pushed to branch master at VideoLAN / VLC


Commits:
7573f948 by Alexandre Janniaux at 2026-08-17T05:46:12+00:00
transcode: fix the encoded end-of-stream picture order

The drained pictures and those that were outputs right before drained
but not already sent over were inverted when sent to the downstream
stream output component. It put the end of the stream ahead of the
blocks that were before it.

Regression from d8c883e273f333d7e2d8c2c2df1e35b280021df5.

- - - - -
a1062792 by Alexandre Janniaux at 2026-08-17T05:46:12+00:00
test: transcode: check the output order on drain

The previous commit fixed the regression from commit
d8c883e273f333d7e2d8c2c2df1e35b280021df5 which made drained video frames
being mixed up ahead of the frames coming right before.

This new test stamps the frames and check that everything is coming in
order to avoid this issue.

To reproduce the issue, the test first generates regular frames and then
makes the transcode drain, leading to the held and encoder drain queues.
The decoder releases the pictures it was holding, which are encoded and
left pending in the transcode output, and the encoder then releases the
frames it was holding for itself when it gets drained in turn.

seq:   0  1  2  3 | 4  5  6  7  8 | 9  10 11
        --------     -----------    --------
         regular     held frames    drained frames
         frames

To ensure this order, the frame sequence number and test stage are saved
into each picture. This test heavily relies on the fact that stream
output pipeline is processed synchronously by the input and not queued
at the input level.

- - - - -


4 changed files:

- modules/stream_out/transcode/video.c
- test/modules/stream_out/transcode.c
- test/modules/stream_out/transcode.h
- test/modules/stream_out/transcode_scenarios.c


Changes:

=====================================
modules/stream_out/transcode/video.c
=====================================
@@ -573,11 +573,14 @@ int transcode_video_process( sout_stream_t *p_stream, sout_stream_id_sys_t *id,
     if( id->encoder == NULL )
         return VLC_SUCCESS;
 
+    /* drained blocks must be appended after already output (pending)
+     * blocks. */
+    block_t *drained = NULL;
     vlc_fifo_Lock( id->output_fifo );
     if( unlikely( !id->b_error && in == NULL ) && transcode_encoder_opened( id->encoder ) )
     {
         msg_Dbg( p_stream, "Draining thread and waiting for that");
-        if( transcode_encoder_drain( id->encoder, out ) == VLC_SUCCESS )
+        if (transcode_encoder_drain(id->encoder, &drained) == VLC_SUCCESS)
             msg_Dbg( p_stream, "Draining done");
         else
             msg_Warn( p_stream, "Draining failed");
@@ -587,9 +590,14 @@ int transcode_video_process( sout_stream_t *p_stream, sout_stream_id_sys_t *id,
     {
         vlc_frame_t *pendings = vlc_fifo_DequeueAllUnlocked( id->output_fifo );
         block_ChainAppend(out, pendings);
+        block_ChainAppend(out, drained);
+        drained = NULL;
     }
     vlc_fifo_Unlock( id->output_fifo );
 
+    if (drained != NULL)
+        block_ChainRelease(drained);
+
     if( b_eos )
         tag_last_block_with_flag( out, BLOCK_FLAG_END_OF_SEQUENCE );
 


=====================================
test/modules/stream_out/transcode.c
=====================================
@@ -1,7 +1,7 @@
 /*****************************************************************************
  * transcode.c: test for transcoding pipeline
  *****************************************************************************
- * Copyright (C) 2021 VideoLabs
+ * Copyright (C) 2021-2026 VideoLabs
  *
  * Author: Alexandre Janniaux <ajanni at videolabs.io>
  *
@@ -78,7 +78,14 @@ static int OpenDecoderDevice(
 static int DecoderDecode(decoder_t *dec, vlc_frame_t *frame)
 {
     if (frame == NULL)
+    {
+        struct transcode_scenario *drained =
+            &transcode_scenarios[current_scenario];
+
+        if (drained->decoder_drain != NULL)
+            return drained->decoder_drain(dec);
         return VLC_SUCCESS;
+    }
 
     const picture_resource_t resource = {
         .p_sys = NULL,
@@ -167,15 +174,21 @@ static int OpenConverter(filter_t *filter)
 
 static vlc_frame_t *EncodeVideo(encoder_t *enc, picture_t *pic)
 {
+    struct transcode_scenario *scenario = &transcode_scenarios[current_scenario];
+
     if (pic == NULL)
+    {
+        if (scenario->encoder_drain != NULL)
+            return scenario->encoder_drain(enc);
         return NULL;
+    }
 
     assert(pic->format.i_chroma == enc->fmt_in.video.i_chroma);
-    vlc_frame_t *frame = vlc_frame_Alloc(4);
+    vlc_frame_t *frame = vlc_frame_Alloc(sizeof(struct test_output));
+    assert(frame != NULL);
 
-    struct transcode_scenario *scenario = &transcode_scenarios[current_scenario];
     if (scenario->encoder_encode != NULL)
-        scenario->encoder_encode(enc, pic);
+        scenario->encoder_encode(enc, pic, frame);
     return frame;
 }
 


=====================================
test/modules/stream_out/transcode.h
=====================================
@@ -1,7 +1,7 @@
 /*****************************************************************************
  * transcode.h: test for transcoding pipeline
  *****************************************************************************
- * Copyright (C) 2021 VideoLabs
+ * Copyright (C) 2021-2026 VideoLabs
  *
  * Author: Alexandre Janniaux <ajanni at videolabs.io>
  *
@@ -25,14 +25,34 @@
 #define TEST_FLAG_CONVERTER 0x01
 #define TEST_FLAG_FILTER 0x02
 
+/* Which stage a picture was released from, so that the stream output can
+ * tell the frames pending in the encoder output from the frames that the
+ * encoder only released when it got drained. */
+enum test_output_origin
+{
+    TEST_OUTPUT_FROM_DECODE,
+    TEST_OUTPUT_FROM_DECODER_DRAIN,
+    TEST_OUTPUT_FROM_ENCODER_DRAIN,
+};
+
+/* Stamp the decoder puts on every picture it releases, in picture_t.p_sys,
+ * and that the encoder copies as-is into the frame it produces from it. */
+struct test_output
+{
+    uint32_t seq;
+    enum test_output_origin origin;
+};
+
 struct transcode_scenario {
     const char *source;
     const char *sout;
     void (*decoder_setup)(decoder_t *);
     int (*decoder_decode)(decoder_t *, picture_t *);
+    int (*decoder_drain)(decoder_t *);
     void (*encoder_setup)(encoder_t *);
     void (*encoder_close)(encoder_t *);
-    void (*encoder_encode)(encoder_t *, picture_t *);
+    void (*encoder_encode)(encoder_t *, picture_t *, vlc_frame_t *);
+    vlc_frame_t *(*encoder_drain)(encoder_t *);
     void (*filter_setup)(filter_t *);
     void (*converter_setup)(filter_t *);
     void (*report_error)(sout_stream_t *);


=====================================
test/modules/stream_out/transcode_scenarios.c
=====================================
@@ -1,7 +1,7 @@
 /*****************************************************************************
  * transcode_scenario.c: testflight for transcoding pipeline
  *****************************************************************************
- * Copyright (C) 2021 VideoLabs
+ * Copyright (C) 2021-2026 VideoLabs
  *
  * Author: Alexandre Janniaux <ajanni at videolabs.io>
  *
@@ -40,6 +40,13 @@ static struct scenario_data
     bool encoder_opened;
     bool encoder_closed;
     bool error_reported;
+
+    /* To check the order of frames */
+    uint32_t decode_seq;
+    vlc_tick_t decode_date;
+    uint32_t output_seq;
+    uint32_t drained_count;
+    bool drain_reported;
 } scenario_data;
 
 static void decoder_fixed_size(decoder_t *dec, vlc_fourcc_t chroma,
@@ -194,18 +201,174 @@ static void encoder_i420_800_600_no_vctx(encoder_t *enc)
 }
 #endif
 
-static void encoder_encode_dummy(encoder_t *enc, picture_t *pic)
+static void encoder_encode_dummy(encoder_t *enc, picture_t *pic,
+                                 vlc_frame_t *out)
 {
-    (void)enc; (void)pic;
+    (void)enc; (void)pic; (void)out;
     msg_Info(enc, "Encode");
 }
 
+#define STRINGIFY_(x) #x
+#define STRINGIFY(x) STRINGIFY_(x)
+
+/* Number of frames generated by source_800_600_eos before EOS. */
+#define DRAIN_ORDER_SOURCE_FRAME_COUNT 4
+
+/* Number of frames the decoder releases when it is drained. They are encoded
+ * right away and pending in the transcode output when the encoder is drained
+ * in turn. */
+#define DRAIN_ORDER_HELD_FRAME_COUNT 5
+
+/* Number of frames the encoder releases when it is drained, like an encoder
+ * flushing the frames it was still holding for its lookahead. */
+#define DRAIN_ORDER_DRAINED_FRAME_COUNT 3
+
+static void picture_stamp_destroy(picture_t *pic)
+{
+    free(pic->p_sys);
+}
+
+/* Stamp every picture output by the decoder with the order it was decoded at
+ * and the stage it comes from. */
+static picture_t *decoder_new_stamped_picture(decoder_t *dec,
+                                              enum test_output_origin origin)
+{
+    struct test_output *stamp = malloc(sizeof *stamp);
+    assert(stamp != NULL);
+    stamp->seq = scenario_data.decode_seq++;
+    stamp->origin = origin;
+
+    const picture_resource_t resource = {
+        .p_sys = stamp,
+        .pf_destroy = picture_stamp_destroy,
+    };
+    picture_t *pic = picture_NewFromResource(&dec->fmt_out.video, &resource);
+    assert(pic != NULL);
+    return pic;
+}
+
+static int decoder_decode_stamped(decoder_t *dec, picture_t *pic)
+{
+    int ret = decoder_UpdateVideoOutput(dec, NULL);
+    assert(ret == VLC_SUCCESS);
+
+    picture_t *stamped =
+        decoder_new_stamped_picture(dec, TEST_OUTPUT_FROM_DECODE);
+    stamped->date = pic->date;
+    picture_Release(pic);
+
+    scenario_data.decode_date = stamped->date;
+    decoder_QueueVideo(dec, stamped);
+    return VLC_SUCCESS;
+}
+
+static int decoder_drain_queue_held_frames(decoder_t *dec)
+{
+    /* Burst the pictures out of the decoder directly to the encoder in
+     * one decoder::pf_decode() call. */
+    for (size_t i = 0; i < DRAIN_ORDER_HELD_FRAME_COUNT; ++i)
+    {
+        picture_t *pic =
+            decoder_new_stamped_picture(dec, TEST_OUTPUT_FROM_DECODER_DRAIN);
+        pic->date = ++scenario_data.decode_date;
+        decoder_QueueVideo(dec, pic);
+    }
+    return VLC_SUCCESS;
+}
+
+static void encoder_encode_stamped(encoder_t *enc, picture_t *pic,
+                                   vlc_frame_t *out)
+{
+    const struct test_output *stamp = pic->p_sys;
+
+    /* The stamp must have gone through the transcode pipeline untouched. */
+    assert(stamp != NULL);
+    assert(out->i_buffer == sizeof *stamp);
+    memcpy(out->p_buffer, stamp, sizeof *stamp);
+    msg_Info(enc, "Encode %u", stamp->seq);
+}
+
+static vlc_frame_t *encoder_drain_held_frames(encoder_t *enc)
+{
+    /* The encoder is drained one frame at a time, until it reports that it has
+     * nothing left. Those frames have no picture to read stamps from, so they
+     * continue the numbering of the decoder, which is done releasing pictures at
+     * this point. */
+
+    if (scenario_data.drained_count == DRAIN_ORDER_DRAINED_FRAME_COUNT)
+        return NULL;
+
+    const struct test_output output = {
+        .seq = scenario_data.decode_seq + scenario_data.drained_count,
+        .origin = TEST_OUTPUT_FROM_ENCODER_DRAIN,
+    };
+
+    vlc_frame_t *out = vlc_frame_Alloc(sizeof output);
+    assert(out != NULL);
+    memcpy(out->p_buffer, &output, sizeof output);
+    msg_Info(enc, "Encode %u, from the drain", output.seq);
+    scenario_data.drained_count++;
+    return out;
+}
+
+static void report_output_in_decode_order(const vlc_frame_t *out)
+{
+    for (; out != NULL; out = out->p_next)
+    {
+        struct test_output output;
+
+        assert(!scenario_data.drain_reported);
+        assert(out->i_buffer == sizeof output);
+        memcpy(&output, out->p_buffer, sizeof output);
+
+        /* Check output order vs decoder order */
+        assert(output.seq == scenario_data.output_seq);
+        scenario_data.output_seq++;
+
+        /* Ensure the frame order and count to match what we expect in the
+         * test. We want every frames to be acknowledged in the right order. */
+        switch (output.origin)
+        {
+            case TEST_OUTPUT_FROM_DECODE:
+                assert(output.seq < DRAIN_ORDER_SOURCE_FRAME_COUNT);
+                break;
+            case TEST_OUTPUT_FROM_DECODER_DRAIN:
+                assert(output.seq >= DRAIN_ORDER_SOURCE_FRAME_COUNT);
+                assert(output.seq < DRAIN_ORDER_SOURCE_FRAME_COUNT
+                                  + DRAIN_ORDER_HELD_FRAME_COUNT);
+                break;
+            case TEST_OUTPUT_FROM_ENCODER_DRAIN:
+                assert(output.seq >= DRAIN_ORDER_SOURCE_FRAME_COUNT
+                                   + DRAIN_ORDER_HELD_FRAME_COUNT);
+                assert(output.seq < DRAIN_ORDER_SOURCE_FRAME_COUNT
+                                  + DRAIN_ORDER_HELD_FRAME_COUNT
+                                  + DRAIN_ORDER_DRAINED_FRAME_COUNT);
+                scenario_data.drain_reported =
+                    output.seq == DRAIN_ORDER_SOURCE_FRAME_COUNT
+                                + DRAIN_ORDER_HELD_FRAME_COUNT
+                                + DRAIN_ORDER_DRAINED_FRAME_COUNT - 1;
+                break;
+            default:
+                vlc_assert_unreachable();
+        }
+    }
+}
+
 static void encoder_close(encoder_t *enc)
 {
     (void)enc;
     scenario_data.encoder_closed = true;
 }
 
+static void encoder_close_drain_order(encoder_t *enc)
+{
+    encoder_close(enc);
+
+    /* The encoder is closed once the whole pipeline has been drained and the
+     * output reported, so the scenario checks can run. */
+    vlc_sem_post(&scenario_data.wait_stop);
+}
+
 static void wait_output_10_frames_reported(const vlc_frame_t *out)
 {
     // Count frame output.
@@ -260,6 +423,11 @@ static void converter_i420_to_nv12_800_600_vctx(filter_t *filter)
 }
 
 const char source_800_600[] = "mock://video_track_count=1;length=100000000000;video_width=800;video_height=600";
+
+/* Same source, but ending after a few frames so that the transcode pipeline
+ * is drained by the end of stream instead of by the stop. */
+static const char source_800_600_eos[] = "mock://video_track_count=1;video_image_count="
+    STRINGIFY(DRAIN_ORDER_SOURCE_FRAME_COUNT) ";video_width=800;video_height=600";
 struct transcode_scenario transcode_scenarios[] =
 {{
     .source = source_800_600,
@@ -365,6 +533,19 @@ struct transcode_scenario transcode_scenarios[] =
     .decoder_decode = decoder_decode_error,
     .report_error = wait_error_reported,
     .encoder_close = encoder_close,
+},{
+    /* The transcode must respect the encoder output order when
+     * submitted the drained pictures. */
+    .source = source_800_600_eos,
+    .sout = "sout=#transcode:output_checker",
+    .decoder_setup = decoder_i420_800_600,
+    .decoder_decode = decoder_decode_stamped,
+    .decoder_drain = decoder_drain_queue_held_frames,
+    .encoder_setup = encoder_i420_800_600,
+    .encoder_encode = encoder_encode_stamped,
+    .encoder_drain = encoder_drain_held_frames,
+    .encoder_close = encoder_close_drain_order,
+    .report_output = report_output_in_decode_order,
 }};
 size_t transcode_scenarios_count = ARRAY_SIZE(transcode_scenarios);
 
@@ -374,6 +555,12 @@ void transcode_scenario_init(void)
     scenario_data.output_frame_count = 0;
     scenario_data.converter_opened = false;
     scenario_data.encoder_opened = false;
+    scenario_data.encoder_closed = false;
+    scenario_data.decode_seq = 0;
+    scenario_data.decode_date = VLC_TICK_INVALID;
+    scenario_data.output_seq = 0;
+    scenario_data.drained_count = 0;
+    scenario_data.drain_reported = false;
     vlc_sem_init(&scenario_data.wait_stop, 0);
 }
 
@@ -393,4 +580,19 @@ void transcode_scenario_check(struct transcode_scenario *scenario)
 
     if (scenario_data.encoder_opened && scenario->encoder_close != NULL)
         assert(scenario_data.encoder_closed);
+
+    if (scenario->encoder_drain != NULL)
+    {
+        /* Check that everything was output correctly */
+        assert(scenario_data.drained_count == DRAIN_ORDER_DRAINED_FRAME_COUNT);
+        assert(scenario_data.drain_reported);
+        assert(scenario_data.decode_seq == DRAIN_ORDER_SOURCE_FRAME_COUNT
+                                         + DRAIN_ORDER_HELD_FRAME_COUNT);
+
+        /* The frames released by the decoder during its drain were still
+         * pending when the encoder was drained. They must all have been
+         * reported, and the drained frames after them. */
+        assert(scenario_data.output_seq == scenario_data.decode_seq
+                                         + DRAIN_ORDER_DRAINED_FRAME_COUNT);
+    }
 }



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/7ceece7e7a66c378777ae35c7a924f47470420d9...a10627928a84c7a98d0a3800f9156c269e53fa0f

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/7ceece7e7a66c378777ae35c7a924f47470420d9...a10627928a84c7a98d0a3800f9156c269e53fa0f
You're receiving this email because of your account on code.videolan.org. Manage all notifications: https://code.videolan.org/-/profile/notifications | Help: https://code.videolan.org/help




More information about the vlc-commits mailing list