From 31d406dc7da78ef0e452e0a2ae0921925b6d4324 Mon Sep 17 00:00:00 2001 From: Till Toenshoff Date: Sat, 20 Dec 2025 21:32:26 +0100 Subject: [PATCH 1/7] chore: centralized and removed duplication on fourcc related code --- include/fourcc_utils.hpp | 47 ++++++++++++++++++++++++++++++++++++++++ include/mp4_atoms.hpp | 16 ++------------ src/chapterforge.cpp | 6 +---- src/parser.cpp | 19 ---------------- 4 files changed, 50 insertions(+), 38 deletions(-) create mode 100644 include/fourcc_utils.hpp diff --git a/include/fourcc_utils.hpp b/include/fourcc_utils.hpp new file mode 100644 index 0000000..1176e4c --- /dev/null +++ b/include/fourcc_utils.hpp @@ -0,0 +1,47 @@ +// +// fourcc_utils.hpp +// ChapterForge +// +// Created by Till Toenshoff on 12/9/25. +// Copyright © 2025 Till Toenshoff. All rights reserved. +// + +#pragma once + +#include +#include +#include + +// FourCC helpers. +inline constexpr uint32_t fourcc(const char a, const char b, const char c, const char d) { + return (uint32_t(uint8_t(a)) << 24) | (uint32_t(uint8_t(b)) << 16) | + (uint32_t(uint8_t(c)) << 8) | (uint32_t(uint8_t(d))); +} + +inline constexpr uint32_t fourcc(const char t[4]) { return fourcc(t[0], t[1], t[2], t[3]); } + +inline uint32_t fourcc(const std::string &s) { + if (s.size() < 4) { + throw std::runtime_error("fourcc string too short"); + } + return fourcc(s[0], s[1], s[2], s[3]); +} + +inline bool is_printable_fourcc(uint32_t type) { + for (int i = 0; i < 4; ++i) { + const uint8_t c = static_cast(type >> (24 - 8 * i)); + if (c < 0x20 || c > 0x7E) { + return false; + } + } + return true; +} + +inline std::string fourcc_to_string(uint32_t type) { + std::string s(4, ' '); + s[0] = static_cast((type >> 24) & 0xFF); + s[1] = static_cast((type >> 16) & 0xFF); + s[2] = static_cast((type >> 8) & 0xFF); + s[3] = static_cast(type & 0xFF); + return s; +} diff --git a/include/mp4_atoms.hpp b/include/mp4_atoms.hpp index 0431d35..d008c67 100644 --- a/include/mp4_atoms.hpp +++ b/include/mp4_atoms.hpp @@ -14,25 +14,13 @@ #include #include +#include "fourcc_utils.hpp" + // Forward declaration. class Atom; using AtomPtr = std::unique_ptr; -// FourCC helpers. -inline uint32_t fourcc(const char a, const char b, const char c, const char d) { - return (uint32_t(uint8_t(a)) << 24) | (uint32_t(uint8_t(b)) << 16) | - (uint32_t(uint8_t(c)) << 8) | (uint32_t(uint8_t(d))); -} - -inline uint32_t fourcc(const char t[4]) { return fourcc(t[0], t[1], t[2], t[3]); } - -inline uint32_t fourcc(const std::string &s) { - if (s.size() < 4) - throw std::runtime_error("fourcc string too short"); - return fourcc(s[0], s[1], s[2], s[3]); -} - class Atom { public: uint32_t type = 0; // FourCC diff --git a/src/chapterforge.cpp b/src/chapterforge.cpp index c77efbe..a5bb5be 100644 --- a/src/chapterforge.cpp +++ b/src/chapterforge.cpp @@ -26,6 +26,7 @@ #include "chapter_text_sample.hpp" #include "chapter_image_sample.hpp" #include "mp4a_builder.hpp" +#include "mp4_atoms.hpp" #include "mp4_muxer.hpp" #include "parser.hpp" @@ -85,11 +86,6 @@ inline uint32_t be32(const uint8_t *p) { (static_cast(p[2]) << 8) | static_cast(p[3]); } -constexpr uint32_t fourcc(uint8_t a, uint8_t b, uint8_t c, uint8_t d) { - return (static_cast(a) << 24) | (static_cast(b) << 16) | - (static_cast(c) << 8) | static_cast(d); -} - // Minimal ilst parser to surface top-level metadata into MetadataSet. static void parse_ilst_metadata(const std::vector &ilst, MetadataSet &out) { auto extract_data_box = [](const uint8_t *base, size_t len, diff --git a/src/parser.cpp b/src/parser.cpp index 9987c3f..baf154e 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -28,25 +28,6 @@ constexpr uint64_t kMetaReservedBytes = 4; constexpr uint64_t kHdlrMinPayload = 20; constexpr uint64_t kMaxAtomPayload = 512 * 1024 * 1024; // 512 MB safety bound -static bool is_printable_fourcc(uint32_t type) { - for (int i = 0; i < 4; ++i) { - uint8_t c = (type >> (24 - 8 * i)) & 0xFF; - if (c < 0x20 || c > 0x7E) { - return false; - } - } - return true; -} - -static std::string fourcc_to_string(uint32_t type) { - std::string s(4, ' '); - s[0] = static_cast((type >> 24) & 0xFF); - s[1] = static_cast((type >> 16) & 0xFF); - s[2] = static_cast((type >> 8) & 0xFF); - s[3] = static_cast(type & 0xFF); - return s; -} - } // namespace static void skip(std::istream &in, uint64_t n) { in.seekg(n, std::ios::cur); } From 35bacf5a37a8383cf4ecadbb21f30f5de77f590c Mon Sep 17 00:00:00 2001 From: Till Toenshoff Date: Sat, 20 Dec 2025 21:45:38 +0100 Subject: [PATCH 2/7] fix: adds opt-out for faststart as it is on by default --- README.md | 12 +++++++----- src/main.cpp | 15 +++++++++------ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index cc3c611..3e34a3b 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,8 @@ The overlay tracks the current commit. Update `REF`/`SHA512` in `ports/chapterfo ``` - Write mode: mux chapters/images/URLs into an output M4A. If the input already has metadata (`ilst`), - it is reused by default. Fast-start is off by default; enable with `--faststart`. + it is reused by default. Fast-start is ON by default (moov before mdat); use `--no-faststart` if you + need the legacy layout. - Read mode: extract metadata, chapter titles/URLs/URL-texts, and images from an M4A. The JSON emitted matches the writer input format and is always printed to stdout. Use `--export-jpegs DIR` to dump cover + chapter images alongside the JSON and reference them in the output. @@ -168,9 +169,10 @@ The overlay tracks the current commit. Update `REF`/`SHA512` in `ports/chapterfo `chapterforge::set_log_verbosity(LogVerbosity::Warn|Info|Debug)` or pass `--log-level warn|info|debug` to the CLI. Debug-only logs stay hidden unless you raise the level. - Options: - - `--faststart` (write) Place `moov` before `mdat` for faster playback start. -- `--log-level LEVEL` One of `warn|info|debug`. -- `--export-jpegs DIR` (read) Export cover/chapter JPEGs to `DIR` and reference them in the JSON. + - `--faststart` (write) Explicitly enable fast-start (default). + - `--no-faststart` (write) Disable fast-start; keep `mdat` before `moov`. + - `--log-level LEVEL` One of `warn|info|debug`. + - `--export-jpegs DIR` (read) Export cover/chapter JPEGs to `DIR` and reference them in the JSON. ## Chapters JSON format @@ -190,7 +192,7 @@ ChapterForge consumes a simple JSON document: "chapters": [ { "title": "Introduction", // required - "start_ms": 0, // required: chapter start time in milliseconds + "start_ms": 0, // required: chapter start time in milliseconds (first snaps to 0) "image": "chapter1.jpg", // optional; path relative to the JSON file "url": "https://example.com", // optional; creates a URL text track with HREF "url_text": "Intro link label" // optional; text payload for the URL track (defaults empty) diff --git a/src/main.cpp b/src/main.cpp index 8c28688..aec41c7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -113,11 +113,13 @@ int main(int argc, char **argv) { // Gather positional arguments (non-option). std::vector positional; std::filesystem::path export_dir; - bool fast_start = false; + bool fast_start = true; // Default to fast-start layout. for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if (arg == "--faststart") { fast_start = true; + } else if (arg == "--no-faststart") { + fast_start = false; } else if (arg == "--log-level" && i + 1 < argc) { chapterforge::set_log_verbosity(parse_level(argv[i + 1])); ++i; @@ -134,14 +136,15 @@ int main(int argc, char **argv) { if (positional.empty()) { std::cerr << "ChapterForge " << CHAPTERFORGE_VERSION_DISPLAY << "\n" << "Copyright (c) 2025 Till Toenshoff\n\n" - << "usage for reading:\n" + << "Usage for reading:\n" << " chapterforge [--export-jpegs DIR] " - << "[--log-level warn|info|debug]\n" - << "usage for writing:\n" + << "[--log-level warn|info|debug]\n\n" + << "Usage for writing:\n" << " chapterforge " - << "[--faststart] [--log-level warn|info|debug]\n" + << "[--no-faststart|--faststart] [--log-level warn|info|debug]\n\n" << "Options:\n" - << " --faststart Place 'moov' atom before 'mdat' for faster playback start.\n" + << " --faststart Place 'moov' atom before 'mdat' for faster playback start (default).\n" + << " --no-faststart Write classic layout with 'mdat' before 'moov'.\n" << " --log-level LEVEL Set logging verbosity (default: info).\n" << " --export-jpegs DIR When reading, write chapter images (and cover if any) to DIR.\n" << " JSON is always written to stdout when reading.\n"; From 1976d81311c26379d6d83bb532303b71ff8562b4 Mon Sep 17 00:00:00 2001 From: Till Toenshoff Date: Sat, 20 Dec 2025 22:16:20 +0100 Subject: [PATCH 3/7] fix: github runners have python installed --- .github/workflows/ci.yml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f323a96..a7b80ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y xxd gpac vim-common nlohmann-json3-dev unzip doxygen python3-pip + sudo apt-get install -y xxd gpac vim-common nlohmann-json3-dev unzip doxygen sudo apt-get install -y atomicparsley || true # Prefer distro bento4; fall back to upstream binaries if missing. if ! sudo apt-get install -y bento4; then @@ -59,18 +59,30 @@ jobs: run: | brew update # Prefer Bento4 mp4info/mp4dump; avoid mp4v2 to prevent binary conflicts. - brew install gpac atomicparsley nlohmann-json bento4 doxygen python@3 || true + brew install gpac atomicparsley nlohmann-json bento4 doxygen || true + + - name: Set up Python (macOS) + if: runner.os == 'macOS' + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Set up Python (Linux) + if: runner.os == 'Linux' + uses: actions/setup-python@v5 + with: + python-version: '3.12' - name: Install tools (Windows) if: runner.os == 'Windows' shell: bash run: | + # Python is preinstalled on GitHub runners; keep a known version via setup-python instead of Chocolatey. choco install -y gpac choco install -y vim || true choco install -y nlohmann-json || true choco install -y 7zip || true choco install -y doxygen || true - choco install -y python || true # Bento4: download prebuilt SDK and expose mp4info/mp4dump B4_VER="1-6-0-640" curl -L -o /tmp/bento4.zip https://www.bok.net/Bento4/binaries/Bento4-SDK-${B4_VER}.x86_64-microsoft-win32.zip @@ -81,6 +93,12 @@ jobs: echo "Checking xxd..." which xxd || true + - name: Set up Python (Windows) + if: runner.os == 'Windows' + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Configure shell: bash run: | From 380983cf900bf829043e0240b2819d60790ba4dc Mon Sep 17 00:00:00 2001 From: Till Toenshoff Date: Sat, 20 Dec 2025 23:10:19 +0100 Subject: [PATCH 4/7] chore: lost the old school banner comment dashes --- src/mdat_writer.cpp | 15 ++++----------- src/meta_builder.cpp | 4 ---- src/mp4_atoms.cpp | 18 ++---------------- src/mp4_muxer.cpp | 2 -- src/mp4a_builder.cpp | 6 ------ src/stbl_audio_builder.cpp | 10 ---------- src/stbl_image_builder.cpp | 12 ------------ src/stbl_text_builder.cpp | 10 ---------- src/trak_builder.cpp | 10 ---------- 9 files changed, 6 insertions(+), 81 deletions(-) diff --git a/src/mdat_writer.cpp b/src/mdat_writer.cpp index d8bc5e3..fddeae5 100644 --- a/src/mdat_writer.cpp +++ b/src/mdat_writer.cpp @@ -10,9 +10,7 @@ #include -// ----------------------------------------------------------------------------- -// Write mdat and record sample offsets. -// ----------------------------------------------------------------------------- +// Write the mdat box and collect relative offsets for each track. MdatOffsets write_mdat( std::ofstream &out, const std::vector> &audio_samples, const std::vector>> &text_tracks_samples, @@ -103,9 +101,7 @@ MdatOffsets write_mdat( return result; } -// ----------------------------------------------------------------------------- -// Patch a single stco table. -// ----------------------------------------------------------------------------- +// Update a single stco table with absolute offsets based on the mdat payload start. void patch_stco_table(Atom *stco, const std::vector &offsets, uint64_t mdat_payload_start) { if (!stco) { @@ -134,8 +130,7 @@ void patch_stco_table(Atom *stco, const std::vector &offsets, } } -// ----------------------------------------------------------------------------- -// Patch all stco atoms (order: audio, text tracks..., image) +// Patch all stco tables (audio, text tracks, images) found under moov. void patch_all_stco(Atom *moov, const MdatOffsets &offs, bool patch_audio) { if (!moov) { return; } auto stcos = moov->find("stco"); @@ -152,9 +147,7 @@ void patch_all_stco(Atom *moov, const MdatOffsets &offs, bool patch_audio) { } } -// ----------------------------------------------------------------------------- -// Compute offsets only (no writing), given a payload start. -// ----------------------------------------------------------------------------- +// Compute offsets without writing an mdat (used for fast layout calculations). MdatOffsets compute_mdat_offsets( uint64_t payload_start, const std::vector> &audio_samples, const std::vector>> &text_tracks_samples, diff --git a/src/meta_builder.cpp b/src/meta_builder.cpp index a69fb8b..1ce5620 100644 --- a/src/meta_builder.cpp +++ b/src/meta_builder.cpp @@ -40,7 +40,6 @@ std::unique_ptr build_ilst_item(const std::string &fourcc, const std::vect return item; } -// ------------------------------------------------------------------------- std::unique_ptr build_ilst(std::vector> items) { auto ilst = Atom::create("ilst"); @@ -52,7 +51,6 @@ std::unique_ptr build_ilst(std::vector> items) { return ilst; } -// ------------------------------------------------------------------------- std::unique_ptr build_meta(std::unique_ptr ilst) { auto meta = Atom::create("meta"); @@ -115,9 +113,7 @@ static std::unique_ptr build_cover_item(const std::vector &cover) return item; } -// ----------------------------------------------------------------------------- // Build udta/meta/ilst container (Apple-style) -// ----------------------------------------------------------------------------- std::unique_ptr build_meta_atom(const MetadataSet &meta) { // // ilst. diff --git a/src/mp4_atoms.cpp b/src/mp4_atoms.cpp index 379f5cc..05e4d65 100644 --- a/src/mp4_atoms.cpp +++ b/src/mp4_atoms.cpp @@ -8,23 +8,15 @@ #include "mp4_atoms.hpp" -// ----------------------------------------------------------------------------- -// Factory. -// ----------------------------------------------------------------------------- +// Factory helpers. AtomPtr Atom::create(const char t[4]) { return std::make_unique(t); } - AtomPtr Atom::create(uint32_t t) { return std::make_unique(t); } - AtomPtr Atom::create(const std::string &t) { return std::make_unique(fourcc(t)); } -// ----------------------------------------------------------------------------- -// Add child. -// ----------------------------------------------------------------------------- +// Child management. void Atom::add(AtomPtr child) { children.push_back(std::move(child)); } -// ----------------------------------------------------------------------------- // Recursive find. -// ----------------------------------------------------------------------------- std::vector Atom::find(const std::string &t) { std::vector result; @@ -42,9 +34,7 @@ std::vector Atom::find(const std::string &t) { return result; } -// ----------------------------------------------------------------------------- // Compute recursive box size. -// ----------------------------------------------------------------------------- void Atom::fix_size_recursive() { // Start with MP4 header: 8 bytes (size + type) uint32_t total = 8; @@ -61,14 +51,10 @@ void Atom::fix_size_recursive() { box_size = total; } -// ----------------------------------------------------------------------------- // Return box size. -// ----------------------------------------------------------------------------- uint32_t Atom::size() const { return box_size; } -// ----------------------------------------------------------------------------- // Write atom to file. -// ----------------------------------------------------------------------------- void Atom::write(std::ofstream &out) const { uint32_t s = box_size; diff --git a/src/mp4_muxer.cpp b/src/mp4_muxer.cpp index 4391f47..654aaee 100644 --- a/src/mp4_muxer.cpp +++ b/src/mp4_muxer.cpp @@ -349,9 +349,7 @@ TestDurationInfo compute_durations_for_test( } // namespace chapterforge::testing #endif -// ----------------------------------------------------------------------------- // Complete MP4 writer. -// ----------------------------------------------------------------------------- bool write_mp4(const std::string &output_path, const AacExtractResult &aac, const std::vector &text_chapters, const std::vector &image_chapters, Mp4aConfig audio_cfg, diff --git a/src/mp4a_builder.cpp b/src/mp4a_builder.cpp index dd266c1..38de3eb 100644 --- a/src/mp4a_builder.cpp +++ b/src/mp4a_builder.cpp @@ -37,10 +37,8 @@ static void write_descr_len_padded4(std::vector &out, uint8_t len) { out.push_back(len); } -// ----------------------------------------------------------------------------- // Build DecoderSpecificInfo (AudioSpecificConfig) // ISO/IEC 14496-3 Table 1.13. -// ----------------------------------------------------------------------------- static std::vector build_audio_specific_config(uint8_t audio_object_type, uint8_t sampling_freq_index, uint8_t channel_config) { @@ -60,9 +58,7 @@ static std::vector build_audio_specific_config(uint8_t audio_object_typ return asc; } -// ----------------------------------------------------------------------------- // Build ESDS box. -// ----------------------------------------------------------------------------- static std::unique_ptr build_esds(const Mp4aConfig &cfg) { auto esds = Atom::create("esds"); auto &p = esds->payload; @@ -103,9 +99,7 @@ static std::unique_ptr build_esds(const Mp4aConfig &cfg) { return esds; } -// ----------------------------------------------------------------------------- // Build mp4a sample entry. -// ----------------------------------------------------------------------------- std::unique_ptr build_mp4a(const Mp4aConfig &cfg) { auto mp4a = Atom::create("mp4a"); auto &p = mp4a->payload; diff --git a/src/stbl_audio_builder.cpp b/src/stbl_audio_builder.cpp index 737b3d1..19501f3 100644 --- a/src/stbl_audio_builder.cpp +++ b/src/stbl_audio_builder.cpp @@ -12,9 +12,7 @@ #include "stsd_builder.hpp" -// ----------------------------------------------------------------------------- // Build stts: each AAC frame = 1024 PCM samples in AAC-LC. -// ----------------------------------------------------------------------------- static std::unique_ptr build_stts(uint32_t sample_count) { auto stts = Atom::create("stts"); auto &p = stts->payload; @@ -32,9 +30,7 @@ static std::unique_ptr build_stts(uint32_t sample_count) { return stts; } -// ----------------------------------------------------------------------------- // Build stsc from a chunk plan. -// ----------------------------------------------------------------------------- static std::unique_ptr build_stsc(const std::vector &chunk_sizes) { auto stsc = Atom::create("stsc"); auto &p = stsc->payload; @@ -66,9 +62,7 @@ static std::unique_ptr build_stsc(const std::vector &chunk_sizes return stsc; } -// ----------------------------------------------------------------------------- // Build stsz (sizes of all AAC frames) -// ----------------------------------------------------------------------------- static std::unique_ptr build_stsz(const std::vector &sizes) { auto stsz = Atom::create("stsz"); auto &p = stsz->payload; @@ -85,9 +79,7 @@ static std::unique_ptr build_stsz(const std::vector &sizes) { return stsz; } -// ----------------------------------------------------------------------------- // Build stco (initially empty — patched later) -// ----------------------------------------------------------------------------- static std::unique_ptr build_stco(uint32_t chunk_count) { auto stco = Atom::create("stco"); auto &p = stco->payload; @@ -103,9 +95,7 @@ static std::unique_ptr build_stco(uint32_t chunk_count) { return stco; } -// ----------------------------------------------------------------------------- // Main audio stbl builder. -// ----------------------------------------------------------------------------- std::unique_ptr build_audio_stbl(const Mp4aConfig &cfg, const std::vector &sample_sizes, const std::vector &chunk_sizes, diff --git a/src/stbl_image_builder.cpp b/src/stbl_image_builder.cpp index 65622cb..dce8cdb 100644 --- a/src/stbl_image_builder.cpp +++ b/src/stbl_image_builder.cpp @@ -11,9 +11,7 @@ #include "chapter_timing.hpp" #include "jpeg_entry_builder.hpp" -// ----------------------------------------------------------------------------- // stts (same logic as text track) -// ----------------------------------------------------------------------------- static std::unique_ptr build_stts_img(const std::vector &samples, uint32_t timescale, uint32_t total_ms) { auto stts = Atom::create("stts"); @@ -33,9 +31,7 @@ static std::unique_ptr build_stts_img(const std::vector build_stss_img(uint32_t sample_count) { auto stss = Atom::create("stss"); auto &p = stss->payload; @@ -49,9 +45,7 @@ static std::unique_ptr build_stss_img(uint32_t sample_count) { return stss; } -// ----------------------------------------------------------------------------- // stsc: 1 sample per chunk. -// ----------------------------------------------------------------------------- static std::unique_ptr build_stsc_img(const std::vector &chunk_plan) { auto stsc = Atom::create("stsc"); auto &p = stsc->payload; @@ -92,9 +86,7 @@ static std::unique_ptr build_stsc_img(const std::vector &chunk_p return stsc; } -// ----------------------------------------------------------------------------- // stsz: JPEG sizes. -// ----------------------------------------------------------------------------- static std::unique_ptr build_stsz_img(const std::vector &samples) { auto stsz = Atom::create("stsz"); auto &p = stsz->payload; @@ -112,9 +104,7 @@ static std::unique_ptr build_stsz_img(const std::vector build_stco_img(uint32_t count) { auto stco = Atom::create("stco"); auto &p = stco->payload; @@ -130,9 +120,7 @@ static std::unique_ptr build_stco_img(uint32_t count) { return stco; } -// ----------------------------------------------------------------------------- // MAIN BUILDER. -// ----------------------------------------------------------------------------- std::unique_ptr build_image_stbl(const std::vector &samples, uint32_t track_timescale, uint16_t width, uint16_t height, const std::vector &chunk_plan, diff --git a/src/stbl_text_builder.cpp b/src/stbl_text_builder.cpp index a955509..0d51422 100644 --- a/src/stbl_text_builder.cpp +++ b/src/stbl_text_builder.cpp @@ -41,9 +41,7 @@ static std::vector encode_tx3g_sample(const ChapterTextSample &s) { return out; } -// ----------------------------------------------------------------------------- // stts: durations converted to track timescale. -// ----------------------------------------------------------------------------- static std::unique_ptr build_stts_text(const std::vector &samples, uint32_t timescale, uint32_t total_ms) { auto stts = Atom::create("stts"); @@ -65,9 +63,7 @@ static std::unique_ptr build_stts_text(const std::vector build_stsc_text(const std::vector &chunk_plan) { auto stsc = Atom::create("stsc"); auto &p = stsc->payload; @@ -107,9 +103,7 @@ static std::unique_ptr build_stsc_text(const std::vector &chunk_ return stsc; } -// ----------------------------------------------------------------------------- // stsz: byte size for each text sample. -// ----------------------------------------------------------------------------- static uint32_t encoded_tx3g_size(const ChapterTextSample &s) { uint32_t base = static_cast(s.text.size() + 2); // len(2) + text if (!s.href.empty()) { @@ -136,9 +130,7 @@ static std::unique_ptr build_stsz_text(const std::vector build_stco_text(uint32_t count) { auto stco = Atom::create("stco"); auto &p = stco->payload; @@ -154,9 +146,7 @@ static std::unique_ptr build_stco_text(uint32_t count) { return stco; } -// ----------------------------------------------------------------------------- // Build text chapter stbl. -// ----------------------------------------------------------------------------- std::unique_ptr build_text_stbl(const std::vector &samples, uint32_t track_timescale, const std::vector &chunk_plan, uint32_t total_ms) { diff --git a/src/trak_builder.cpp b/src/trak_builder.cpp index a143c4a..2c5fb80 100644 --- a/src/trak_builder.cpp +++ b/src/trak_builder.cpp @@ -22,9 +22,7 @@ #include "tkhd_builder.hpp" #include "vmhd_builder.hpp" -// ----------------------------------------------------------------------------- // Helper: build tref/chap for audio track. -// ----------------------------------------------------------------------------- static std::unique_ptr build_tref_chap(const std::vector &refs) { auto tref = Atom::create("tref"); auto chap = Atom::create("chap"); @@ -41,9 +39,7 @@ static std::unique_ptr build_tref_chap(const std::vector &refs) return tref; } -// ----------------------------------------------------------------------------- // Audio track. -// ----------------------------------------------------------------------------- std::unique_ptr build_trak_audio(uint32_t track_id, uint32_t timescale, uint64_t duration_ts, std::unique_ptr stbl_audio, const std::vector &chapter_ref_track_ids, @@ -75,9 +71,7 @@ std::unique_ptr build_trak_audio(uint32_t track_id, uint32_t timescale, ui return trak; } -// ----------------------------------------------------------------------------- // Text chapter track. -// ----------------------------------------------------------------------------- std::unique_ptr build_trak_text(uint32_t track_id, uint32_t timescale, uint64_t duration_ts, std::unique_ptr stbl_text, uint64_t tkhd_duration_mvhd, @@ -105,9 +99,7 @@ std::unique_ptr build_trak_text(uint32_t track_id, uint32_t timescale, uin return trak; } -// ----------------------------------------------------------------------------- // Timed metadata track (metadata samples aligned to chapters). -// ----------------------------------------------------------------------------- std::unique_ptr build_trak_metadata(uint32_t track_id, uint32_t timescale, uint64_t duration_ts, std::unique_ptr stbl_metadata, @@ -133,9 +125,7 @@ std::unique_ptr build_trak_metadata(uint32_t track_id, uint32_t timescale, return trak; } -// ----------------------------------------------------------------------------- // Image chapter track. -// ----------------------------------------------------------------------------- std::unique_ptr build_trak_image(uint32_t track_id, uint32_t timescale, uint64_t duration_ts, std::unique_ptr stbl_image, uint16_t width, uint16_t height, uint64_t tkhd_duration_mvhd) { From c0df884a638642f8b49fd1b052656637c139cbd3 Mon Sep 17 00:00:00 2001 From: Till Toenshoff Date: Sat, 20 Dec 2025 23:27:43 +0100 Subject: [PATCH 5/7] chore: refined function comments where useful --- src/meta_builder.cpp | 4 ++++ src/stbl_audio_builder.cpp | 3 ++- src/stbl_image_builder.cpp | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/meta_builder.cpp b/src/meta_builder.cpp index 1ce5620..6c05b30 100644 --- a/src/meta_builder.cpp +++ b/src/meta_builder.cpp @@ -22,6 +22,7 @@ // lang (uint32=0) // +// Create a single ilst entry with a data atom of the given type. std::unique_ptr build_ilst_item(const std::string &fourcc, const std::vector &data, uint32_t data_type) { auto item = Atom::create(fourcc.c_str()); @@ -41,6 +42,7 @@ std::unique_ptr build_ilst_item(const std::string &fourcc, const std::vect } +// Assemble an ilst atom from the supplied items. std::unique_ptr build_ilst(std::vector> items) { auto ilst = Atom::create("ilst"); @@ -52,6 +54,7 @@ std::unique_ptr build_ilst(std::vector> items) { } +// Wrap ilst in a meta atom containing the required handler. std::unique_ptr build_meta(std::unique_ptr ilst) { auto meta = Atom::create("meta"); @@ -114,6 +117,7 @@ static std::unique_ptr build_cover_item(const std::vector &cover) } // Build udta/meta/ilst container (Apple-style) +// Build the full udta/meta/ilst structure for top-level metadata. std::unique_ptr build_meta_atom(const MetadataSet &meta) { // // ilst. diff --git a/src/stbl_audio_builder.cpp b/src/stbl_audio_builder.cpp index 19501f3..c080072 100644 --- a/src/stbl_audio_builder.cpp +++ b/src/stbl_audio_builder.cpp @@ -95,7 +95,7 @@ static std::unique_ptr build_stco(uint32_t chunk_count) { return stco; } -// Main audio stbl builder. +// Build audio stbl from parsed AAC config and sample layout. std::unique_ptr build_audio_stbl(const Mp4aConfig &cfg, const std::vector &sample_sizes, const std::vector &chunk_sizes, @@ -118,6 +118,7 @@ std::unique_ptr build_audio_stbl(const Mp4aConfig &cfg, return stbl; } +// Rehydrate an audio stbl from raw atom payloads (used when reusing source stbl). std::unique_ptr build_audio_stbl_raw(const std::vector &stsd_payload, const std::vector &stts_payload, const std::vector &stsc_payload, diff --git a/src/stbl_image_builder.cpp b/src/stbl_image_builder.cpp index dce8cdb..dd311bb 100644 --- a/src/stbl_image_builder.cpp +++ b/src/stbl_image_builder.cpp @@ -120,7 +120,7 @@ static std::unique_ptr build_stco_img(uint32_t count) { return stco; } -// MAIN BUILDER. +// Build image stbl (jpeg sample entry + timing + chunk tables). std::unique_ptr build_image_stbl(const std::vector &samples, uint32_t track_timescale, uint16_t width, uint16_t height, const std::vector &chunk_plan, From 19ca5f6d8a9f0a94402d49b7140879c465e6d2aa Mon Sep 17 00:00:00 2001 From: Till Toenshoff Date: Sat, 20 Dec 2025 23:30:04 +0100 Subject: [PATCH 6/7] fix: sprinkled retries all over the CI downloads --- .github/workflows/ci.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7b80ea..22baf74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,15 +40,15 @@ jobs: - name: Install tools (Linux) if: runner.os == 'Linux' run: | - sudo apt-get update - sudo apt-get install -y xxd gpac vim-common nlohmann-json3-dev unzip doxygen - sudo apt-get install -y atomicparsley || true + sudo apt-get update -o Acquire::Retries=5 + sudo apt-get install -o Acquire::Retries=5 -y xxd gpac vim-common nlohmann-json3-dev unzip doxygen + sudo apt-get install -o Acquire::Retries=5 -y atomicparsley || true # Prefer distro bento4; fall back to upstream binaries if missing. if ! sudo apt-get install -y bento4; then B4_VER="1-6-0-640" B4_URL="https://www.bok.net/Bento4/binaries/Bento4-SDK-${B4_VER}.x86_64-unknown-linux.zip" echo "Fetching Bento4 from ${B4_URL}" - curl -L -o /tmp/bento4.zip "${B4_URL}" + curl -L --retry 5 --retry-delay 5 --fail --retry-all-errors -o /tmp/bento4.zip "${B4_URL}" sudo unzip -q /tmp/bento4.zip -d /opt/bento4 sudo ln -sf /opt/bento4/Bento4-SDK-${B4_VER}.x86_64-unknown-linux/bin/mp4info /usr/local/bin/mp4info sudo ln -sf /opt/bento4/Bento4-SDK-${B4_VER}.x86_64-unknown-linux/bin/mp4dump /usr/local/bin/mp4dump @@ -57,6 +57,7 @@ jobs: - name: Install tools (macOS) if: runner.os == 'macOS' run: | + export HOMEBREW_CURL_RETRIES=5 brew update # Prefer Bento4 mp4info/mp4dump; avoid mp4v2 to prevent binary conflicts. brew install gpac atomicparsley nlohmann-json bento4 doxygen || true @@ -78,14 +79,17 @@ jobs: shell: bash run: | # Python is preinstalled on GitHub runners; keep a known version via setup-python instead of Chocolatey. + export chocolateyDownloadRetries=5 + export chocolateyDownloadRetrySeconds=5 choco install -y gpac choco install -y vim || true choco install -y nlohmann-json || true choco install -y 7zip || true - choco install -y doxygen || true + # Prefer doxygen.install (available on chocolatey); fall back to doxygen portable if needed. + choco install -y doxygen.install || choco install -y doxygen || true # Bento4: download prebuilt SDK and expose mp4info/mp4dump B4_VER="1-6-0-640" - curl -L -o /tmp/bento4.zip https://www.bok.net/Bento4/binaries/Bento4-SDK-${B4_VER}.x86_64-microsoft-win32.zip + curl -L --retry 5 --retry-delay 5 --fail --retry-all-errors -o /tmp/bento4.zip https://www.bok.net/Bento4/binaries/Bento4-SDK-${B4_VER}.x86_64-microsoft-win32.zip mkdir -p /c/bento4 unzip -q /tmp/bento4.zip -d /c/bento4 echo "/c/bento4/Bento4-SDK-${B4_VER}.x86_64-microsoft-win32/bin" >> $GITHUB_PATH From 00711120ac0835429e8dde3fc952e7e110ce38f2 Mon Sep 17 00:00:00 2001 From: Till Toenshoff Date: Sat, 20 Dec 2025 23:38:50 +0100 Subject: [PATCH 7/7] fix: packaging fix --- .github/workflows/update-packaging.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-packaging.yml b/.github/workflows/update-packaging.yml index 64a15bb..3a0427a 100644 --- a/.github/workflows/update-packaging.yml +++ b/.github/workflows/update-packaging.yml @@ -16,7 +16,8 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - ref: ${{ github.event.repository.default_branch }} + # Always base packaging changes on the default branch (main), not the tag name. + ref: main - name: Determine tag id: vars @@ -83,4 +84,4 @@ jobs: - Update Homebrew formula URL/SHA to ${{ steps.vars.outputs.tag }} - Update vcpkg overlay REF/SHA512 to ${{ steps.vars.outputs.tag }} branch: "bot/update-packaging-${{ steps.vars.outputs.tag }}" - base: ${{ github.event.repository.default_branch }} + base: main