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.)