Commit 5499bc26 for libheif
commit 5499bc2661e63cd529b4cca4307fc70809c69252
Author: Dirk Lemstra <dirk@lemstra.org>
Date: Sun Oct 11 23:20:41 2026 +0200
Release drained sequence decoder contexts for repeated playback, correct FFmpeg EOF handling, and add regression coverage.
diff --git a/libheif/plugins/decoder_ffmpeg.cc b/libheif/plugins/decoder_ffmpeg.cc
index cc5bc580..39f15036 100644
--- a/libheif/plugins/decoder_ffmpeg.cc
+++ b/libheif/plugins/decoder_ffmpeg.cc
@@ -665,7 +665,7 @@ static heif_error ffmpeg_av_decode(ffmpeg_decoder* decoder, AVCodecContext* av_d
int ret;
ret = avcodec_receive_frame(av_dec_ctx, av_frame);
- if (ret == AVERROR(EAGAIN) || ret == AVERROR(AVERROR_EOF)) {
+ if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
*image = nullptr;
return heif_error_ok;
}
diff --git a/libheif/sequences/track_visual.cc b/libheif/sequences/track_visual.cc
index 713d1798..18a4c58b 100644
--- a/libheif/sequences/track_visual.cc
+++ b/libheif/sequences/track_visual.cc
@@ -348,6 +348,7 @@ Result<std::shared_ptr<HeifPixelImage> > Track_Visual::decode_next_image_sample(
heif_suberror_Unspecified,
"Did not decode all frames");
}
+ m_active_decoder->release_decoder();
m_active_decoder.reset();
m_decoder_is_flushed = false;
}
diff --git a/tests/sequence_vfr_durations.cc b/tests/sequence_vfr_durations.cc
index 7019db14..3f36eb92 100644
--- a/tests/sequence_vfr_durations.cc
+++ b/tests/sequence_vfr_durations.cc
@@ -42,6 +42,7 @@
#include <algorithm>
#include <cstdint>
+#include <string>
#include <vector>
namespace {
@@ -129,7 +130,8 @@ std::vector<uint8_t> encode_vfr_sequence(const std::vector<uint32_t>& durations,
std::vector<uint32_t> decode_durations(const std::vector<uint8_t>& file,
heif_colorspace colorspace, heif_chroma chroma,
- bool check_pixels = false, bool ignore_editlist = false)
+ bool check_pixels = false, bool ignore_editlist = false,
+ const char* decoder_id = nullptr)
{
heif_context* ctx = heif_context_alloc();
REQUIRE(ctx != nullptr);
@@ -142,6 +144,7 @@ std::vector<uint32_t> decode_durations(const std::vector<uint8_t>& file,
heif_decoding_options* options = heif_decoding_options_alloc();
REQUIRE(options != nullptr);
options->ignore_sequence_editlist = ignore_editlist;
+ options->decoder_id = decoder_id;
std::vector<uint32_t> durations;
for (;;) {
heif_image* img = nullptr;
@@ -306,3 +309,28 @@ TEST_CASE("sequence decoding drains all frames across chunk decoder changes")
CHECK(decode_durations(file, heif_colorspace_RGB, heif_chroma_interleaved_RGB, true) == expected);
CHECK(decode_durations(file, heif_colorspace_RGB, heif_chroma_interleaved_RGB, true, true) == durations);
}
+
+
+TEST_CASE("sequence decoding recreates flushed FFmpeg decoders for repeated playback")
+{
+ const int count = heif_get_decoder_descriptors(heif_compression_AV1, nullptr, 0);
+ std::vector<const heif_decoder_descriptor*> decoders(count);
+ const int available = heif_get_decoder_descriptors(heif_compression_AV1, decoders.data(), count);
+ bool have_ffmpeg = false;
+ for (int i = 0; i < available; ++i) {
+ const char* id = heif_decoder_descriptor_get_id_name(decoders[i]);
+ if (id && std::string(id) == "ffmpeg") {
+ have_ffmpeg = true;
+ }
+ }
+ if (!heif_have_encoder_for_format(heif_compression_AV1) || !have_ffmpeg) {
+ SKIP("No AV1 encoder/FFmpeg decoder pair available");
+ }
+
+ const std::vector<uint32_t> durations = {100, 250, 400};
+ const auto file = make_two_chunk_sequence(
+ encode_vfr_sequence(durations, heif_compression_AV1, true, 2), true);
+ const std::vector<uint32_t> expected = {100, 250, 400, 100, 250, 400};
+ CHECK(decode_durations(file, heif_colorspace_RGB, heif_chroma_interleaved_RGB,
+ true, false, "ffmpeg") == expected);
+}