Commit 86a99d64 for libheif

commit 86a99d648236dc18e66081b62f28be8999972b36
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Fri May 15 22:32:25 2026 +0200

    add API function heif_track_get_number_of_repetitions() (#1777)

diff --git a/libheif/api/libheif/heif_sequences.cc b/libheif/api/libheif/heif_sequences.cc
index f7c9a4cf..b8640758 100644
--- a/libheif/api/libheif/heif_sequences.cc
+++ b/libheif/api/libheif/heif_sequences.cc
@@ -134,6 +134,12 @@ uint32_t heif_track_get_timescale(const heif_track* track)
 }


+uint32_t heif_track_get_number_of_repetitions(const heif_track* track)
+{
+  return track->track->get_number_of_repetitions();
+}
+
+
 heif_error heif_track_get_image_resolution(const heif_track* track_ptr, uint16_t* out_width, uint16_t* out_height)
 {
   auto track = track_ptr->track;
diff --git a/libheif/api/libheif/heif_sequences.h b/libheif/api/libheif/heif_sequences.h
index 4012f1ee..aee2e3cc 100644
--- a/libheif/api/libheif/heif_sequences.h
+++ b/libheif/api/libheif/heif_sequences.h
@@ -159,6 +159,40 @@ int heif_track_has_alpha_channel(const heif_track*);
 LIBHEIF_API
 uint32_t heif_track_get_timescale(const heif_track*);

+/**
+ * Special return value of `heif_track_get_number_of_repetitions()` indicating that
+ * the editlist requests indefinite repetition (the mvhd duration is the ISOBMFF
+ * "duration unknown" sentinel and the editlist is in repeat mode).
+ */
+#define heif_sequence_track_number_of_repetitions_infinite 0xFFFFFFFFu
+
+/**
+ * How many times the media segment should be played according to the track's edit list.
+ *
+ * Returns:
+ *  - 0 if the edit list is absent or follows a pattern that libheif does not interpret
+ *    as a loop count. Callers should fall back to a single playback in that case.
+ *  - `heif_sequence_track_number_of_repetitions_infinite` (= UINT32_MAX) when the file
+ *    signals indefinite playback (mvhd duration is the all-1s sentinel together with an
+ *    editlist in repeat mode), or when the repetition count does not fit in uint32_t.
+ *  - Otherwise the number of times the media segment is played.
+ *
+ * The reported value is informational; it does not change how
+ * `heif_track_decode_next_image()` walks samples. By default, that function applies
+ * the edit list and (for repeated playback) returns samples for every requested
+ * repetition; iterating until end-of-sequence on an infinite-loop file therefore
+ * never terminates.
+ *
+ * Clients that want to handle repetition themselves (e.g. to honor an "infinite"
+ * value with their own looping policy or to enforce an application-level cap) should
+ * set `heif_decoding_options::ignore_sequence_editlist` when calling
+ * `heif_track_decode_next_image()`. With that flag set, libheif plays the media
+ * timeline exactly once. Use the value returned by this function to decide how often
+ * to replay the track at the application level.
+ */
+LIBHEIF_API
+uint32_t heif_track_get_number_of_repetitions(const heif_track*);
+

 // --- reading visual tracks

diff --git a/libheif/context.cc b/libheif/context.cc
index 2cadeed8..94c08391 100644
--- a/libheif/context.cc
+++ b/libheif/context.cc
@@ -2166,6 +2166,17 @@ uint64_t HeifContext::get_sequence_duration() const
 }


+bool HeifContext::is_sequence_duration_indefinite() const
+{
+  auto mvhd = m_heif_file->get_mvhd_box();
+  if (!mvhd) {
+    return false;
+  }
+
+  return mvhd->is_duration_indefinite();
+}
+
+
 Result<std::shared_ptr<Track_Visual>> HeifContext::add_visual_sequence_track(const TrackOptions* options,
                                                                              uint32_t handler_type,
                                                                              uint16_t width, uint16_t height)
diff --git a/libheif/context.h b/libheif/context.h
index 898111fd..3976d466 100644
--- a/libheif/context.h
+++ b/libheif/context.h
@@ -222,6 +222,10 @@ public:

   uint64_t get_sequence_duration() const;

