From e75f402564aacf9cef4a8fd1240863bc0397a0be Mon Sep 17 00:00:00 2001 From: Till Toenshoff Date: Sat, 27 Dec 2025 16:12:04 +0100 Subject: [PATCH 1/2] fix: urls are now exposed by AVFoundations loadChapters --- src/mp4_muxer.cpp | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/mp4_muxer.cpp b/src/mp4_muxer.cpp index 654aaee..97a26bd 100644 --- a/src/mp4_muxer.cpp +++ b/src/mp4_muxer.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -197,6 +198,40 @@ static PreparedTextTracks prepare_text_tracks( return prepared; } +// Mirror href values from any auxiliary text tracks onto the primary title track so that +// players that only inspect the chapter title track (e.g., AVFoundation) still surface URLs. +static std::vector merge_href_into_titles( + const std::vector &titles, + const std::vector>> &extra_text_tracks) { + if (titles.empty() || extra_text_tracks.empty()) { + return titles; + } + + std::multimap href_by_start; + for (const auto &track : extra_text_tracks) { + for (const auto &sample : track.second) { + if (!sample.href.empty()) { + href_by_start.emplace(sample.start_ms, sample.href); + } + } + } + if (href_by_start.empty()) { + return titles; + } + + auto merged = titles; + for (auto &t : merged) { + if (!t.href.empty()) { + continue; + } + auto it = href_by_start.find(t.start_ms); + if (it != href_by_start.end()) { + t.href = it->second; + } + } + return merged; +} + static DurationInfo compute_durations(const AacExtractResult &aac, Mp4aConfig &audio_cfg, const std::vector &text_chapters, const std::vector &image_chapters) { @@ -402,7 +437,8 @@ bool write_mp4(const std::string &output_path, const AacExtractResult &aac, const auto &audio_samples = aac.frames; // reuse extracted buffers without copying // Build padded tx3g samples for title and URL tracks. - PreparedTextTracks prepared_text = prepare_text_tracks(text_chapters, extra_text_tracks); + auto primary_with_href = merge_href_into_titles(text_chapters, extra_text_tracks); + PreparedTextTracks prepared_text = prepare_text_tracks(primary_with_href, extra_text_tracks); // // Image samples: JPEG binary data (may be empty if no images were provided) From 62703d1f6908d81e8d1063a1c70e06d4e106038e Mon Sep 17 00:00:00 2001 From: Till Toenshoff Date: Sat, 27 Dec 2025 16:12:35 +0100 Subject: [PATCH 2/2] fix: tightened a test that left me assuming wrong --- tests/avfoundation_urltext.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/avfoundation_urltext.sh b/tests/avfoundation_urltext.sh index 1af0fbe..c778aa8 100755 --- a/tests/avfoundation_urltext.sh +++ b/tests/avfoundation_urltext.sh @@ -133,8 +133,11 @@ for idx, c in enumerate(chapters): # Validate hrefs (if any) show up either in sample payload or extraAttributes dump hrefs = [c.get("url", "") for c in chapters if c.get("url")] for href in hrefs: - if href not in logtxt: - fail(f"Missing href '{href}' in Swift log") + # Require the href to appear in the chapterMetadataGroups extraAttributes (HREF), + # not just in raw text payload dumps. + pattern = rf"item key=.*extra=.*HREF.*{re.escape(href)}" + if not re.search(pattern, logtxt): + fail(f"Missing href '{href}' in chapterMetadataGroups (extraAttributes)") print("AVFoundation URL text OK (strings, timings, hrefs present)") PY