Commit 0bc3a351 for libheif

commit 0bc3a3512854f74783529897d417b0ab821e28de
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Mon Sep 21 03:28:02 2026 +0200

    Sequence encoders: extract pending output instead of moving out of the optional

    Rename Encoder::encode_sequence_get_data() to encode_sequence_extract_data()
    and let it hand out the pending CodedImageData with
    std::exchange(m_current_output_data, std::nullopt).

    Moving out of a std::optional leaves it engaged with a moved-from value, so
    after the first frame the member was never disengaged again. "Nothing
    pending" was represented two ways: a disengaged optional before the first
    frame, and an engaged one with empty vectors afterwards, which the caller
    had to detect with a separate bitstream/properties emptiness check. The
    HEVC/VVC get_data() paths were written against the second form only, which
    is what let the header-only first drain fixed in #1907 dereference the
    disengaged optional.

    With the reset, a disengaged optional is the only "no output pending"
    state. HEVC and VVC drop the got_some_data flag and use that single check;
    HEVC also drops the redundant bitstream.empty() test in front of the hvcC
    attachment. AVIF no longer parks an empty CodedImageData when the encoder
    is still buffering. AVC keeps its m_output_image_complete gate in front of
    the exchange, because x264_encoder_headers() emits an SEI together with the
    SPS/PPS and that SEI has to be held back until the slice data arrives. The
    JPEG, JPEG 2000 and uncompressed encoders used has_value() as their
    encode_sequence_started() answer and now track that with a bool.

    Sequence output for HEVC, AVC, AVIF, VVC and JPEG is byte-identical to the
    previous code, and the test suite passes with -D_GLIBCXX_ASSERTIONS.

diff --git a/libheif/codecs/avc_enc.cc b/libheif/codecs/avc_enc.cc
index 955ecdf2..cb900936 100644
--- a/libheif/codecs/avc_enc.cc
+++ b/libheif/codecs/avc_enc.cc
@@ -25,6 +25,7 @@
 #include "api_structs.h"

 #include <string>
+#include <utility>

 #include "plugins/nalu_utils.h"

@@ -175,15 +176,16 @@ Error Encoder_AVC::encode_sequence_flush(heif_encoder* encoder)
 }


