[Matroska] Allow Orphaned Chapter Reading (when Chapter has no EditionID)

Fix: Handle orphan ChapterAtom elements not wrapped in EditionEntry

The Matroska specification requires every ChapterAtom to be inside an
EditionEntry. However, some muxers (older FFmpeg versions, some streaming
tools) produce files with ChapterAtom elements directly under Chapters,
without an EditionEntry wrapper.
MKVToolNix and FFmpeg both handle this case gracefully by treating orphan
atoms as belonging to an implicit default edition. Previously, TagLib
silently ignored these chapters, returning an empty ChapterEditionList.

This change:
- Collects orphan ChapterAtom elements encountered directly under Chapters
- Wraps them in an implicit default edition (UID = 0, isDefault = true,
  isOrdered = false) so they are exposed through the existing
  chapterEditionList() API
- Extracts the atom-parsing logic into a private parseChapterAtom() helper
  to avoid code duplication between the two call sites

No existing behavior is changed - files that already conform to the spec
(chapters inside an EditionEntry) parse identically.
This commit is contained in:
HomerJau
2026-04-26 12:00:48 +10:00
committed by Urs Fleisch
parent 5e1cb4081d
commit e07b956fda

View File

@@ -31,6 +31,48 @@
using namespace TagLib;
namespace {
Matroska::Chapter parseChapterAtom(
const std::unique_ptr<EBML::Element> &atomElement)
{
Matroska::Chapter::UID chapterUid = 0;
Matroska::Chapter::Time chapterTimeStart = 0;
Matroska::Chapter::Time chapterTimeEnd = 0;
List<Matroska::Chapter::Display> chapterDisplays;
bool chapterHidden = false;
const auto chapterAtom = EBML::element_cast<EBML::Element::Id::MkChapterAtom>(atomElement);
for(const auto &chapterChild : *chapterAtom) {
if(const EBML::Element::Id cid = chapterChild->getId(); cid == EBML::Element::Id::MkChapterUID)
chapterUid = EBML::element_cast<EBML::Element::Id::MkChapterUID>(chapterChild)->getValue();
else if(cid == EBML::Element::Id::MkChapterTimeStart)
chapterTimeStart = EBML::element_cast<EBML::Element::Id::MkChapterTimeStart>(chapterChild)->getValue();
else if(cid == EBML::Element::Id::MkChapterTimeEnd)
chapterTimeEnd = EBML::element_cast<EBML::Element::Id::MkChapterTimeEnd>(chapterChild)->getValue();
else if(cid == EBML::Element::Id::MkChapterFlagHidden)
chapterHidden = EBML::element_cast<EBML::Element::Id::MkChapterFlagHidden>(chapterChild)->getValue() != 0;
else if (cid == EBML::Element::Id::MkChapterDisplay) {
const auto display = EBML::element_cast<EBML::Element::Id::MkChapterDisplay>(chapterChild);
String displayString;
String displayLanguage;
for(const auto &displayChild : *display) {
if (const EBML::Element::Id did = displayChild->getId(); did == EBML::Element::Id::MkChapString)
displayString = EBML::element_cast<EBML::Element::Id::MkChapString>(displayChild)->getValue();
else if(did == EBML::Element::Id::MkChapLanguage)
displayLanguage = EBML::element_cast<EBML::Element::Id::MkChapLanguage>(displayChild)->getValue();
}
if(!displayString.isEmpty()) {
chapterDisplays.append(Matroska::Chapter::Display(displayString, displayLanguage));
}
}
}
return Matroska::Chapter(chapterTimeStart, chapterTimeEnd, chapterDisplays,
chapterUid, chapterHidden);
}
} // namespae
EBML::MkChapters::MkChapters(int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkChapters, sizeLength, dataSize, offset)
{
@@ -41,7 +83,7 @@ EBML::MkChapters::MkChapters(Id, int sizeLength, offset_t dataSize, offset_t off
{
}
EBML::MkChapters::MkChapters():
EBML::MkChapters::MkChapters() :
MasterElement(Id::MkChapters, 0, 0, 0)
{
}
@@ -52,7 +94,20 @@ std::unique_ptr<Matroska::Chapters> EBML::MkChapters::parse() const
chapters->setOffset(offset);
chapters->setSize(getSize());
// Collect any orphan ChapterAtom elements not wrapped in an EditionEntry.
// The Matroska spec requires ChapterAtom to be inside an EditionEntry, but
// some muxers produce files with ChapterAtom directly under Chapters.
// MKVToolNix and FFmpeg handle this case by treating the orphan atoms as
// belonging to an implicit default edition.
List<Matroska::Chapter> orphanChapters;
for(const auto &element : elements) {
if(element->getId() == Id::MkChapterAtom) {
if(auto chapter = parseChapterAtom(element); chapter.uid()) {
orphanChapters.append(chapter);
}
continue;
}
if(element->getId() != Id::MkEditionEntry)
continue;
@@ -69,39 +124,8 @@ std::unique_ptr<Matroska::Chapters> EBML::MkChapters::parse() const
else if(id == Id::MkEditionFlagOrdered)
editionIsOrdered = element_cast<Id::MkEditionFlagOrdered>(editionChild)->getValue() != 0;
else if(id == Id::MkChapterAtom) {
Matroska::Chapter::UID chapterUid = 0;
Matroska::Chapter::Time chapterTimeStart = 0;
Matroska::Chapter::Time chapterTimeEnd = 0;
List<Matroska::Chapter::Display> chapterDisplays;
bool chapterHidden = false;
const auto chapterAtom = element_cast<Id::MkChapterAtom>(editionChild);
for(const auto &chapterChild : *chapterAtom) {
if(const Id cid = chapterChild->getId(); cid == Id::MkChapterUID)
chapterUid = element_cast<Id::MkChapterUID>(chapterChild)->getValue();
else if(cid == Id::MkChapterTimeStart)
chapterTimeStart = element_cast<Id::MkChapterTimeStart>(chapterChild)->getValue();
else if(cid == Id::MkChapterTimeEnd)
chapterTimeEnd = element_cast<Id::MkChapterTimeEnd>(chapterChild)->getValue();
else if(cid == Id::MkChapterFlagHidden)
chapterHidden = element_cast<Id::MkChapterFlagHidden>(chapterChild)->getValue() != 0;
else if(cid == Id::MkChapterDisplay) {
const auto display = element_cast<Id::MkChapterDisplay>(chapterChild);
String displayString;
String displayLanguage;
for(const auto &displayChild : *display) {
if(const Id did = displayChild->getId(); did == Id::MkChapString)
displayString = element_cast<Id::MkChapString>(displayChild)->getValue();
else if(did == Id::MkChapLanguage)
displayLanguage = element_cast<Id::MkChapLanguage>(displayChild)->getValue();
}
if(!displayString.isEmpty()) {
chapterDisplays.append(Matroska::Chapter::Display(displayString, displayLanguage));
}
}
}
if(chapterUid) {
editionChapters.append(Matroska::Chapter(
chapterTimeStart, chapterTimeEnd, chapterDisplays, chapterUid, chapterHidden));
if(auto chapter = parseChapterAtom(editionChild); chapter.uid()) {
editionChapters.append(chapter);
}
}
}
@@ -110,5 +134,13 @@ std::unique_ptr<Matroska::Chapters> EBML::MkChapters::parse() const
editionChapters, editionIsDefault, editionIsOrdered, editionUid));
}
}
// If orphan chapters were found, wrap them in an implicit default edition
// so they are not silently lost.
if (!orphanChapters.isEmpty()) {
chapters->addChapterEdition(Matroska::ChapterEdition(
orphanChapters, true, false, 0));
}
return chapters;
}