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

When HEVC decoding is interrupted during draining and the decoder is flushed 
for a seek or restart, 
it can still return a frame from the previous playback position, even before 
any new compressed data is submitted. 
Applications expecting a clean decoder reset can therefore receive an old frame 
in the new playback sequence.

The flush currently clears the decoded-picture buffer but leaves already queued 
output frames intact. 
Frames already queued in output_fifo retain references independently of the 
decoded picture buffer.
After an interrupted drain, `avcodec_receive_frame()` can return one of these 
frames after `avcodec_flush_buffers()`, before any new input packet is supplied.

This patch discards those pending frames while retaining the reusable FIFO 
allocation.

Add a public-codec-API regression covering no drain, interrupted drain, 
completed drain, 
repeated flushes, and decoder reuse from the first and a nonzero IDR. 
Compare every output timestamp and pixel digest with sequential decoding in 
single-threaded and two/four-thread frame and slice modes.

The FATE regression uses the separately supplied hevc/flush-reuse.hevc sample: 
50 frames with 
reordered B pictures and closed-GOP IDRs at frames 0 and 25.

```fate-samples
hevc/flush-reuse.hevc
```



>From 11cb2abce9202c407ccb214408debe23d0e5d675 Mon Sep 17 00:00:00 2001
From: moto <[email protected]>
Date: Thu, 5 Nov 2026 11:54:27 -0800
Subject: [PATCH] avcodec/hevc: discard queued output frames on flush

Frames already queued in output_fifo retain references independently of the 
decoded picture buffer. After an interrupted drain, avcodec_receive_frame() can 
return one of these frames after avcodec_flush_buffers(), before any new input 
packet is supplied.

Discard the queued frames when flushing while retaining the FIFO and its 
reusable container pool.

Add a public-codec-API regression covering no drain, interrupted drain, 
completed drain, repeated flushes, and decoder reuse from the first and a 
nonzero IDR. Compare every output timestamp and pixel digest with sequential 
decoding in single-threaded and two/four-thread frame and slice modes.

The FATE regression uses the separately supplied hevc/flush-reuse.hevc sample: 
50 frames with reordered B pictures and closed-GOP IDRs at frames 0 and 25.
---
 libavcodec/hevc/hevcdec.c       |   2 +
 tests/api/Makefile              |   1 +
 tests/api/api-hevc-flush-test.c | 260 ++++++++++++++++++++++++++++++++
 tests/fate/api.mak              |   6 +
 4 files changed, 269 insertions(+)
 create mode 100644 tests/api/api-hevc-flush-test.c