-std::optional<Encoder::CodedImageData> Encoder_AVC::encode_sequence_get_data()
+std::optional<Encoder::CodedImageData> Encoder_AVC::encode_sequence_extract_data()
 {
-  if (m_output_image_complete) {
-    m_output_image_complete = false;
-    return std::move(m_current_output_data);
-  }
-  else {
+  // Non-VCL NALs can arrive ahead of their picture (x264 emits an SEI together with the
+  // SPS/PPS headers). They are held back until the slice data has been collected.
+  if (!m_output_image_complete) {
     return std::nullopt;
   }
+
+  m_output_image_complete = false;
+  return std::exchange(m_current_output_data, std::nullopt);
 }

 Error Encoder_AVC::get_data(heif_encoder* encoder)
diff --git a/libheif/codecs/avc_enc.h b/libheif/codecs/avc_enc.h
index c879be8e..08541fca 100644
--- a/libheif/codecs/avc_enc.h
+++ b/libheif/codecs/avc_enc.h
@@ -51,7 +51,7 @@ public:

   Error encode_sequence_flush(heif_encoder* encoder) override;

-  std::optional<CodedImageData> encode_sequence_get_data() override;
+  std::optional<CodedImageData> encode_sequence_extract_data() override;

   std::shared_ptr<Box_VisualSampleEntry> get_sample_description_box(const CodedImageData&) const override;

@@ -59,7 +59,7 @@ private:
   bool m_encoder_active = false;
   bool m_end_of_sequence_reached = false;

-  // Whether the hvcC is complete and was returned in an encode_sequence_get_data() call.
+  // Whether the avcC is complete and was returned in an encode_sequence_extract_data() call.
   bool m_avcC_has_SPS = false;
   bool m_avcC_has_PPS = false;
   std::shared_ptr<class Box_avcC> m_avcC;
diff --git a/libheif/codecs/avif_enc.cc b/libheif/codecs/avif_enc.cc
index dba271cb..e6352d6a 100644
--- a/libheif/codecs/avif_enc.cc
+++ b/libheif/codecs/avif_enc.cc
@@ -25,6 +25,7 @@
 #include "api_structs.h"

 #include <string>
+#include <utility>

 enum heif_av1_obu_type : uint8_t
 {
@@ -183,6 +184,11 @@ Error Encoder_AVIF::get_data(heif_encoder* encoder)
   codedImage.codingConstraints.intra_pred_used = true;
   codedImage.codingConstraints.all_ref_pics_intra = m_all_refs_intra;

+  if (codedImage.bitstream.empty() && codedImage.properties.empty()) {
+    // The encoder is still buffering frames and returned nothing. Leave no output pending.
+    return {};
+  }
+
   m_current_output_data = std::move(codedImage);

   return {};
@@ -199,9 +205,9 @@ Error Encoder_AVIF::encode_sequence_flush(heif_encoder* encoder)
 }


-std::optional<Encoder::CodedImageData> Encoder_AVIF::encode_sequence_get_data()
+std::optional<Encoder::CodedImageData> Encoder_AVIF::encode_sequence_extract_data()
 {
-  return std::move(m_current_output_data);
+  return std::exchange(m_current_output_data, std::nullopt);
 }


diff --git a/libheif/codecs/avif_enc.h b/libheif/codecs/avif_enc.h
index f704a4f8..a0754e59 100644
--- a/libheif/codecs/avif_enc.h
+++ b/libheif/codecs/avif_enc.h
@@ -53,7 +53,7 @@ public:

   Error encode_sequence_flush(heif_encoder* encoder) override;

-  std::optional<CodedImageData> encode_sequence_get_data() override;
+  std::optional<CodedImageData> encode_sequence_extract_data() override;


   std::shared_ptr<Box_VisualSampleEntry> get_sample_description_box(const CodedImageData&) const override;
diff --git a/libheif/codecs/encoder.h b/libheif/codecs/encoder.h
index a10d7ed7..75203046 100644
--- a/libheif/codecs/encoder.h
+++ b/libheif/codecs/encoder.h
@@ -94,7 +94,11 @@ public:

   virtual Error encode_sequence_flush(heif_encoder* encoder) { return {}; }

-  virtual std::optional<CodedImageData> encode_sequence_get_data() { return std::nullopt; }
+  // Hands out the coded data collected since the previous call and leaves nothing pending
+  // in the encoder, so that the next call returns std::nullopt until new data has arrived.
+  // The result may carry only properties (e.g. the codec configuration box that some
+  // encoders attach at the end of the sequence) and an empty bitstream.
+  virtual std::optional<CodedImageData> encode_sequence_extract_data() { return std::nullopt; }

   virtual std::shared_ptr<Box_VisualSampleEntry> get_sample_description_box(const CodedImageData&) const { return {}; }
 };
diff --git a/libheif/codecs/hevc_enc.cc b/libheif/codecs/hevc_enc.cc
index 20186582..75b6d932 100644
--- a/libheif/codecs/hevc_enc.cc
+++ b/libheif/codecs/hevc_enc.cc
@@ -25,6 +25,7 @@
 #include "api_structs.h"

 #include <string>
+#include <utility>

 #include "plugins/nalu_utils.h"

@@ -170,17 +171,15 @@ Error Encoder_HEVC::encode_sequence_flush(heif_encoder* encoder)
 }