+  // Returns true if the mvhd box signals an "indefinite" / unknown duration.
+  // For such files, an editlist in repeat mode means "loop forever".
+  bool is_sequence_duration_indefinite() const;
+
   void set_sequence_timescale(uint32_t timescale);

   void set_number_of_sequence_repetitions(uint32_t repetitions);
diff --git a/libheif/sequences/seq_boxes.h b/libheif/sequences/seq_boxes.h
index b12350dc..5aec8c63 100644
--- a/libheif/sequences/seq_boxes.h
+++ b/libheif/sequences/seq_boxes.h
@@ -27,6 +27,7 @@
 #include <string>
 #include <memory>
 #include <vector>
+#include <limits>


 class Box_container : public Box {
@@ -78,6 +79,18 @@ public:

   uint64_t get_duration() const { return m_duration; }

+  // True when the duration field carries the ISOBMFF "duration unknown / indefinite"
+  // sentinel (all-1s for the field width corresponding to the box version).
+  // Files written with such a duration alongside an editlist in repeat mode signal
+  // that the media should be looped indefinitely.
+  bool is_duration_indefinite() const
+  {
+    if (get_version() == 1) {
+      return m_duration == std::numeric_limits<uint64_t>::max();
+    }
+    return m_duration == std::numeric_limits<uint32_t>::max();
+  }
+
   void set_duration(uint64_t duration) { m_duration = duration; }

   void set_time_scale(uint32_t timescale) { m_timescale = timescale; }
diff --git a/libheif/sequences/track.cc b/libheif/sequences/track.cc
index 405b6ad3..9a5da292 100644
--- a/libheif/sequences/track.cc
+++ b/libheif/sequences/track.cc
@@ -1065,7 +1065,20 @@ Error Track::init_sample_timing_table()
       };
     }

-    m_num_output_samples = m_heif_context->get_sequence_duration() / get_duration_in_media_units() * media_timeline.size();
+    uint64_t multiplier = m_heif_context->get_sequence_duration() / get_duration_in_media_units();
+    m_num_output_samples = multiplier * media_timeline.size();
+
+    if (m_heif_context->is_sequence_duration_indefinite()) {
+      // mvhd carries the all-1s sentinel -> editlist repeats forever.
+      m_num_repetitions = std::numeric_limits<uint32_t>::max();
+    }
+    else if (multiplier >= std::numeric_limits<uint32_t>::max()) {
+      // Doesn't fit in the API's uint32_t; treat as effectively infinite.
+      m_num_repetitions = std::numeric_limits<uint32_t>::max();
+    }
+    else {
+      m_num_repetitions = static_cast<uint32_t>(multiplier);
+    }
   }
   else {
     fallback = true;
@@ -1075,6 +1088,7 @@ Error Track::init_sample_timing_table()
   if (fallback) {
     m_presentation_timeline = media_timeline;
     m_num_output_samples = media_timeline.size();
+    m_num_repetitions = 0; // editlist absent or not understood
   }

   return {};
diff --git a/libheif/sequences/track.h b/libheif/sequences/track.h
index 31fd3f5a..494161ca 100644
--- a/libheif/sequences/track.h
+++ b/libheif/sequences/track.h
@@ -184,6 +184,9 @@ public:

   bool end_of_sequence_reached() const;

+  // See m_num_repetitions for the meaning of the return value.
+  uint32_t get_number_of_repetitions() const { return m_num_repetitions; }
+
   // Compute some parameters after all frames have been encoded (for example: track duration).
   virtual Error finalize_track();

@@ -219,6 +222,12 @@ protected:
   std::vector<SampleTiming> m_presentation_timeline;
   uint64_t m_num_output_samples = 0; // Can be larger than the vector. It then repeats the playback.

+  // How many times the media timeline is repeated as dictated by the editlist.
+  // 0  = no editlist / editlist pattern not supported (caller should assume a single playback).
+  // UINT32_MAX = infinite (mvhd duration is the indefinite-sentinel and the editlist is in repeat mode).
+  // N  = the media segment is played N times.
+  uint32_t m_num_repetitions = 0;
+
   // Continuous counting through all repetitions. You have to take the modulo operation to get the
   // index into m_presentation_timeline SampleTiming table.
   // (At 30 fps, this 32 bit integer will overflow in >4 years. I think this is acceptable.)