Commit c2b38c17 for libheif
commit c2b38c17adb1145dac622a5385611ecfcbbd5469
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Thu Dec 18 20:53:50 2025 +0100
[BSD3] write and parse 'ctts' box
diff --git a/libheif/box.cc b/libheif/box.cc
index 99b751a0..b47f289e 100644
--- a/libheif/box.cc
+++ b/libheif/box.cc
@@ -803,6 +803,10 @@ Error Box::read(BitstreamRange& range, std::shared_ptr<Box>* result, const heif_
box = std::make_shared<Box_stts>();
break;
+ case fourcc("ctts"):
+ box = std::make_shared<Box_ctts>();
+ break;
+
case fourcc("stsc"):
box = std::make_shared<Box_stsc>();
break;
diff --git a/libheif/sequences/seq_boxes.cc b/libheif/sequences/seq_boxes.cc
index ed80f120..9b478783 100644
--- a/libheif/sequences/seq_boxes.cc
+++ b/libheif/sequences/seq_boxes.cc
@@ -664,6 +664,176 @@ uint64_t Box_stts::get_total_duration(bool include_last_frame_duration)
}
+Error Box_ctts::parse(BitstreamRange& range, const heif_security_limits* limits)
+{
+ parse_full_box_header(range);
+
+ uint8_t version = get_version();
+
+ if (version > 1) {
+ return unsupported_version_error("ctts");
+ }
+
+ uint32_t entry_count = range.read32();
+
+ if (entry_count > limits->max_sequence_frames) {
+ return {
+ heif_error_Memory_allocation_error,
+ heif_suberror_Security_limit_exceeded,
+ "Security limit for maximum number of sequence frames exceeded"
+ };
+ }
+
+ if (auto err = m_memory_handle.alloc(entry_count * sizeof(OffsetToSample),
+ limits, "the 'ctts' table")) {
+ return err;
+ }
+
+ m_entries.resize(entry_count);
+
+ for (uint32_t i = 0; i < entry_count; i++) {
+ if (range.eof()) {
+ std::stringstream sstr;
+ sstr << "ctts box should contain " << entry_count << " entries, but box only contained "
+ << i << " entries";
+
+ return {
+ heif_error_Invalid_input,
+ heif_suberror_End_of_data,
+ sstr.str()
+ };
+ }
+
+ OffsetToSample entry{};
+ entry.sample_count = range.read32();
+ if (version == 0) {
+ uint32_t offset = range.read32();
+ if (offset > INT32_MAX) {
+ return {
+ heif_error_Unsupported_feature,
+ heif_suberror_Unsupported_parameter,
+ "We don't support offsets > 0x7fff in 'ctts' box."
+ };
+ }
+
+ entry.sample_offset = static_cast<int32_t>(offset);
+ }
+ else if (version == 1) {
+ entry.sample_offset = range.read32s();
+ }
+ else {
+ assert(false);
+ }
+
+ m_entries[i] = entry;
+ }
+
+ return range.get_error();
+}
+
+
+std::string Box_ctts::dump(Indent& indent) const
+{
+ std::ostringstream sstr;
+ sstr << FullBox::dump(indent);
+ for (size_t i = 0; i < m_entries.size(); i++) {
+ sstr << indent << "[" << i << "] : cnt=" << m_entries[i].sample_count << ", offset=" << m_entries[i].sample_offset << "\n";
+ }
+
+ return sstr.str();
+}
+
+
+int32_t Box_ctts::compute_min_offset() const
+{
+ int32_t min_offset = INT32_MAX;
+ for (const auto& entry : m_entries) {
+ min_offset = std::min(min_offset, entry.sample_offset);
+ }
+
+ return min_offset;
+}
+
+
+Error Box_ctts::write(StreamWriter& writer) const
+{
+ size_t box_start = reserve_box_header_space(writer);
+
+ int32_t min_offset;
+
+ if (get_version() == 0) {
+ // shift such that all offsets are >= 0
+ min_offset = compute_min_offset();
+ }
+ else {
+ // do not modify offsets
+ min_offset = 0;
+ }
+
+ writer.write32(static_cast<uint32_t>(m_entries.size()));
+ for (const auto& sample : m_entries) {
+ writer.write32(sample.sample_count);
+ writer.write32s(sample.sample_offset - min_offset);
+ }
+
+ prepend_header(writer, box_start);
+
+ return Error::Ok;
+}
+
+
+int32_t Box_ctts::get_sample_offset(uint32_t sample_idx)
+{
+ size_t i = 0;
+ while (i < m_entries.size()) {
+ if (sample_idx < m_entries[i].sample_count) {
+ return m_entries[i].sample_offset;
+ }
+ else {
+ sample_idx -= m_entries[i].sample_count;
+ }
+ }
+
+ return 0;
+}
+
+
+void Box_ctts::append_sample_offset(int32_t offset)
+{
+ if (m_entries.empty() || m_entries.back().sample_offset != offset) {
+ OffsetToSample entry{};
+ entry.sample_offset = offset;
+ entry.sample_count = 1;
+ m_entries.push_back(entry);
+ return;
+ }
+
+ m_entries.back().sample_count++;
+}
+
+
+bool Box_ctts::is_constant_offset() const
+{
+ return m_entries.empty() || m_entries.size() == 1;
+}
+
+void Box_ctts::derive_box_version()
+{
+ set_version(0);
+}
+
+
+size_t Box_stsc::get_number_of_samples() const
+{
+ size_t total = 0;
+ for (const auto& entry : m_entries) {
+ total += entry.samples_per_chunk;
+ }
+
+ return total;
+}
+
+
Error Box_stsc::parse(BitstreamRange& range, const heif_security_limits* limits)
{
parse_full_box_header(range);
diff --git a/libheif/sequences/seq_boxes.h b/libheif/sequences/seq_boxes.h
index 83a3313b..3f61be58 100644
--- a/libheif/sequences/seq_boxes.h
+++ b/libheif/sequences/seq_boxes.h
@@ -360,6 +360,44 @@ private:
};
+// Composition Time to Sample Box
+class Box_ctts : public FullBox {
+public:
+ Box_ctts()
+ {
+ set_short_type(fourcc("ctts"));
+ }
+
+ std::string dump(Indent&) const override;
+
+ const char* debug_box_name() const override { return "Composition Time to Sample"; }
+
+ Error write(StreamWriter& writer) const override;
+
+ struct OffsetToSample {
+ uint32_t sample_count;
+ int32_t sample_offset; // either uint32_t or int32_t, we assume that all uint32_t values will also fit into int32_t
+ };
+
+ int32_t get_sample_offset(uint32_t sample_idx);
+
+ void append_sample_offset(int32_t offset);
+
+ bool is_constant_offset() const;
+
+ void derive_box_version() override;
+
+ int32_t compute_min_offset() const;
+
+protected:
+ Error parse(BitstreamRange& range, const heif_security_limits*) override;
+
+private:
+ std::vector<OffsetToSample> m_entries;
+ MemoryHandle m_memory_handle;
+};
+
+
// Sample to Chunk Box
class Box_stsc : public FullBox {
public:
@@ -395,6 +433,8 @@ public:
return m_entries.back().samples_per_chunk == 0;
}
+ size_t get_number_of_samples() const;
+
protected:
Error parse(BitstreamRange& range, const heif_security_limits*) override;
diff --git a/libheif/sequences/track.cc b/libheif/sequences/track.cc
index 93be0c77..3d2b52d2 100644
--- a/libheif/sequences/track.cc
+++ b/libheif/sequences/track.cc
@@ -480,6 +480,10 @@ Track::Track(HeifContext* ctx, uint32_t track_id, const TrackOptions* options, u
m_stts = std::make_shared<Box_stts>();
m_stbl->append_child_box(m_stts);
+ m_ctts = std::make_shared<Box_ctts>();
+ m_stbl->append_child_box(m_ctts);
+ // TODO: will only be added when needed
+
m_stsc = std::make_shared<Box_stsc>();
m_stbl->append_child_box(m_stsc);
@@ -785,7 +789,9 @@ void Track::set_sample_description_box(std::shared_ptr<Box> sample_description_b
}
-Error Track::write_sample_data(const std::vector<uint8_t>& raw_data, uint32_t sample_duration, bool is_sync_sample,
+Error Track::write_sample_data(const std::vector<uint8_t>& raw_data, uint32_t sample_duration,
+ int32_t composition_time_offset,
+ bool is_sync_sample,
const heif_tai_timestamp_packet* tai, const std::optional<std::string>& gimi_contentID)
{
m_chunk_data.insert(m_chunk_data.end(), raw_data.begin(), raw_data.end());
@@ -805,6 +811,7 @@ Error Track::write_sample_data(const std::vector<uint8_t>& raw_data, uint32_t sa
}
m_stts->append_sample_duration(sample_duration);
+ m_ctts->append_sample_offset(composition_time_offset);
// --- sample timestamp
diff --git a/libheif/sequences/track.h b/libheif/sequences/track.h
index 5dd8c0b6..d58dd614 100644
--- a/libheif/sequences/track.h
+++ b/libheif/sequences/track.h
@@ -237,6 +237,7 @@ protected:
std::shared_ptr<Box_stsc> m_stsc;
std::shared_ptr<Box_stco> m_stco;
std::shared_ptr<Box_stts> m_stts;
+ std::shared_ptr<Box_ctts> m_ctts; // optional box, TODO: add only if needed
std::shared_ptr<Box_stss> m_stss;
std::shared_ptr<Box_stsz> m_stsz;
std::shared_ptr<Box_elst> m_elst;
@@ -269,7 +270,10 @@ protected:
// Write the actual sample data. `tai` may be null and `gimi_contentID` may be empty.
// In these cases, no timestamp or no contentID will be written, respectively.
- Error write_sample_data(const std::vector<uint8_t>& raw_data, uint32_t sample_duration, bool is_sync_sample,
+ Error write_sample_data(const std::vector<uint8_t>& raw_data,
+ uint32_t sample_duration,
+ int32_t composition_time_offset,
+ bool is_sync_sample,
const heif_tai_timestamp_packet* tai,
const std::optional<std::string>& gimi_contentID);
};
diff --git a/libheif/sequences/track_metadata.cc b/libheif/sequences/track_metadata.cc
index aaf1dedc..3d11bebd 100644
--- a/libheif/sequences/track_metadata.cc
+++ b/libheif/sequences/track_metadata.cc
@@ -177,6 +177,7 @@ Error Track_Metadata::write_raw_metadata(const heif_raw_sequence_sample* raw_sam
Error err = write_sample_data(raw_sample->data,
raw_sample->duration,
+ 0,
true,
raw_sample->timestamp,
raw_sample->gimi_sample_content_id);
diff --git a/libheif/sequences/track_visual.cc b/libheif/sequences/track_visual.cc
index a64f70dd..1b905f0f 100644
--- a/libheif/sequences/track_visual.cc
+++ b/libheif/sequences/track_visual.cc
@@ -628,8 +628,12 @@ Result<bool> Track_Visual::process_encoded_data(heif_encoder* h_encoder)
auto& user_data = m_frame_user_data[frame_number];
+ int32_t decoding_time = static_cast<int32_t>(m_stsc->get_number_of_samples()) * m_sample_duration;
+ int32_t composition_time = static_cast<int32_t>(frame_number) * m_sample_duration;
+
Error err = write_sample_data(data.bitstream,
user_data.sample_duration,
+ composition_time - decoding_time,
data.is_sync_frame,
user_data.tai_timestamp,
user_data.gimi_content_id);