-std::optional<Encoder::CodedImageData> Encoder_HEVC::encode_sequence_get_data()
+std::optional<Encoder::CodedImageData> Encoder_HEVC::encode_sequence_extract_data()
 {
-  return std::move(m_current_output_data);
+  return std::exchange(m_current_output_data, std::nullopt);
 }

 Error Encoder_HEVC::get_data(heif_encoder* encoder)
 {
   //CodedImageData codedImage;

-  bool got_some_data = false;
-
   for (;;) {
     uint8_t* data;
     int size;
@@ -196,8 +195,6 @@ Error Encoder_HEVC::get_data(heif_encoder* encoder)
       break;
     }

-    got_some_data = true;
-
     const uint8_t nal_type = (data[0] >> 1);
     const bool is_sync = (nal_type == 19 || nal_type == 20 || nal_type == 21);
     const bool is_image_data = (nal_type >= 0 && nal_type <= HEVC_NAL_UNIT_MAX_VCL);
@@ -243,17 +240,10 @@ Error Encoder_HEVC::get_data(heif_encoder* encoder)
     }
   }

-  if (!got_some_data) {
-    return {};
-  }
-
-  // The encoder can hand out parameter-set NALs before any slice data. x265,
-  // for example, emits VPS/SPS/PPS from encoder_headers() as soon as the
-  // sequence encoder is opened, so the first get_data() after
-  // start_sequence_encoding() sees only headers. Those are collected into
-  // m_hvcC and leave m_current_output_data unset, so there is no coded
-  // image to report yet. Without this check the dereferences below run on a
-  // disengaged std::optional.
+  // No coded image to report when the encoder returned no NALs, or only parameter sets.
+  // x265, for example, emits VPS/SPS/PPS from encoder_headers() as soon as the sequence
+  // encoder is opened, so the first get_data() after start_sequence_encoding() sees only
+  // headers. Those went into m_hvcC above and m_current_output_data stays disengaged.
   if (!m_current_output_data) {
     return {};
   }
@@ -268,8 +258,7 @@ Error Encoder_HEVC::get_data(heif_encoder* encoder)
   //     TODO: it's maybe better to return this at the end so that we are sure to have all headers
   //           and also complete codingConstraints.

-  if (!m_current_output_data->bitstream.empty() &&
-      m_hvcC_has_VPS && m_hvcC_has_SPS && m_hvcC_has_PPS && !m_hvcC_sent) {
+  if (m_hvcC_has_VPS && m_hvcC_has_SPS && m_hvcC_has_PPS && !m_hvcC_sent) {
   //if (/*m_end_of_sequence_reached &&*/ m_hvcC && !m_hvcC_sent) {
     m_current_output_data->properties.push_back(m_hvcC);
     m_hvcC = nullptr;
diff --git a/libheif/codecs/hevc_enc.h b/libheif/codecs/hevc_enc.h
index dc57fcc9..cc9e5ac0 100644
--- a/libheif/codecs/hevc_enc.h
+++ b/libheif/codecs/hevc_enc.h
@@ -51,7 +51,7 @@ public:

   Error encode_sequence_flush(heif_encoder* encoder) override;

-  std::optional<CodedImageData> encode_sequence_get_data() override;
+  std::optional<CodedImageData> encode_sequence_extract_data() override;

   std::shared_ptr<Box_VisualSampleEntry> get_sample_description_box(const CodedImageData&) const override;

@@ -59,7 +59,7 @@ private:
   bool m_encoder_active = false;
   bool m_end_of_sequence_reached = false;

-  // Whether the hvcC is complete and was returned in an encode_sequence_get_data() call.
+  // Whether the hvcC is complete and was returned in an encode_sequence_extract_data() call.
   bool m_hvcC_has_VPS = false;
   bool m_hvcC_has_SPS = false;
   bool m_hvcC_has_PPS = false;
diff --git a/libheif/codecs/jpeg2000_enc.h b/libheif/codecs/jpeg2000_enc.h
index a535bab7..ee899da2 100644
--- a/libheif/codecs/jpeg2000_enc.h
+++ b/libheif/codecs/jpeg2000_enc.h
@@ -42,7 +42,7 @@ public:

   std::shared_ptr<Box_VisualSampleEntry> get_sample_description_box(const CodedImageData&) const override;

-  bool encode_sequence_started() const override { return m_codedImageData.has_value(); }
+  bool encode_sequence_started() const override { return m_sequence_started; }

   Error encode_sequence_frame(const std::shared_ptr<HeifPixelImage>& image,
                                       heif_encoder* encoder,
@@ -62,6 +62,7 @@ public:

     m_codedImageData->frame_nr = frame_number;
     m_codedImageData->is_sync_frame = true;
+    m_sequence_started = true;

     return {};
   }
@@ -71,13 +72,14 @@ public:
     return {};
   }

-  std::optional<CodedImageData> encode_sequence_get_data() override
+  std::optional<CodedImageData> encode_sequence_extract_data() override
   {
-    return std::move(m_codedImageData);
+    return std::exchange(m_codedImageData, std::nullopt);
   }

 private:
   std::optional<CodedImageData> m_codedImageData;
+  bool m_sequence_started = false;
 };


