Commit ce87f3c9 for libheif

commit ce87f3c98871cb137bed70426f4b31e8d9b8c03e
Author: Dirk Lemstra <dirk@lemstra.org>
Date:   Sun Oct 11 23:37:50 2026 +0200

    Poll pending sequence frames before flushing decoders

diff --git a/libheif/sequences/track_visual.cc b/libheif/sequences/track_visual.cc
index 18a4c58b..f38e77d3 100644
--- a/libheif/sequences/track_visual.cc
+++ b/libheif/sequences/track_visual.cc
@@ -313,13 +313,6 @@ Result<std::shared_ptr<HeifPixelImage> > Track_Visual::decode_next_image_sample(
       // or finishing input. The next sample may belong to another decoder.
       const bool end_of_input = m_next_sample_to_be_decoded >= num_output_samples;
       const bool switching_decoder = decoder != m_active_decoder;
-      if ((end_of_input || switching_decoder) && !m_decoder_is_flushed) {
-        if (Error err = m_active_decoder->flush_decoder()) {
-          return err;
-        }
-        m_decoder_is_flushed = true;
-      }
-
       Result<std::shared_ptr<HeifPixelImage> > getFrameResult = m_active_decoder->get_decoded_frame(options,
                                                                                                     &decoded_sample_idx,
                                                                                                     m_heif_context->get_security_limits());
@@ -340,6 +333,14 @@ Result<std::shared_ptr<HeifPixelImage> > Track_Visual::decode_next_image_sample(
         break;
       }

+      if ((end_of_input || switching_decoder) && !m_decoder_is_flushed) {
+        if (Error err = m_active_decoder->flush_decoder()) {
+          return err;
+        }
+        m_decoder_is_flushed = true;
+        continue;
+      }
+
       // A drained decoder can be replaced at a chunk boundary. At end-of-input,
       // a missing output frame is an error.
       if (m_decoder_is_flushed) {
diff --git a/tests/sequence_vfr_durations.cc b/tests/sequence_vfr_durations.cc
index 3f36eb92..d8e30e8d 100644
--- a/tests/sequence_vfr_durations.cc
+++ b/tests/sequence_vfr_durations.cc
@@ -43,6 +43,7 @@
 #include <algorithm>
 #include <cstdint>
 #include <string>
+#include <utility>
 #include <vector>

 namespace {
@@ -79,7 +80,8 @@ heif_image* make_gray_image(uint32_t duration, uint8_t value)
 }

 std::vector<uint8_t> encode_vfr_sequence(const std::vector<uint32_t>& durations, heif_compression_format format,
-                                         bool intra_only = false, uint32_t repetitions = 1)
+                                         bool intra_only = false, uint32_t repetitions = 1,
+                                         bool reordered = false)
 {
   heif_context* ctx = heif_context_alloc();
   REQUIRE(ctx != nullptr);
@@ -102,6 +104,9 @@ std::vector<uint8_t> encode_vfr_sequence(const std::vector<uint32_t>& durations,
   if (intra_only) {
     options->gop_structure = heif_sequence_gop_structure_intra_only;
   }
+  else if (reordered) {
+    options->gop_structure = heif_sequence_gop_structure_unrestricted;
+  }
   for (size_t i = 0; i < durations.size(); ++i) {
     heif_image* img = make_gray_image(durations[i], static_cast<uint8_t>((i + 1) * 60));

@@ -311,11 +316,13 @@ TEST_CASE("sequence decoding drains all frames across chunk decoder changes")
 }


-TEST_CASE("sequence decoding recreates flushed FFmpeg decoders for repeated playback")
+TEST_CASE("sequence decoding drains pending FFmpeg packets and recreates flushed decoders")
 {
-  const int count = heif_get_decoder_descriptors(heif_compression_AV1, nullptr, 0);
+  const auto format = GENERATE(heif_compression_AV1, heif_compression_HEVC);
+  INFO("compression format: " << format);
+  const int count = heif_get_decoder_descriptors(format, nullptr, 0);
   std::vector<const heif_decoder_descriptor*> decoders(count);
-  const int available = heif_get_decoder_descriptors(heif_compression_AV1, decoders.data(), count);
+  const int available = heif_get_decoder_descriptors(format, 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]);
@@ -323,14 +330,29 @@ TEST_CASE("sequence decoding recreates flushed FFmpeg decoders for repeated play
       have_ffmpeg = true;
     }
   }
-  if (!heif_have_encoder_for_format(heif_compression_AV1) || !have_ffmpeg) {
-    SKIP("No AV1 encoder/FFmpeg decoder pair available");
+  if (!heif_have_encoder_for_format(format) || !have_ffmpeg) {
+    SKIP("No 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};
+  enum class SequenceLayout { short_stream, inter_frame, different_descriptions };
+  const auto layout = GENERATE(SequenceLayout::short_stream, SequenceLayout::inter_frame,
+                               SequenceLayout::different_descriptions);
+  const bool different_descriptions = layout == SequenceLayout::different_descriptions;
+  const uint32_t repetitions = GENERATE(1u, 2u);
+  const bool short_sequence = layout == SequenceLayout::short_stream;
+  const std::vector<uint32_t> durations = short_sequence ? std::vector<uint32_t>{100}
+                                                       : std::vector<uint32_t>{100, 250, 400};
+  auto file = encode_vfr_sequence(durations, format, different_descriptions, repetitions,
+                                  format == heif_compression_HEVC);
+  if (different_descriptions) {
+    file = make_two_chunk_sequence(std::move(file), true);
+  }
+  std::vector<uint32_t> expected;
+  for (uint32_t i = 0; i < repetitions; ++i) {
+    expected.insert(expected.end(), durations.begin(), durations.end());
+  }
+  CHECK(decode_durations(file, heif_colorspace_RGB, heif_chroma_interleaved_RGB,
+                         !short_sequence, false, "ffmpeg") == expected);
   CHECK(decode_durations(file, heif_colorspace_RGB, heif_chroma_interleaved_RGB,
-                         true, false, "ffmpeg") == expected);
+                         !short_sequence, true, "ffmpeg") == durations);
 }