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]