diff --git a/libheif/codecs/jpeg_enc.h b/libheif/codecs/jpeg_enc.h
index ed8b8cdd..c9f90f7f 100644
--- a/libheif/codecs/jpeg_enc.h
+++ b/libheif/codecs/jpeg_enc.h
@@ -45,7 +45,7 @@ public:
   std::shared_ptr<Box_VisualSampleEntry> get_sample_description_box(const CodedImageData&) const override;


-  bool encode_sequence_started() const override { return m_codedImageData.has_value(); }
+  bool encode_sequence_started() const override { return m_sequence_started; }

   Error encode_sequence_frame(const std::shared_ptr<HeifPixelImage>& image,
                                       heif_encoder* encoder,
@@ -65,6 +65,7 @@ public:

     m_codedImageData->frame_nr = frame_number;
     m_codedImageData->is_sync_frame = true;
+    m_sequence_started = true;

     return {};
   }
@@ -74,13 +75,14 @@ public:
     return {};
   }

-  std::optional<CodedImageData> encode_sequence_get_data() override
+  std::optional<CodedImageData> encode_sequence_extract_data() override
   {
-    return std::move(m_codedImageData);
+    return std::exchange(m_codedImageData, std::nullopt);
   }

 private:
   std::optional<CodedImageData> m_codedImageData;
+  bool m_sequence_started = false;
 };


diff --git a/libheif/codecs/uncompressed/unc_enc.h b/libheif/codecs/uncompressed/unc_enc.h
index 01d7eeeb..25bb3f46 100644
--- a/libheif/codecs/uncompressed/unc_enc.h
+++ b/libheif/codecs/uncompressed/unc_enc.h
@@ -42,7 +42,7 @@ public:

   std::shared_ptr<class Box_VisualSampleEntry> get_sample_description_box(const CodedImageData&) const override;

-  bool encode_sequence_started() const override { return m_codedImageData.has_value(); }
+  bool encode_sequence_started() const override { return m_sequence_started; }

   Error encode_sequence_frame(const std::shared_ptr<HeifPixelImage>& image,
                                       heif_encoder* encoder,
@@ -64,6 +64,7 @@ public:

     m_codedImageData->frame_nr = frame_number;
     m_codedImageData->is_sync_frame = true;
+    m_sequence_started = true;

     return {};
   }
@@ -73,13 +74,14 @@ public:
     return {};
   }

-  std::optional<CodedImageData> encode_sequence_get_data() override
+  std::optional<CodedImageData> encode_sequence_extract_data() override
   {
-    return std::move(m_codedImageData);
+    return std::exchange(m_codedImageData, std::nullopt);
   }

 private:
   std::optional<CodedImageData> m_codedImageData;
+  bool m_sequence_started = false;
 };


diff --git a/libheif/codecs/vvc_enc.cc b/libheif/codecs/vvc_enc.cc
index df4ae0f3..0bd3350e 100644
--- a/libheif/codecs/vvc_enc.cc
+++ b/libheif/codecs/vvc_enc.cc
@@ -25,6 +25,7 @@
 #include "api_structs.h"

 #include <string>