diff --git a/libavcodec/hevc/hevcdec.c b/libavcodec/hevc/hevcdec.c
index 83fbdef9ba4..c997e0d7e24 100644
--- a/libavcodec/hevc/hevcdec.c
+++ b/libavcodec/hevc/hevcdec.c
@@ -5128,6 +5128,8 @@ static av_cold int hevc_decode_init(AVCodecContext *avctx)
 static av_cold void hevc_decode_flush(AVCodecContext *avctx)
 {
     HEVCContext *s = avctx->priv_data;
+    av_container_fifo_drain(s->output_fifo,
+                            av_container_fifo_can_read(s->output_fifo));
     ff_hevc_flush_dpb(s);
     ff_hevc_reset_sei(&s->sei);
     ff_dovi_ctx_flush(&s->dovi_ctx);
diff --git a/tests/api/Makefile b/tests/api/Makefile
index c411e82e5f1..b66561fed5e 100644
--- a/tests/api/Makefile
+++ b/tests/api/Makefile
@@ -5,6 +5,7 @@ APITESTPROGS-$(call DEMDEC, H264, H264) += api-h264
 APITESTPROGS-$(call DEMDEC, H264, H264) += api-h264-slice
 APITESTPROGS-$(CONFIG_MOV_MUXER)        += api-movenc
 APITESTPROGS-$(CONFIG_MOVTEXT_ENCODER)  += api-movtext
+APITESTPROGS-$(call ALLYES, HEVC_DECODER HEVC_PARSER) += api-hevc-flush
 APITESTPROGS-$(call ALLYES, HEVC_MP4TOANNEXB_BSF HEVC_DECODER) += 
api-hevc-mp4toannexb
 APITESTPROGS-yes += api-seek api-dump-stream-meta
 APITESTPROGS-$(call DEMDEC, H263, H263) += api-band
diff --git a/tests/api/api-hevc-flush-test.c b/tests/api/api-hevc-flush-test.c
new file mode 100644
index 00000000000..fc39f4fb7ca
--- /dev/null
+++ b/tests/api/api-hevc-flush-test.c
@@ -0,0 +1,260 @@
+/*
+ * 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
+ */
+
+/* Check that flushing an HEVC decoder discards pending output frames. */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "libavcodec/avcodec.h"
+#include "libavutil/error.h"
+#include "libavutil/imgutils.h"
+#include "libavutil/md5.h"
+#include "libavutil/mem.h"
+
+typedef struct FrameDigest {
+    int64_t pts;
+    uint8_t md5[16];
+} FrameDigest;
+
+typedef struct Fixture {
+    AVPacket **packets;
+    int count;
+    int restart;
+} Fixture;
+
+static void require(int condition, const char *message)
+{
+    if (!condition) {
+        fprintf(stderr, "FAIL: %s\n", message);
+        exit(1);
+    }
+}
+
+static void check(int ret, const char *operation)
+{
+    char message[AV_ERROR_MAX_STRING_SIZE];
+
+    if (ret < 0) {
+        av_strerror(ret, message, sizeof(message));
+        fprintf(stderr, "FAIL: %s: %s (%d)\n", operation, message, ret);
+        exit(1);
+    }
+}
+
+static void add_packet(Fixture *fixture, AVCodecParserContext *parser,
+                       const uint8_t *data, int size, int *poc_base)
+{
+    AVPacket *packet = av_packet_alloc();
+    AVPacket **packets;
+
+    require(packet != NULL, "packet allocation");
+    check(av_new_packet(packet, size), "packet buffer allocation");
+    memcpy(packet->data, data, size);
+    if (parser->key_frame && !parser->output_picture_number && fixture->count) 
{
+        *poc_base = fixture->count;
+        fixture->restart = fixture->count;
+    }
+    packet->pts = *poc_base + parser->output_picture_number;
+    packet->dts = fixture->count;
+    packets = av_realloc_array(fixture->packets, fixture->count + 1,
+                               sizeof(*packets));
+    require(packets != NULL, "packet list allocation");
+    fixture->packets = packets;
+    fixture->packets[fixture->count++] = packet;
+}
+
+static Fixture load_fixture(const char *path)
+{
+    const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_HEVC);
+    AVCodecContext *context = avcodec_alloc_context3(codec);
+    AVCodecParserContext *parser = av_parser_init(AV_CODEC_ID_HEVC);
+    Fixture fixture = { 0 };
+    uint8_t input[4096 + AV_INPUT_BUFFER_PADDING_SIZE];
+    uint8_t *data, *output;
+    int size, used, output_size, poc_base = 0;
+    FILE *file = fopen(path, "rb");
+
+    require(file && codec && context && parser, "fixture/parser setup");
+    while ((size = fread(input, 1, 4096, file)) > 0) {
+        memset(input + size, 0, AV_INPUT_BUFFER_PADDING_SIZE);
+        data = input;
+        while (size > 0) {
+            used = av_parser_parse2(parser, context, &output, &output_size,
+                                    data, size, AV_NOPTS_VALUE, 
AV_NOPTS_VALUE, 0);
+            check(used, "parse fixture");
+            require(used || output_size, "parser progress");
+            data += used;
+            size -= used;
+            if (output_size)
+                add_packet(&fixture, parser, output, output_size, &poc_base);
+        }
+    }
+    require(!ferror(file), "read fixture");
+    check(av_parser_parse2(parser, context, &output, &output_size, NULL, 0,
+                           AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0), "finish 
parser");
+    if (output_size)
+        add_packet(&fixture, parser, output, output_size, &poc_base);
+    fclose(file);
+    av_parser_close(parser);
+    avcodec_free_context(&context);
+    require(fixture.count == 50 && fixture.restart == 25,
+            "fixture must contain 50 frames and an IDR at frame 25");
+    return fixture;
+}
+
+static void digest_frame(const AVFrame *frame, FrameDigest *digest)
+{
+    uint8_t *pixels;
+    int size = av_image_get_buffer_size(frame->format, frame->width,
+                                        frame->height, 1);
+
+    check(size, "image buffer size");
+    pixels = av_malloc(size);
+    require(pixels != NULL, "image buffer allocation");
+    check(av_image_copy_to_buffer(pixels, size,
+                                  (const uint8_t *const *)frame->data,
+                                  frame->linesize, frame->format,
+                                  frame->width, frame->height, 1), "copy 
pixels");
+    digest->pts = frame->pts;
+    av_md5_sum(digest->md5, pixels, size);
+    av_free(pixels);
+}
+
+static int receive(AVCodecContext *context, AVFrame *frame,
+                   FrameDigest *digests, int *count, int limit)
+{
+    int ret;
+
+    while ((ret = avcodec_receive_frame(context, frame)) >= 0) {
+        require(*count < limit, "too many output frames");
+        digest_frame(frame, &digests[(*count)++]);
+        av_frame_unref(frame);
+    }
+    require(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF,
+            "unexpected receive error");
+    return ret;
+}
+
+static int decode(AVCodecContext *context, AVFrame *frame, const Fixture 
*fixture,
+                  int start, int drain, FrameDigest *digests)
+{
+    int count = 0, i, ret;
+
+    for (i = start; i < fixture->count; i++) {
+        check(avcodec_send_packet(context, fixture->packets[i]), "send 
packet");
+        ret = receive(context, frame, digests, &count, fixture->count);
+        require(ret == AVERROR(EAGAIN), "EOF before drain");
+    }
+    if (drain) {
+        check(avcodec_send_packet(context, NULL), "send drain");
+        ret = receive(context, frame, digests, &count, fixture->count);
+        require(ret == AVERROR_EOF, "completed drain must end in EOF");
+    }
+    return count;
+}
+
+static AVCodecContext *open_decoder(int threads, int thread_type)
+{
+    const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_HEVC);
+    AVCodecContext *context = avcodec_alloc_context3(codec);
+
+    require(codec && context, "decoder allocation");
+    context->thread_count = threads;
+    context->thread_type = thread_type;
+    check(avcodec_open2(context, codec, NULL), "open decoder");
+    return context;
+}
+
+static void compare(const FrameDigest *actual, int count,
+                    const FrameDigest *expected, int expected_count)
+{
+    int i;
+
+    require(count == expected_count, "frame count changed on reuse");
+    for (i = 0; i < count; i++) {
+        require(actual[i].pts == expected[i].pts, "output PTS changed on 
reuse");
+        require(!memcmp(actual[i].md5, expected[i].md5, 16),
+                "decoded pixels changed on reuse");
+    }
+}
+
+static void test_mode(const Fixture *fixture, int threads, int thread_type)
+{
+    AVCodecContext *context = open_decoder(threads, thread_type);
+    AVFrame *frame = av_frame_alloc();
+    FrameDigest *baseline = av_calloc(fixture->count, sizeof(*baseline));
+    FrameDigest *actual = av_calloc(fixture->count, sizeof(*actual));
+    int count, drain_kind, i;
+
+    require(frame && baseline && actual, "test allocation");
+    count = decode(context, frame, fixture, 0, 1, baseline);
+    require(count == fixture->count, "baseline frame count");
+    for (i = 0; i < count; i++)
+        require(baseline[i].pts == i, "baseline display order");
+    avcodec_flush_buffers(context);
+    for (drain_kind = 0; drain_kind < 3; drain_kind++) {
+        decode(context, frame, fixture, 0, drain_kind == 2, actual);
+        if (drain_kind == 1) {
+            check(avcodec_send_packet(context, NULL), "start interrupted 
drain");
+            check(avcodec_receive_frame(context, frame), "first drained 
frame");
+            av_frame_unref(frame);
+        }
+        avcodec_flush_buffers(context);
+        for (i = 0; i < 3; i++) {
+            require(avcodec_receive_frame(context, frame) == AVERROR(EAGAIN),
+                    "frame returned after flush without new input");
+            avcodec_flush_buffers(context);
+        }
+        count = decode(context, frame, fixture, 0, 1, actual);
+        compare(actual, count, baseline, fixture->count);
+        avcodec_flush_buffers(context);
+        count = decode(context, frame, fixture, fixture->restart, 1, actual);
+        compare(actual, count, baseline + fixture->restart,
+                fixture->count - fixture->restart);
+        avcodec_flush_buffers(context);
+    }
+    av_free(baseline);
+    av_free(actual);
+    av_frame_free(&frame);
+    avcodec_free_context(&context);
+}
+
+int main(int argc, char **argv)
+{
+    Fixture fixture;
+    int threads, i;
+
+    require(argc == 2 || argc == 4,
+            "usage: api-hevc-flush-test fixture.hevc [threads thread_type]");
+    fixture = load_fixture(argv[1]);
+    if (argc == 4) {
+        test_mode(&fixture, atoi(argv[2]), atoi(argv[3]));
+    } else {
+        test_mode(&fixture, 1, FF_THREAD_FRAME | FF_THREAD_SLICE);
+        for (threads = 2; threads <= 4; threads *= 2) {
+            test_mode(&fixture, threads, FF_THREAD_FRAME);
+            test_mode(&fixture, threads, FF_THREAD_SLICE);
+        }
+    }
+    for (i = 0; i < fixture.count; i++)
+        av_packet_free(&fixture.packets[i]);
+    av_free(fixture.packets);
+    return 0;
+}
diff --git a/tests/fate/api.mak b/tests/fate/api.mak
index 1659c7979c0..bdb9bd2988c 100644
--- a/tests/fate/api.mak
+++ b/tests/fate/api.mak
@@ -20,6 +20,11 @@ FATE_API_LIBAVCODEC-$(CONFIG_MOVTEXT_ENCODER) += 
fate-api-movtext
 fate-api-movtext: $(APITESTSDIR)/api-movtext-test$(EXESUF)
 fate-api-movtext: CMD = run $(APITESTSDIR)/api-movtext-test$(EXESUF)
 
+FATE_API_SAMPLES_LIBAVCODEC-$(call ALLYES, HEVC_DECODER HEVC_PARSER) += 
fate-api-hevc-flush
+fate-api-hevc-flush: $(APITESTSDIR)/api-hevc-flush-test$(EXESUF)
+fate-api-hevc-flush: CMD = run $(APITESTSDIR)/api-hevc-flush-test$(EXESUF) 
$(TARGET_SAMPLES)/hevc/flush-reuse.hevc
+fate-api-hevc-flush: CMP = null
+
 FATE_API_LIBAVCODEC-$(call ALLYES, HEVC_MP4TOANNEXB_BSF HEVC_DECODER) += 
fate-api-hevc-mp4toannexb
 fate-api-hevc-mp4toannexb: $(APITESTSDIR)/api-hevc-mp4toannexb-test$(EXESUF)
 fate-api-hevc-mp4toannexb: CMD = run 
$(APITESTSDIR)/api-hevc-mp4toannexb-test$(EXESUF)
@@ -59,6 +64,7 @@ fate-api-threadmessage: 
$(APITESTSDIR)/api-threadmessage-test$(EXESUF)
 fate-api-threadmessage: CMD = run 
$(APITESTSDIR)/api-threadmessage-test$(EXESUF) 3 10 30 50 2 20 40
 fate-api-threadmessage: CMP = null
 
+FATE_API_SAMPLES-$(CONFIG_AVCODEC) += $(FATE_API_SAMPLES_LIBAVCODEC-yes)
 FATE_API_SAMPLES-$(CONFIG_AVFORMAT) += $(FATE_API_SAMPLES_LIBAVFORMAT-yes)
 
 ifdef SAMPLES
-- 
2.54.0

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

Reply via email to