+#include <utility>

 #include "plugins/nalu_utils.h"

@@ -175,17 +176,15 @@ Error Encoder_VVC::encode_sequence_flush(heif_encoder* encoder)
 }


-std::optional<Encoder::CodedImageData> Encoder_VVC::encode_sequence_get_data()
+std::optional<Encoder::CodedImageData> Encoder_VVC::encode_sequence_extract_data()
 {
-  return std::move(m_current_output_data);
+  return std::exchange(m_current_output_data, std::nullopt);
 }

 Error Encoder_VVC::get_data(heif_encoder* encoder)
 {
   //CodedImageData codedImage;

-  bool got_some_data = false;
-
   for (;;) {
     uint8_t* data;
     int size;
@@ -201,8 +200,6 @@ Error Encoder_VVC::get_data(heif_encoder* encoder)
       break;
     }

-    got_some_data = true;
-
     const uint8_t nal_type = (data[1] >> 3);
     const bool is_sync = (nal_type == 7 || nal_type == 8 || nal_type == 9);
     const bool is_image_data = (nal_type >= 0 && nal_type <= VVC_NAL_UNIT_MAX_VCL);
@@ -249,17 +246,9 @@ Error Encoder_VVC::get_data(heif_encoder* encoder)
     }
   }

-  if (!got_some_data) {
-    return {};
-  }
-
-  // The encoder can hand out parameter-set NALs before any slice data. x265,
-  // for example, emits VPS/SPS/PPS from encoder_headers() as soon as the
-  // sequence encoder is opened, so the first get_data() after
-  // start_sequence_encoding() sees only headers. Those are collected into
-  // m_vvcC and leave m_current_output_data unset, so there is no coded
-  // image to report yet. Without this check the dereferences below run on a
-  // disengaged std::optional.
+  // No coded image to report when the encoder returned no NALs, or only parameter sets
+  // (an encoder may emit VPS/SPS/PPS as soon as the sequence encoder is opened, before any
+  // slice data). Those went into m_vvcC above and m_current_output_data stays disengaged.
   if (!m_current_output_data) {
     return {};
   }
diff --git a/libheif/codecs/vvc_enc.h b/libheif/codecs/vvc_enc.h
index b629816d..a6630e27 100644
--- a/libheif/codecs/vvc_enc.h
+++ b/libheif/codecs/vvc_enc.h
@@ -54,13 +54,13 @@ public:

   Error encode_sequence_flush(heif_encoder* encoder) override;

-  std::optional<CodedImageData> encode_sequence_get_data() override;
+  std::optional<CodedImageData> encode_sequence_extract_data() override;

 private:
   bool m_encoder_active = false;
   bool m_end_of_sequence_reached = false;

-  // Whether the vvcC is complete and was returned in an encode_sequence_get_data() call.
+  // Whether the vvcC is complete and was returned in an encode_sequence_extract_data() call.
   bool m_vvcC_has_VPS = false;
   bool m_vvcC_has_SPS = false;
   bool m_vvcC_has_PPS = false;
diff --git a/libheif/sequences/track_visual.cc b/libheif/sequences/track_visual.cc
index deb7cdb1..92add576 100644
--- a/libheif/sequences/track_visual.cc
+++ b/libheif/sequences/track_visual.cc
@@ -651,9 +651,10 @@ Result<bool> Track_Visual::process_encoded_data(heif_encoder* h_encoder)
 {
   auto encoder = m_chunks.back()->get_encoder();

-  std::optional<Encoder::CodedImageData> encodingResult = encoder->encode_sequence_get_data();
+  std::optional<Encoder::CodedImageData> encodingResult = encoder->encode_sequence_extract_data();
   if (!encodingResult) {
-    return {};
+    // nothing pending in the encoder
+    return {false};
   }

   const Encoder::CodedImageData& data = *encodingResult;