diff --git a/taglib/CMakeLists.txt b/taglib/CMakeLists.txt index 2651249a..aa304fb3 100644 --- a/taglib/CMakeLists.txt +++ b/taglib/CMakeLists.txt @@ -196,7 +196,9 @@ if(WITH_MP4) mp4/mp4coverart.h mp4/mp4stem.h mp4/mp4itemfactory.h - mp4/mp4chapterlist.h + mp4/mp4chapter.h + mp4/mp4chapterholder.h + mp4/mp4nerochapterlist.h mp4/mp4qtchapterlist.h ) endif() @@ -374,7 +376,9 @@ if(WITH_MP4) mp4/mp4coverart.cpp mp4/mp4stem.cpp mp4/mp4itemfactory.cpp - mp4/mp4chapterlist.cpp + mp4/mp4chapter.cpp + mp4/mp4chapterholder.cpp + mp4/mp4nerochapterlist.cpp mp4/mp4qtchapterlist.cpp ) endif() diff --git a/taglib/mp4/mp4chapter.cpp b/taglib/mp4/mp4chapter.cpp new file mode 100644 index 00000000..baa1a26c --- /dev/null +++ b/taglib/mp4/mp4chapter.cpp @@ -0,0 +1,79 @@ +/************************************************************************** + copyright : (C) 2026 by Ryan Francesconi + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "mp4chapter.h" +#include "tstring.h" + +using namespace TagLib; + +class MP4::Chapter::ChapterPrivate +{ +public: + ChapterPrivate() = default; + ~ChapterPrivate() = default; + String title; + long long startTime {0}; +}; + +MP4::Chapter::Chapter(const String &title, long long startTime) : + d(std::make_unique()) +{ + d->title = title; + d->startTime = startTime; +} + +MP4::Chapter::Chapter(const Chapter &other) : + d(std::make_unique(*other.d)) +{ +} + +MP4::Chapter::Chapter(Chapter &&other) noexcept = default; + +MP4::Chapter::Chapter::~Chapter() = default; + +MP4::Chapter &MP4::Chapter::Chapter::operator=(const Chapter &other) +{ + Chapter(other).swap(*this); + return *this; +} + +MP4::Chapter &MP4::Chapter::Chapter::operator=( + Chapter &&other) noexcept = default; + +void MP4::Chapter::swap(Chapter &other) noexcept +{ + using std::swap; + + swap(d, other.d); +} + +const String &MP4::Chapter::title() const +{ + return d->title; +} + +long long MP4::Chapter::startTime() const +{ + return d->startTime; +} diff --git a/taglib/mp4/mp4chapter.h b/taglib/mp4/mp4chapter.h new file mode 100644 index 00000000..91627bb2 --- /dev/null +++ b/taglib/mp4/mp4chapter.h @@ -0,0 +1,98 @@ +/************************************************************************** + copyright : (C) 2026 by Ryan Francesconi + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_MP4CHAPTER_H +#define TAGLIB_MP4CHAPTER_H + +#include +#include "taglib_export.h" +#include "tlist.h" + +namespace TagLib { + class String; + namespace MP4 { + + /*! + * A single Nero-style chapter marker. + */ + class TAGLIB_EXPORT Chapter { + public: + /*! + * Construct a chapter. + */ + Chapter(const String &title, long long startTime); + + /*! + * Construct a chapter as a copy of \a other. + */ + Chapter(const Chapter &other); + + /*! + * Construct a chapter moving from \a other. + */ + Chapter(Chapter &&other) noexcept; + + /*! + * Destroys this chapter. + */ + ~Chapter(); + + /*! + * Copies the contents of \a other into this object. + */ + Chapter &operator=(const Chapter &other); + + /*! + * Moves the contents of \a other into this object. + */ + Chapter &operator=(Chapter &&other) noexcept; + + /*! + * Exchanges the content of the object with the content of \a other. + */ + void swap(Chapter &other) noexcept; + + /*! + * Returns the title representing the chapter. + */ + const String &title() const; + + /*! + * Returns the start time in milliseconds. + */ + long long startTime() const; + + private: + class ChapterPrivate; + TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE + std::unique_ptr d; + }; + + //! List of chapters. + using ChapterList = List; + + } // namespace MP4 +} // namespace TagLib + +#endif diff --git a/taglib/mp4/mp4chapterholder.cpp b/taglib/mp4/mp4chapterholder.cpp new file mode 100644 index 00000000..8d9c1ddb --- /dev/null +++ b/taglib/mp4/mp4chapterholder.cpp @@ -0,0 +1,44 @@ +/************************************************************************** + copyright : (C) 2006 by Urs Fleisch + email : ufleisch@users.sourceforge.net + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "mp4chapterholder.h" + +using namespace TagLib; + +MP4::ChapterList MP4::ChapterHolder::chapters() const +{ + return chapterList; +} + +void MP4::ChapterHolder::setChapters(const ChapterList &chapters) +{ + chapterList = chapters; + modified = true; +} + +bool MP4::ChapterHolder::isModified() const +{ + return modified; +} diff --git a/taglib/mp4/mp4chapterholder.h b/taglib/mp4/mp4chapterholder.h new file mode 100644 index 00000000..94e8a52f --- /dev/null +++ b/taglib/mp4/mp4chapterholder.h @@ -0,0 +1,110 @@ +/************************************************************************** + copyright : (C) 2006 by Urs Fleisch + email : ufleisch@users.sourceforge.net + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_MP4CHAPTERHOLDER_H +#define TAGLIB_MP4CHAPTERHOLDER_H + +#include "mp4chapter.h" + +namespace TagLib { + class File; + namespace MP4 { + /*! + * Base class to hold chapters and store modified state. + */ + class ChapterHolder { + public: + /*! + * Get list of chapters. + */ + ChapterList chapters() const; + + /*! + * Set list of chapters. + */ + void setChapters(const ChapterList &chapters); + + /*! + * Returns \c true if the list of chapters has been modified. + */ + bool isModified() const; + + protected: + ChapterList chapterList; + bool modified = false; + }; + + /*! + * Lazily fetch list of chapters. + * @tparam T class derived from ChapterHolder and implementing read(File *) + * @param holder unique pointer to holder, initially null + * @param file file with chapters + * @return list of chapters, empty if no chapters found. + */ + template + ChapterList getChaptersLazy(std::unique_ptr &holder, TagLib::File *file) + { + if (!holder) { + holder = std::make_unique(); + holder->read(file); + } + return holder->chapters(); + } + + /*! + * Lazily set a list of chapters. + * @tparam T class derived from ChapterHolder + * @param holder unique pointer to holder, initially null + * @param chapters list of chapters to set + */ + template + void setChaptersLazy(std::unique_ptr &holder, const ChapterList& chapters) + { + if (!holder) { + holder = std::make_unique(); + } + holder->setChapters(chapters); + } + + /*! + * Save a list of chapters if it has been modified. + * @tparam T class derived from ChapterHolder and implementing write(File *) + * @param holder unique pointer to holder, initially null + * @param file file with chapters + * @return true if write successful or not modified. + */ + template + bool saveChaptersIfModified(std::unique_ptr &holder, TagLib::File *file) + { + if(holder && holder->isModified()) { + return holder->write(file); + } + return true; + } + + } // namespace MP4 +} // namespace TagLib + +#endif diff --git a/taglib/mp4/mp4file.cpp b/taglib/mp4/mp4file.cpp index cf760be2..5f8e5bec 100644 --- a/taglib/mp4/mp4file.cpp +++ b/taglib/mp4/mp4file.cpp @@ -30,6 +30,8 @@ #include "tagutils.h" #include "mp4itemfactory.h" +#include "mp4nerochapterlist.h" +#include "mp4qtchapterlist.h" using namespace TagLib; @@ -48,6 +50,8 @@ public: std::unique_ptr tag; std::unique_ptr atoms; std::unique_ptr properties; + std::unique_ptr neroChapterList; + std::unique_ptr qtChapterList; }; //////////////////////////////////////////////////////////////////////////////// @@ -111,6 +115,26 @@ MP4::Properties *MP4::File::audioProperties() const return d->properties.get(); } +MP4::ChapterList MP4::File::neroChapters() +{ + return getChaptersLazy(d->neroChapterList, this); +} + +void MP4::File::setNeroChapters(const ChapterList& chapters) +{ + setChaptersLazy(d->neroChapterList, chapters); +} + +MP4::ChapterList MP4::File::qtChapters() +{ + return getChaptersLazy(d->qtChapterList, this); +} + +void MP4::File::setQtChapters(const ChapterList& chapters) +{ + setChaptersLazy(d->qtChapterList, chapters); +} + void MP4::File::read(bool readProperties) { @@ -148,7 +172,9 @@ MP4::File::save() return false; } - return d->tag->save(); + return d->tag->save() && + saveChaptersIfModified(d->neroChapterList, this) && + saveChaptersIfModified(d->qtChapterList, this); } bool diff --git a/taglib/mp4/mp4file.h b/taglib/mp4/mp4file.h index faf215e4..ba5ce608 100644 --- a/taglib/mp4/mp4file.h +++ b/taglib/mp4/mp4file.h @@ -31,6 +31,7 @@ #include "mp4tag.h" #include "tag.h" #include "mp4properties.h" +#include "mp4chapter.h" namespace TagLib { //! An implementation of MP4 (AAC, ALAC, ...) metadata @@ -130,6 +131,26 @@ namespace TagLib { */ Properties *audioProperties() const override; + /*! + * Returns the Nero style chapters for this file. + */ + ChapterList neroChapters(); + + /*! + * Sets the Nero style chapters for this file. + */ + void setNeroChapters(const ChapterList &chapters); + + /*! + * Returns the QuickTime chapters for this file. + */ + ChapterList qtChapters(); + + /*! + * Sets the QuickTime style chapters for this file. + */ + void setQtChapters(const ChapterList &chapters); + /*! * Save the file. * diff --git a/taglib/mp4/mp4chapterlist.cpp b/taglib/mp4/mp4nerochapterlist.cpp similarity index 71% rename from taglib/mp4/mp4chapterlist.cpp rename to taglib/mp4/mp4nerochapterlist.cpp index 5314e4fa..644e96b1 100644 --- a/taglib/mp4/mp4chapterlist.cpp +++ b/taglib/mp4/mp4nerochapterlist.cpp @@ -22,7 +22,7 @@ * http://www.mozilla.org/MPL/ * ***************************************************************************/ -#include "mp4chapterlist.h" +#include "mp4nerochapterlist.h" #include @@ -36,7 +36,7 @@ namespace { ByteVector renderAtom(const ByteVector &name, const ByteVector &data) { - return ByteVector::fromUInt(static_cast(data.size() + 8)) + name + data; + return ByteVector::fromUInt(data.size() + 8) + name + data; } // Update parent atom sizes along a path when child size changes by delta. @@ -52,11 +52,10 @@ namespace for(auto it = path.begin(); it != itEnd; ++it) { file->seek((*it)->offset()); - long size = file->readBlock(4).toUInt(); - if(size == 1) { + if(const long size = file->readBlock(4).toUInt(); size == 1) { // 64-bit size file->seek(4, TagLib::File::Current); - long long longSize = file->readBlock(8).toLongLong(); + const long long longSize = file->readBlock(8).toLongLong(); file->seek((*it)->offset() + 8); file->writeBlock(ByteVector::fromLongLong(longSize + delta)); } @@ -70,10 +69,10 @@ namespace // Update stco/co64/tfhd chunk offsets when file content shifts. // Mirrors MP4::Tag::updateOffsets(). - void updateChunkOffsets(TagLib::File *file, MP4::Atoms *atoms, + void updateChunkOffsets(TagLib::File *file, const MP4::Atoms *atoms, offset_t delta, offset_t offset) { - if(MP4::Atom *moov = atoms->find("moov")) { + if(const MP4::Atom *moov = atoms->find("moov")) { const MP4::AtomList stco = moov->findall("stco", true); for(const auto &atom : stco) { if(atom->offset() > offset) @@ -113,7 +112,7 @@ namespace } } - if(MP4::Atom *moof = atoms->find("moof")) { + if(const MP4::Atom *moof = atoms->find("moof")) { const MP4::AtomList tfhd = moof->findall("tfhd", true); for(const auto &atom : tfhd) { if(atom->offset() > offset) @@ -135,7 +134,7 @@ namespace // Build the binary payload for a chpl atom (version 1). ByteVector renderChplData(const MP4::ChapterList &chapters) { - unsigned int count = std::min(static_cast(chapters.size()), 255U); + const unsigned int count = std::min(chapters.size(), 255U); ByteVector data; // Version (1 byte) + flags (3 bytes) + reserved (4 bytes) @@ -152,11 +151,11 @@ namespace break; // Start time: 8 bytes big-endian, on-disk format is 100-nanosecond units - data.append(ByteVector::fromLongLong(ch.startTime * 10000LL)); + data.append(ByteVector::fromLongLong(ch.startTime() * 10000LL)); // Title: 1-byte length + UTF-8 bytes (max 255 bytes) - ByteVector titleBytes = ch.title.data(String::UTF8); - unsigned int titleLen = std::min(static_cast(titleBytes.size()), 255U); + ByteVector titleBytes = ch.title().data(String::UTF8); + const unsigned int titleLen = std::min(titleBytes.size(), 255U); data.append(static_cast(titleLen & 0xFF)); if(titleLen > 0) data.append(titleBytes.mid(0, titleLen)); @@ -175,7 +174,7 @@ namespace return chapters; unsigned int pos = 0; - unsigned char version = static_cast(data[pos++]); + const auto version = static_cast(data[pos++]); // Skip flags (3 bytes) pos += 3; @@ -187,13 +186,13 @@ namespace if(pos >= data.size()) return chapters; - unsigned int count = static_cast(data[pos++]); + const unsigned int count = static_cast(data[pos++]); for(unsigned int i = 0; i < count && pos + 9 <= data.size(); ++i) { - long long startTime100ns = data.toLongLong(pos); + const long long startTime100ns = data.toLongLong(pos); pos += 8; - unsigned int titleLen = static_cast(data[pos++]); + const unsigned int titleLen = static_cast(data[pos++]); String title; if(titleLen > 0 && pos + titleLen <= data.size()) { @@ -201,10 +200,7 @@ namespace pos += titleLen; } - MP4::Chapter ch; - ch.startTime = startTime100ns / 10000LL; - ch.title = title; - chapters.append(ch); + chapters.append(MP4::Chapter(title, startTime100ns / 10000LL)); } return chapters; @@ -216,83 +212,61 @@ namespace // public members //////////////////////////////////////////////////////////////////////////////// -MP4::ChapterList -MP4::MP4ChapterList::read(const char *path) +bool MP4::NeroChapterList::read(TagLib::File *file) { - MP4::File file(path, false); - if(!file.isOpen() || !file.isValid()) { - debug("MP4ChapterList::read() -- Could not open file"); - return ChapterList(); + const Atoms atoms(file); + + const Atom *chpl = atoms.find("moov", "udta", "chpl"); + modified = false; + chapterList.clear(); + if(chpl) { + // Read the atom content (skip 8-byte atom header) + file->seek(chpl->offset() + 8); + const ByteVector data = file->readBlock(chpl->length() - 8); + + chapterList = parseChplData(data); + return true; } - - return read(&file); + return false; } -MP4::ChapterList -MP4::MP4ChapterList::read(MP4::File *file) +bool MP4::NeroChapterList::write(TagLib::File *file) { - Atoms atoms(file); + // Writing an empty list is equivalent to removing the chapters. + if(chapterList.isEmpty()) + return remove(file); - Atom *chpl = atoms.find("moov", "udta", "chpl"); - if(!chpl) - return ChapterList(); - - // Read the atom content (skip 8-byte atom header) - file->seek(chpl->offset() + 8); - ByteVector data = file->readBlock(chpl->length() - 8); - - return parseChplData(data); -} - -bool -MP4::MP4ChapterList::write(const char *path, const ChapterList &chapters) -{ - MP4::File file(path, false); - if(!file.isOpen() || !file.isValid() || file.readOnly()) { - debug("MP4ChapterList::write() -- Could not open file for writing"); - return false; - } - - return write(&file, chapters); -} - -bool -MP4::MP4ChapterList::write(MP4::File *file, const ChapterList &chapters) -{ - Atoms atoms(file); + const Atoms atoms(file); if(!atoms.find("moov")) { debug("MP4ChapterList::write() -- No moov atom found"); return false; } - ByteVector chplPayload = renderChplData(chapters); - ByteVector chplAtom = renderAtom("chpl", chplPayload); + const ByteVector chplPayload = renderChplData(chapterList); + const ByteVector chplAtom = renderAtom("chpl", chplPayload); - Atom *existingChpl = atoms.find("moov", "udta", "chpl"); - - if(existingChpl) { + if(const Atom *existingChpl = atoms.find("moov", "udta", "chpl")) { // Replace existing chpl atom - offset_t offset = existingChpl->offset(); - offset_t oldLength = existingChpl->length(); - offset_t delta = static_cast(chplAtom.size()) - oldLength; + const offset_t offset = existingChpl->offset(); + const offset_t oldLength = existingChpl->length(); + const offset_t delta = static_cast(chplAtom.size()) - oldLength; file->insert(chplAtom, offset, oldLength); if(delta != 0) { // Update parent sizes: moov and udta - AtomList parentPath = atoms.path("moov", "udta", "chpl"); + const AtomList parentPath = atoms.path("moov", "udta", "chpl"); updateParentSizes(file, parentPath, delta, 1); // ignore chpl itself updateChunkOffsets(file, &atoms, delta, offset); } } else { // Need to insert a new chpl atom - AtomList udtaPath = atoms.path("moov", "udta"); - if(udtaPath.size() == 2) { + if(AtomList udtaPath = atoms.path("moov", "udta"); udtaPath.size() == 2) { // udta exists -- insert chpl at the beginning of udta's content - offset_t insertOffset = udtaPath.back()->offset() + 8; + const offset_t insertOffset = udtaPath.back()->offset() + 8; file->insert(chplAtom, insertOffset, 0); updateParentSizes(file, udtaPath, chplAtom.size()); @@ -300,7 +274,7 @@ MP4::MP4ChapterList::write(MP4::File *file, const ChapterList &chapters) } else { // No udta -- insert udta + chpl at the beginning of moov's content - ByteVector udtaAtom = renderAtom("udta", chplAtom); + const ByteVector udtaAtom = renderAtom("udta", chplAtom); AtomList moovPath = atoms.path("moov"); if(moovPath.isEmpty()) { @@ -308,7 +282,7 @@ MP4::MP4ChapterList::write(MP4::File *file, const ChapterList &chapters) return false; } - offset_t insertOffset = moovPath.back()->offset() + 8; + const offset_t insertOffset = moovPath.back()->offset() + 8; file->insert(udtaAtom, insertOffset, 0); updateParentSizes(file, moovPath, udtaAtom.size()); @@ -316,39 +290,29 @@ MP4::MP4ChapterList::write(MP4::File *file, const ChapterList &chapters) } } + modified = false; return true; } -bool -MP4::MP4ChapterList::remove(const char *path) +bool MP4::NeroChapterList::remove(TagLib::File *file) { - MP4::File file(path, false); - if(!file.isOpen() || !file.isValid() || file.readOnly()) { - debug("MP4ChapterList::remove() -- Could not open file for writing"); - return false; - } + const Atoms atoms(file); + chapterList.clear(); + modified = false; - return remove(&file); -} - -bool -MP4::MP4ChapterList::remove(MP4::File *file) -{ - Atoms atoms(file); - - Atom *chpl = atoms.find("moov", "udta", "chpl"); + const Atom *chpl = atoms.find("moov", "udta", "chpl"); if(!chpl) { // No chpl atom -- nothing to remove return true; } - offset_t offset = chpl->offset(); - offset_t length = chpl->length(); + const offset_t offset = chpl->offset(); + const offset_t length = chpl->length(); file->removeBlock(offset, length); // Update parent sizes with negative delta - AtomList parentPath = atoms.path("moov", "udta", "chpl"); + const AtomList parentPath = atoms.path("moov", "udta", "chpl"); updateParentSizes(file, parentPath, -length, 1); // ignore chpl itself updateChunkOffsets(file, &atoms, -length, offset); diff --git a/taglib/mp4/mp4chapterlist.h b/taglib/mp4/mp4nerochapterlist.h similarity index 65% rename from taglib/mp4/mp4chapterlist.h rename to taglib/mp4/mp4nerochapterlist.h index 1bb9ed83..3d2ac4c2 100644 --- a/taglib/mp4/mp4chapterlist.h +++ b/taglib/mp4/mp4nerochapterlist.h @@ -25,52 +25,25 @@ #ifndef TAGLIB_MP4CHAPTERLIST_H #define TAGLIB_MP4CHAPTERLIST_H -#include "tlist.h" -#include "tstring.h" -#include "taglib_export.h" -#include "mp4file.h" +#include "mp4chapterholder.h" namespace TagLib { + class File; namespace MP4 { - /*! - * A single Nero-style chapter marker. - */ - struct TAGLIB_EXPORT Chapter { - long long startTime; //!< Start time in milliseconds - String title; - }; - - using ChapterList = List; - /*! * Reads, writes, and removes Nero-style chapter markers (chpl atom) * from MP4 files. Operates independently of MP4::Tag -- the chpl atom * lives at moov/udta/chpl, a sibling of the metadata ilst path. */ - class TAGLIB_EXPORT MP4ChapterList + class NeroChapterList : public ChapterHolder { public: - /*! - * Reads chapter markers from the MP4 file at \a path. - * Returns an empty list if the file has no chpl atom. - */ - static ChapterList read(const char *path); - /*! * Reads chapter markers from the already-opened \a file. - * Avoids a second open when the caller already has the file open. - * Returns an empty list if the file has no chpl atom. + * Returns \c false if the file has no chpl atom. */ - static ChapterList read(MP4::File *file); - - /*! - * Writes chapter markers to the MP4 file at \a path, - * replacing any existing chpl atom. The chapter count is - * capped at 255 (Nero format limit). - * Returns \c true on success. - */ - static bool write(const char *path, const ChapterList &chapters); + bool read(TagLib::File *file); /*! * Writes chapter markers to the already-opened \a file, @@ -78,19 +51,13 @@ namespace TagLib { * The chapter count is capped at 255 (Nero format limit). * Returns \c true on success. */ - static bool write(MP4::File *file, const ChapterList &chapters); - - /*! - * Removes the chpl atom from the MP4 file at \a path. - * Returns \c true on success, or if no chpl atom exists. - */ - static bool remove(const char *path); + bool write(TagLib::File *file); /*! * Removes the chpl atom from the already-opened \a file. * Returns \c true on success, or if no chpl atom exists. */ - static bool remove(MP4::File *file); + bool remove(TagLib::File *file); }; } // namespace MP4 diff --git a/taglib/mp4/mp4qtchapterlist.cpp b/taglib/mp4/mp4qtchapterlist.cpp index ccdeccfb..22359c4a 100644 --- a/taglib/mp4/mp4qtchapterlist.cpp +++ b/taglib/mp4/mp4qtchapterlist.cpp @@ -24,7 +24,6 @@ #include "mp4qtchapterlist.h" -#include #include #include @@ -41,7 +40,7 @@ namespace ByteVector renderAtom(const ByteVector &name, const ByteVector &data) { - return ByteVector::fromUInt(static_cast(data.size() + 8)) + name + data; + return ByteVector::fromUInt(data.size() + 8) + name + data; } //! Build a full-box (version + flags) atom. @@ -68,10 +67,9 @@ namespace for(auto it = path.begin(); it != itEnd; ++it) { file->seek((*it)->offset()); - long size = file->readBlock(4).toUInt(); - if(size == 1) { + if(const long size = file->readBlock(4).toUInt(); size == 1) { file->seek(4, TagLib::File::Current); - long long longSize = file->readBlock(8).toLongLong(); + const long long longSize = file->readBlock(8).toLongLong(); file->seek((*it)->offset() + 8); file->writeBlock(ByteVector::fromLongLong(longSize + delta)); } @@ -82,10 +80,10 @@ namespace } } - void updateChunkOffsets(TagLib::File *file, MP4::Atoms *atoms, + void updateChunkOffsets(TagLib::File *file, const MP4::Atoms *atoms, offset_t delta, offset_t offset) { - if(MP4::Atom *moov = atoms->find("moov")) { + if(const MP4::Atom *moov = atoms->find("moov")) { const MP4::AtomList stco = moov->findall("stco", true); for(const auto &atom : stco) { if(atom->offset() > offset) @@ -125,7 +123,7 @@ namespace } } - if(MP4::Atom *moof = atoms->find("moof")) { + if(const MP4::Atom *moof = atoms->find("moof")) { const MP4::AtomList tfhd = moof->findall("tfhd", true); for(const auto &atom : tfhd) { if(atom->offset() > offset) @@ -154,14 +152,14 @@ namespace }; //! Reads movie-level info from mvhd. - MovieInfo readMovieInfo(TagLib::File *file, MP4::Atoms *atoms) + MovieInfo readMovieInfo(TagLib::File *file, const MP4::Atoms *atoms) { MovieInfo info; MP4::Atom *moov = atoms->find("moov"); if(!moov) return info; - MP4::Atom *mvhd = moov->find("mvhd"); + const MP4::Atom *mvhd = moov->find("mvhd"); if(!mvhd) return info; @@ -170,7 +168,7 @@ namespace if(data.size() < 8 + 4) return info; - unsigned char version = static_cast(data[8]); + const auto version = static_cast(data[8]); long long timescale, duration; if(version == 1 && data.size() >= 8 + 28) { timescale = data.toUInt(28U); @@ -201,10 +199,10 @@ namespace }; //! Finds the first audio track (hdlr handler_type == "soun"). - TrackInfo findAudioTrack(TagLib::File *file, MP4::Atoms *atoms) + TrackInfo findAudioTrack(TagLib::File *file, const MP4::Atoms *atoms) { TrackInfo info; - MP4::Atom *moov = atoms->find("moov"); + const MP4::Atom *moov = atoms->find("moov"); if(!moov) return info; @@ -214,16 +212,16 @@ namespace if(!hdlr) continue; file->seek(hdlr->offset()); - ByteVector data = file->readBlock(hdlr->length()); // handler_type is at offset 16 from atom start (8 header + 4 version/flags + 4 pre_defined) - if(data.containsAt("soun", 16)) { + if(ByteVector data = file->readBlock(hdlr->length()); + data.containsAt("soun", 16)) { info.trak = trak; // Read track_id from tkhd - if(MP4::Atom *tkhd = trak->find("tkhd")) { + if(const MP4::Atom *tkhd = trak->find("tkhd")) { file->seek(tkhd->offset()); ByteVector tkhdData = file->readBlock(tkhd->length()); - unsigned char version = static_cast(tkhdData[8]); - if(version == 1 && tkhdData.size() >= 8 + 20 + 4) { + if(const auto version = static_cast(tkhdData[8]); + version == 1 && tkhdData.size() >= 8 + 20 + 4) { info.trackId = tkhdData.toUInt(28U); } else if(tkhdData.size() >= 8 + 12 + 4) { @@ -237,45 +235,45 @@ namespace } //! Reads the next_track_ID from mvhd. - unsigned int getNextTrackId(TagLib::File *file, MP4::Atoms *atoms) + unsigned int getNextTrackId(TagLib::File *file, const MP4::Atoms *atoms) { MP4::Atom *moov = atoms->find("moov"); if(!moov) return 0; - MP4::Atom *mvhd = moov->find("mvhd"); + const MP4::Atom *mvhd = moov->find("mvhd"); if(!mvhd) return 0; file->seek(mvhd->offset()); ByteVector data = file->readBlock(mvhd->length()); - unsigned char version = static_cast(data[8]); + const auto version = static_cast(data[8]); // next_track_ID is the last 4 bytes of mvhd // version 0: header(8) + version/flags(4) + creation(4) + modification(4) // + timescale(4) + duration(4) + ... total fixed = 108 bytes // version 1: header(8) + version/flags(4) + creation(8) + modification(8) // + timescale(4) + duration(8) + ... total fixed = 120 bytes - unsigned int nextTrackIdOffset = (version == 1) ? 120 - 4 : 108 - 4; - if(data.size() >= nextTrackIdOffset + 4) + if(const unsigned int nextTrackIdOffset = version == 1 ? 120 - 4 : 108 - 4; + data.size() >= nextTrackIdOffset + 4) return data.toUInt(nextTrackIdOffset); return 0; } //! Writes next_track_ID in mvhd. - void setNextTrackId(TagLib::File *file, MP4::Atoms *atoms, unsigned int newId) + void setNextTrackId(TagLib::File *file, const MP4::Atoms *atoms, unsigned int newId) { MP4::Atom *moov = atoms->find("moov"); if(!moov) return; - MP4::Atom *mvhd = moov->find("mvhd"); + const MP4::Atom *mvhd = moov->find("mvhd"); if(!mvhd) return; file->seek(mvhd->offset()); ByteVector data = file->readBlock(mvhd->length()); - unsigned char version = static_cast(data[8]); + const auto version = static_cast(data[8]); - unsigned int nextTrackIdOffset = (version == 1) ? 120 - 4 : 108 - 4; - if(data.size() >= nextTrackIdOffset + 4) { + if(const unsigned int nextTrackIdOffset = version == 1 ? 120 - 4 : 108 - 4; + data.size() >= nextTrackIdOffset + 4) { file->seek(mvhd->offset() + nextTrackIdOffset); file->writeBlock(ByteVector::fromUInt(newId)); } @@ -285,41 +283,40 @@ namespace //! Finds an existing chapter track by scanning for tref/chap in the audio track. //! tref is NOT in TagLib's container list, so we read it manually. - MP4::Atom *findChapterTrak(TagLib::File *file, MP4::Atoms *atoms, - MP4::Atom *audioTrak) + MP4::Atom *findChapterTrak(TagLib::File *file, const MP4::Atoms *atoms, + const MP4::Atom *audioTrak) { if(!audioTrak) return nullptr; - MP4::Atom *moov = atoms->find("moov"); + const MP4::Atom *moov = atoms->find("moov"); if(!moov) return nullptr; for(const auto &child : audioTrak->children()) { if(child->name() == "tref") { file->seek(child->offset() + 8); - offset_t trefEnd = child->offset() + child->length(); + const offset_t trefEnd = child->offset() + child->length(); while(file->tell() + 8 <= trefEnd) { - offset_t boxStart = file->tell(); + const offset_t boxStart = file->tell(); ByteVector header = file->readBlock(8); if(header.size() < 8) break; - unsigned int boxSize = header.toUInt(); + const unsigned int boxSize = header.toUInt(); if(boxSize < 8) break; - ByteVector boxName = header.mid(4, 4); - - if(boxName == "chap" && boxSize >= 12) { + if(ByteVector boxName = header.mid(4, 4); + boxName == "chap" && boxSize >= 12) { ByteVector refData = file->readBlock(boxSize - 8); - unsigned int refTrackId = refData.toUInt(); + const unsigned int refTrackId = refData.toUInt(); const MP4::AtomList allTraks = moov->findall("trak"); for(const auto &t : allTraks) { - MP4::Atom *tkhd = t->find("tkhd"); + const MP4::Atom *tkhd = t->find("tkhd"); if(!tkhd) continue; @@ -328,7 +325,7 @@ namespace if(tkhdData.size() < 24) continue; - unsigned char version = static_cast(tkhdData[8]); + const auto version = static_cast(tkhdData[8]); unsigned int tid; if(version == 1 && tkhdData.size() >= 32) { tid = tkhdData.toUInt(28U); @@ -359,8 +356,8 @@ namespace //! Builds a single text sample: 2-byte big-endian length + UTF-8 text + encd atom. ByteVector buildTextSample(const String &title) { - ByteVector utf8 = title.data(String::UTF8); - unsigned int textLen = static_cast(utf8.size()); + const ByteVector utf8 = title.data(String::UTF8); + const unsigned int textLen = utf8.size(); ByteVector sample; sample.append(ByteVector::fromShort(static_cast(textLen))); @@ -384,7 +381,7 @@ namespace { std::vector sizes; for(const auto &ch : chapters) { - unsigned int textLen = static_cast(ch.title.data(String::UTF8).size()); + const unsigned int textLen = ch.title().data(String::UTF8).size(); sizes.push_back(2 + textLen + encdAtomSize); } return sizes; @@ -424,7 +421,7 @@ namespace }; ByteVector sampleEntry; - unsigned int entrySize = 8 + sizeof(entryBody); + constexpr unsigned int entrySize = 8 + sizeof(entryBody); sampleEntry.append(ByteVector::fromUInt(entrySize)); sampleEntry.append(ByteVector("text", 4)); sampleEntry.append(ByteVector(reinterpret_cast(entryBody), @@ -443,7 +440,7 @@ namespace ByteVector buildStts(const MP4::ChapterList &chapters, unsigned int timescale, long long durationMs) { - unsigned int count = static_cast(chapters.size()); + const unsigned int count = chapters.size(); if(count == 0) return ByteVector(); @@ -453,7 +450,7 @@ namespace static_cast(timeMs) * static_cast(timescale) / 1000.0 + 0.5); }; - unsigned int totalDuration = static_cast( + const auto totalDuration = static_cast( static_cast(durationMs) * static_cast(timescale) / 1000.0 + 0.5); // Build per-sample durations @@ -462,10 +459,10 @@ namespace for(unsigned int i = 0; i < count; ++i, ++it) { auto next = it; ++next; - unsigned int startTs = toTimescale(it->startTime); + const unsigned int startTs = toTimescale(it->startTime()); unsigned int dur; if(next != chapters.end()) { - unsigned int nextTs = toTimescale(next->startTime); + const unsigned int nextTs = toTimescale(next->startTime()); dur = nextTs - startTs; } else { @@ -479,7 +476,7 @@ namespace // AVFoundation requires this layout rather than run-length encoding. ByteVector payload; payload.append(ByteVector::fromUInt(count)); - for(auto d : durations) { + for(const auto d : durations) { payload.append(ByteVector::fromUInt(1)); // sample count payload.append(ByteVector::fromUInt(d)); // sample delta } @@ -493,7 +490,7 @@ namespace ByteVector payload; payload.append(ByteVector::fromUInt(0)); // default_sample_size = 0 (per-sample) payload.append(ByteVector::fromUInt(static_cast(sampleSizes.size()))); - for(auto sz : sampleSizes) + for(const auto sz : sampleSizes) payload.append(ByteVector::fromUInt(sz)); return renderFullBox("stsz", 0, 0, payload); } @@ -535,12 +532,12 @@ namespace offset_t textDataOffset, unsigned int movieDuration) { - unsigned int count = static_cast(chapters.size()); - unsigned int totalDuration = static_cast( + unsigned int count = chapters.size(); + auto totalDuration = static_cast( static_cast(durationMs) * static_cast(timescale) / 1000.0 + 0.5); // Single chunk offset -- all samples are contiguous starting at textDataOffset - unsigned int chunkOffset = static_cast(textDataOffset); + auto chunkOffset = static_cast(textDataOffset); // -- tkhd (track header) -- // version 0: 8 header + 4 ver/flags + 4 creation + 4 modification @@ -692,7 +689,7 @@ namespace { ByteVector chapData; chapData.append(ByteVector::fromUInt(chapterTrackId)); - ByteVector chap = renderAtom("chap", chapData); + const ByteVector chap = renderAtom("chap", chapData); return renderAtom("tref", chap); } @@ -708,7 +705,7 @@ namespace { ChapterTrackInfo info; - MP4::Atom *mdhd = chapterTrak->find("mdia", "mdhd"); + const MP4::Atom *mdhd = chapterTrak->find("mdia", "mdhd"); if(!mdhd) return info; @@ -717,8 +714,8 @@ namespace if(data.size() < 8 + 4) return info; - unsigned char version = static_cast(data[8]); - if(version == 1 && data.size() >= 40) { + if(const auto version = static_cast(data[8]); + version == 1 && data.size() >= 40) { // v1 mdhd: header(8) + ver/flags(4) + creation(8) + modification(8) // + timescale(4)@28 + duration(8)@32 + lang(2) + pre(2) = 44 info.timescale = data.toUInt(28U); @@ -742,16 +739,16 @@ namespace std::vector readStts(TagLib::File *file, MP4::Atom *chapterTrak) { std::vector entries; - MP4::Atom *stts = chapterTrak->find("mdia", "minf", "stbl", "stts"); + const MP4::Atom *stts = chapterTrak->find("mdia", "minf", "stbl", "stts"); if(!stts) return entries; file->seek(stts->offset() + 12); // skip header(8) + version/flags(4) - ByteVector data = file->readBlock(stts->length() - 12); + const ByteVector data = file->readBlock(stts->length() - 12); if(data.size() < 4) return entries; - unsigned int count = data.toUInt(); + const unsigned int count = data.toUInt(); unsigned int pos = 4; for(unsigned int i = 0; i < count && pos + 8 <= data.size(); ++i) { SttsEntry e; @@ -767,16 +764,16 @@ namespace std::vector readStco(TagLib::File *file, MP4::Atom *chapterTrak) { std::vector offsets; - MP4::Atom *stco = chapterTrak->find("mdia", "minf", "stbl", "stco"); + const MP4::Atom *stco = chapterTrak->find("mdia", "minf", "stbl", "stco"); if(!stco) return offsets; file->seek(stco->offset() + 12); - ByteVector data = file->readBlock(stco->length() - 12); + const ByteVector data = file->readBlock(stco->length() - 12); if(data.size() < 4) return offsets; - unsigned int count = data.toUInt(); + const unsigned int count = data.toUInt(); unsigned int pos = 4; for(unsigned int i = 0; i < count && pos + 4 <= data.size(); ++i) { offsets.push_back(data.toUInt(pos)); @@ -796,12 +793,12 @@ namespace SampleSizeInfo readStsz(TagLib::File *file, MP4::Atom *chapterTrak) { SampleSizeInfo info; - MP4::Atom *stsz = chapterTrak->find("mdia", "minf", "stbl", "stsz"); + const MP4::Atom *stsz = chapterTrak->find("mdia", "minf", "stbl", "stsz"); if(!stsz) return info; file->seek(stsz->offset() + 12); - ByteVector data = file->readBlock(stsz->length() - 12); + const ByteVector data = file->readBlock(stsz->length() - 12); if(data.size() < 8) return info; @@ -825,7 +822,7 @@ namespace MP4::Atom *chapterTrak, const SampleSizeInfo &sizeInfo) { - std::vector chunkOffsets = readStco(file, chapterTrak); + const std::vector chunkOffsets = readStco(file, chapterTrak); if(chunkOffsets.empty()) return {}; @@ -837,12 +834,11 @@ namespace }; std::vector stscEntries; - MP4::Atom *stsc = chapterTrak->find("mdia", "minf", "stbl", "stsc"); - if(stsc) { + if(const MP4::Atom *stsc = chapterTrak->find("mdia", "minf", "stbl", "stsc")) { file->seek(stsc->offset() + 12); - ByteVector data = file->readBlock(stsc->length() - 12); - if(data.size() >= 4) { - unsigned int entryCount = data.toUInt(); + if(const ByteVector data = file->readBlock(stsc->length() - 12); + data.size() >= 4) { + const unsigned int entryCount = data.toUInt(); unsigned int pos = 4; for(unsigned int i = 0; i < entryCount && pos + 12 <= data.size(); ++i) { StscEntry e; @@ -862,16 +858,16 @@ namespace // Resolve per-sample offsets by walking chunks std::vector sampleOffsets; - unsigned int totalChunks = static_cast(chunkOffsets.size()); + const auto totalChunks = static_cast(chunkOffsets.size()); unsigned int sampleIndex = 0; for(unsigned int chunkIdx = 0; chunkIdx < totalChunks; ++chunkIdx) { // Find which stsc entry applies to this chunk (1-based) - unsigned int chunkNum = chunkIdx + 1; + const unsigned int chunkNum = chunkIdx + 1; unsigned int samplesInChunk = stscEntries[0].samplesPerChunk; - for(unsigned int e = 0; e < stscEntries.size(); ++e) { - if(stscEntries[e].firstChunk <= chunkNum) { - samplesInChunk = stscEntries[e].samplesPerChunk; + for(const auto & stscEntry : stscEntries) { + if(stscEntry.firstChunk <= chunkNum) { + samplesInChunk = stscEntry.samplesPerChunk; } else { break; @@ -898,11 +894,11 @@ namespace String readTextSample(TagLib::File *file, unsigned int offset, unsigned int maxSize) { file->seek(offset); - ByteVector data = file->readBlock(maxSize); + const ByteVector data = file->readBlock(maxSize); if(data.size() < 2) return String(); - unsigned int textLen = data.toUShort(); + const unsigned int textLen = data.toUShort(); if(textLen == 0 || textLen + 2 > data.size()) return String(); @@ -914,25 +910,25 @@ namespace //! Removes the tref atom from the audio track. //! Updates trak size, parent sizes, and chunk offsets. //! audioTrak's in-memory children list is NOT modified (caller re-parses if needed). - void removeAudioTref(TagLib::File *file, MP4::Atoms *atoms, MP4::Atom *audioTrak) + void removeAudioTref(TagLib::File *file, const MP4::Atoms *atoms, const MP4::Atom *audioTrak) { for(const auto &child : audioTrak->children()) { if(child->name() != "tref") continue; - offset_t trefOff = child->offset(); - offset_t trefLen = child->length(); + const offset_t trefOff = child->offset(); + const offset_t trefLen = child->length(); file->removeBlock(trefOff, trefLen); // Fix audio trak size on disk file->seek(audioTrak->offset()); - unsigned int trakSize = file->readBlock(4).toUInt(); + const unsigned int trakSize = file->readBlock(4).toUInt(); file->seek(audioTrak->offset()); file->writeBlock(ByteVector::fromUInt( static_cast(trakSize - trefLen))); - MP4::AtomList moovPath = atoms->path("moov"); + const MP4::AtomList moovPath = atoms->path("moov"); updateParentSizes(file, moovPath, -trefLen); updateChunkOffsets(file, atoms, -trefLen, trefOff); return; @@ -945,41 +941,31 @@ namespace // public members //////////////////////////////////////////////////////////////////////////////// -MP4::ChapterList -MP4::MP4QTChapterList::read(const char *path) +bool MP4::QtChapterList::read(TagLib::File *file) { - MP4::File file(path, false); - if(!file.isOpen() || !file.isValid()) - return ChapterList(); - - return read(&file); -} - -MP4::ChapterList -MP4::MP4QTChapterList::read(MP4::File *file) -{ - Atoms atoms(file); + const Atoms atoms(file); + modified = false; + chapterList.clear(); TrackInfo audio = findAudioTrack(file, &atoms); if(!audio.trak) - return ChapterList(); + return false; Atom *chapterTrak = findChapterTrak(file, &atoms, audio.trak); if(!chapterTrak) - return ChapterList(); + return false; ChapterTrackInfo trackInfo = readChapterTrackInfo(file, chapterTrak); if(trackInfo.timescale == 0) - return ChapterList(); + return false; - std::vector sttsEntries = readStts(file, chapterTrak); - SampleSizeInfo sizeInfo = readStsz(file, chapterTrak); - std::vector offsets = resolveSampleOffsets(file, chapterTrak, sizeInfo); + const std::vector sttsEntries = readStts(file, chapterTrak); + const SampleSizeInfo sizeInfo = readStsz(file, chapterTrak); + const std::vector offsets = resolveSampleOffsets(file, chapterTrak, sizeInfo); if(offsets.empty()) - return ChapterList(); + return false; - ChapterList chapters; unsigned int sampleIndex = 0; long long currentTime = 0; @@ -994,14 +980,11 @@ MP4::MP4QTChapterList::read(MP4::File *file) String title = readTextSample(file, offsets[sampleIndex], sampleSize); - long long startTimeMs = static_cast( + const auto startTimeMs = static_cast( static_cast(currentTime) * 1000.0 / static_cast(trackInfo.timescale) + 0.5); - Chapter ch; - ch.startTime = startTimeMs; - ch.title = title; - chapters.append(ch); + chapterList.append(Chapter(title, startTimeMs)); currentTime += entry.sampleDelta; sampleIndex++; @@ -1010,33 +993,20 @@ MP4::MP4QTChapterList::read(MP4::File *file) // Strip a leading dummy chapter (empty title at time 0) that was inserted // during write to preserve non-zero first-chapter start times. - if(chapters.size() > 1) { - const Chapter &first = chapters.front(); - if(first.startTime == 0 && first.title.isEmpty()) { - chapters.erase(chapters.begin()); + if(chapterList.size() > 1) { + if(const Chapter &first = chapterList.front(); + first.startTime() == 0 && first.title().isEmpty()) { + chapterList.erase(chapterList.begin()); } } - return chapters; + return true; } -bool -MP4::MP4QTChapterList::write(const char *path, const ChapterList &chapters) -{ - MP4::File file(path, false); - if(!file.isOpen() || !file.isValid() || file.readOnly()) { - debug("MP4QTChapterList::write() -- Could not open file for writing"); - return false; - } - - return write(&file, chapters); -} - -bool -MP4::MP4QTChapterList::write(MP4::File *file, const ChapterList &chapters) +bool MP4::QtChapterList::write(TagLib::File *file) { // Writing an empty list is equivalent to removing the chapter track. - if(chapters.isEmpty()) + if(chapterList.isEmpty()) return remove(file); // ---- Phase 1: Parse and gather info ---- @@ -1048,12 +1018,12 @@ MP4::MP4QTChapterList::write(MP4::File *file, const ChapterList &chapters) return false; } - MovieInfo movieInfo = readMovieInfo(file, &atoms); + const MovieInfo movieInfo = readMovieInfo(file, &atoms); if(movieInfo.durationMs <= 0) { debug("MP4QTChapterList::write() -- Could not determine file duration"); return false; } - long long durationMs = movieInfo.durationMs; + const long long durationMs = movieInfo.durationMs; TrackInfo audio = findAudioTrack(file, &atoms); if(!audio.trak) { @@ -1065,15 +1035,14 @@ MP4::MP4QTChapterList::write(MP4::File *file, const ChapterList &chapters) // Pointer to the Atoms object we'll use for the insert phase. // Points to `atoms` for fresh writes, or `cleanAtoms` after cleanup. - Atoms *activeAtoms = &atoms; + const Atoms *activeAtoms = &atoms; // Optional second parse -- only constructed when replacing existing chapters. std::unique_ptr cleanAtoms; - Atom *existingChapter = findChapterTrak(file, &atoms, audio.trak); - if(existingChapter) { + if(Atom *existingChapter = findChapterTrak(file, &atoms, audio.trak)) { // Remove chapter trak FIRST (higher offset in file). - offset_t chapterOff = existingChapter->offset(); - offset_t chapterLen = existingChapter->length(); + const offset_t chapterOff = existingChapter->offset(); + const offset_t chapterLen = existingChapter->length(); // Remove from in-memory tree so updateChunkOffsets skips its stco. moov->removeChild(existingChapter); @@ -1081,7 +1050,7 @@ MP4::MP4QTChapterList::write(MP4::File *file, const ChapterList &chapters) file->removeBlock(chapterOff, chapterLen); - AtomList moovPath = atoms.path("moov"); + const AtomList moovPath = atoms.path("moov"); updateParentSizes(file, moovPath, -chapterLen); updateChunkOffsets(file, &atoms, -chapterLen, chapterOff); @@ -1109,32 +1078,29 @@ MP4::MP4QTChapterList::write(MP4::File *file, const ChapterList &chapters) // QT chapter tracks always start at media time 0. If the first chapter has a // non-zero start time, prepend a dummy chapter at time 0 with an empty title // so the absolute positions are preserved as stts durations. - ChapterList workingChapters(chapters); - if(!workingChapters.isEmpty() && workingChapters.front().startTime > 0) { - Chapter dummy; - dummy.startTime = 0; - dummy.title = String(); - workingChapters.prepend(dummy); + ChapterList workingChapters(chapterList); + if(!workingChapters.isEmpty() && workingChapters.front().startTime() > 0) { + workingChapters.prepend(Chapter(String(), 0)); } - unsigned int nextId = getNextTrackId(file, activeAtoms); - unsigned int chapterTrackId = nextId > 0 ? nextId : audio.trackId + 1; + const unsigned int nextId = getNextTrackId(file, activeAtoms); + const unsigned int chapterTrackId = nextId > 0 ? nextId : audio.trackId + 1; constexpr unsigned int timescale = 1000; - std::vector sampleSizes = calculateSampleSizes(workingChapters); + const std::vector sampleSizes = calculateSampleSizes(workingChapters); // Build tref/chap atom for audio track - ByteVector trefAtom = buildTref(chapterTrackId); + const ByteVector trefAtom = buildTref(chapterTrackId); // Two-pass build for chapter trak: first to measure size, then with correct stco offsets. - ByteVector trakMeasure = buildChapterTrak( + const ByteVector trakMeasure = buildChapterTrak( chapterTrackId, timescale, durationMs, workingChapters, sampleSizes, 0, movieInfo.duration); - offset_t totalInsert = static_cast(trefAtom.size() + trakMeasure.size()); + const auto totalInsert = static_cast(trefAtom.size() + trakMeasure.size()); // Text samples go inside an mdat atom at EOF. stco offsets point past the 8-byte mdat header. - offset_t textDataOffset = file->length() + totalInsert + 8; + const offset_t textDataOffset = file->length() + totalInsert + 8; // Build final trak with correct stco offsets pointing to where text data will land. - ByteVector trakAtom = buildChapterTrak( + const ByteVector trakAtom = buildChapterTrak( chapterTrackId, timescale, durationMs, workingChapters, sampleSizes, textDataOffset, movieInfo.duration); @@ -1144,19 +1110,19 @@ MP4::MP4QTChapterList::write(MP4::File *file, const ChapterList &chapters) // Insert at the end of the audio trak boundary. // tref is logically inside audio trak; chapter trak is logically after it. - offset_t insertOffset = audio.trak->offset() + audio.trak->length(); + const offset_t insertOffset = audio.trak->offset() + audio.trak->length(); file->insert(combinedPayload, insertOffset, 0); // Fix audio trak size on disk -- only tref goes inside file->seek(audio.trak->offset()); - unsigned int audioTrakSize = file->readBlock(4).toUInt(); - unsigned int newAudioTrakSize = static_cast(audioTrakSize + trefAtom.size()); + const unsigned int audioTrakSize = file->readBlock(4).toUInt(); + const unsigned int newAudioTrakSize = audioTrakSize + trefAtom.size(); file->seek(audio.trak->offset()); file->writeBlock(ByteVector::fromUInt(newAudioTrakSize)); // Fix moov size -- both tref and chapter trak are inside moov - AtomList moovPath = activeAtoms->path("moov"); + const AtomList moovPath = activeAtoms->path("moov"); updateParentSizes(file, moovPath, combinedPayload.size()); // Fix existing chunk offsets -- only the ORIGINAL atom tree is iterated, @@ -1167,10 +1133,10 @@ MP4::MP4QTChapterList::write(MP4::File *file, const ChapterList &chapters) ByteVector textSamples; for(const auto &ch : workingChapters) { - textSamples.append(buildTextSample(ch.title)); + textSamples.append(buildTextSample(ch.title())); } // Wrap text samples in an mdat atom so players can find them. - ByteVector mdatAtom = renderAtom("mdat", textSamples); + const ByteVector mdatAtom = renderAtom("mdat", textSamples); file->seek(0, TagLib::File::End); file->writeBlock(mdatAtom); @@ -1178,30 +1144,20 @@ MP4::MP4QTChapterList::write(MP4::File *file, const ChapterList &chapters) // ---- Phase 5: Update mvhd next_track_ID ---- // mvhd is before insertOffset, so its offset is unchanged. - unsigned int currentNextId = getNextTrackId(file, activeAtoms); - if(chapterTrackId >= currentNextId) { + if(const unsigned int currentNextId = getNextTrackId(file, activeAtoms); + chapterTrackId >= currentNextId) { setNextTrackId(file, activeAtoms, chapterTrackId + 1); } + modified = false; return true; } -bool -MP4::MP4QTChapterList::remove(const char *path) -{ - MP4::File file(path, false); - if(!file.isOpen() || !file.isValid() || file.readOnly()) { - debug("MP4QTChapterList::remove() -- Could not open file for writing"); - return false; - } - - return remove(&file); -} - -bool -MP4::MP4QTChapterList::remove(MP4::File *file) +bool MP4::QtChapterList::remove(TagLib::File *file) { Atoms atoms(file); + chapterList.clear(); + modified = false; TrackInfo audio = findAudioTrack(file, &atoms); if(!audio.trak) @@ -1216,8 +1172,8 @@ MP4::MP4QTChapterList::remove(MP4::File *file) return false; // Remove chapter trak FIRST (higher offset in file). - offset_t chapterOff = chapterTrak->offset(); - offset_t chapterLen = chapterTrak->length(); + const offset_t chapterOff = chapterTrak->offset(); + const offset_t chapterLen = chapterTrak->length(); // Remove from in-memory tree so updateChunkOffsets skips its stco. moov->removeChild(chapterTrak); @@ -1225,7 +1181,7 @@ MP4::MP4QTChapterList::remove(MP4::File *file) file->removeBlock(chapterOff, chapterLen); - AtomList moovPath = atoms.path("moov"); + const AtomList moovPath = atoms.path("moov"); updateParentSizes(file, moovPath, -chapterLen); updateChunkOffsets(file, &atoms, -chapterLen, chapterOff); diff --git a/taglib/mp4/mp4qtchapterlist.h b/taglib/mp4/mp4qtchapterlist.h index af0d6b4c..c2900156 100644 --- a/taglib/mp4/mp4qtchapterlist.h +++ b/taglib/mp4/mp4qtchapterlist.h @@ -25,9 +25,10 @@ #ifndef TAGLIB_MP4QTCHAPTERLIST_H #define TAGLIB_MP4QTCHAPTERLIST_H -#include "mp4chapterlist.h" +#include "mp4chapterholder.h" namespace TagLib { + class File; namespace MP4 { /*! @@ -46,52 +47,29 @@ namespace TagLib { * \c MP4ChapterList so that existing \c Chapter / \c ChapterList * types can be shared. */ - class TAGLIB_EXPORT MP4QTChapterList + class QtChapterList : public ChapterHolder { public: /*! * Reads chapter markers from the QuickTime chapter track in the - * MP4 file at \a path. Returns an empty list if the file has no - * chapter track (i.e. no \c tref/chap reference to a text track). + * already-opened \a file. + * Returns \c false if the file has no chapter track. */ - static ChapterList read(const char *path); - - /*! - * Reads chapter markers from the QuickTime chapter track in the - * already-opened \a file. Avoids a second open when the caller - * already has the file open. - * Returns an empty list if the file has no chapter track. - */ - static ChapterList read(MP4::File *file); - - /*! - * Writes chapter markers as a QuickTime chapter track to the MP4 - * file at \a path, replacing any existing chapter track. The - * file's duration is read internally from the movie header. - * Returns \c true on success. - */ - static bool write(const char *path, const ChapterList &chapters); + bool read(TagLib::File *file); /*! * Writes chapter markers as a QuickTime chapter track to the * already-opened \a file, replacing any existing chapter track. * Returns \c true on success. */ - static bool write(MP4::File *file, const ChapterList &chapters); - - /*! - * Removes the QuickTime chapter track and its \c tref/chap - * reference from the MP4 file at \a path. - * Returns \c true on success, or if no chapter track exists. - */ - static bool remove(const char *path); + bool write(TagLib::File *file); /*! * Removes the QuickTime chapter track and its \c tref/chap * reference from the already-opened \a file. * Returns \c true on success, or if no chapter track exists. */ - static bool remove(MP4::File *file); + bool remove(TagLib::File *file); }; } // namespace MP4 diff --git a/tests/test_mp4.cpp b/tests/test_mp4.cpp index 1e6b8899..27496975 100644 --- a/tests/test_mp4.cpp +++ b/tests/test_mp4.cpp @@ -34,8 +34,6 @@ #include "mp4atom.h" #include "mp4file.h" #include "mp4itemfactory.h" -#include "mp4chapterlist.h" -#include "mp4qtchapterlist.h" #include "plainfile.h" #include #include "utils.h" @@ -115,8 +113,6 @@ class TestMP4 : public CppUnit::TestFixture CPPUNIT_TEST(testQTChapterListOverwrite); CPPUNIT_TEST(testQTChapterListTimestampPrecision); CPPUNIT_TEST(testQTChapterListNonZeroFirstChapter); - CPPUNIT_TEST(testChapterListFileAPI); - CPPUNIT_TEST(testQTChapterListFileAPI); CPPUNIT_TEST_SUITE_END(); public: @@ -888,6 +884,7 @@ public: CPPUNIT_ASSERT_EQUAL(String("TITLE"), f.tag()->title()); } } + void testChapterListWrite() { ScopedFileCopy copy("no-tags", ".m4a"); @@ -895,59 +892,47 @@ public: // File should have no chapters initially { - MP4::ChapterList chapters = MP4::MP4ChapterList::read(filename.c_str()); + MP4::File f(filename.c_str()); + MP4::ChapterList chapters = f.neroChapters(); CPPUNIT_ASSERT(chapters.isEmpty()); } // Write chapters { - MP4::ChapterList chapters; - MP4::Chapter ch1; - ch1.startTime = 0; - ch1.title = "Introduction"; - chapters.append(ch1); - - MP4::Chapter ch2; - ch2.startTime = 30000LL; // 30 seconds in ms - ch2.title = "Main Content"; - chapters.append(ch2); - - MP4::Chapter ch3; - ch3.startTime = 60000LL; // 60 seconds in ms - ch3.title = "Conclusion"; - chapters.append(ch3); - - CPPUNIT_ASSERT(MP4::MP4ChapterList::write(filename.c_str(), chapters)); + MP4::File f(filename.c_str()); + f.setNeroChapters(MP4::ChapterList{ + MP4::Chapter("Introduction", 0), + MP4::Chapter("Main Content", 30000LL), + MP4::Chapter("Conclusion", 60000LL) + }); + CPPUNIT_ASSERT(f.save()); } // Read back and verify { - MP4::ChapterList chapters = MP4::MP4ChapterList::read(filename.c_str()); + MP4::File f(filename.c_str()); + MP4::ChapterList chapters = f.neroChapters(); CPPUNIT_ASSERT_EQUAL(3U, chapters.size()); - CPPUNIT_ASSERT_EQUAL(0LL, chapters[0].startTime); - CPPUNIT_ASSERT_EQUAL(String("Introduction"), chapters[0].title); - CPPUNIT_ASSERT_EQUAL(30000LL, chapters[1].startTime); - CPPUNIT_ASSERT_EQUAL(String("Main Content"), chapters[1].title); - CPPUNIT_ASSERT_EQUAL(60000LL, chapters[2].startTime); - CPPUNIT_ASSERT_EQUAL(String("Conclusion"), chapters[2].title); - } + CPPUNIT_ASSERT_EQUAL(0LL, chapters[0].startTime()); + CPPUNIT_ASSERT_EQUAL(String("Introduction"), chapters[0].title()); + CPPUNIT_ASSERT_EQUAL(30000LL, chapters[1].startTime()); + CPPUNIT_ASSERT_EQUAL(String("Main Content"), chapters[1].title()); + CPPUNIT_ASSERT_EQUAL(60000LL, chapters[2].startTime()); + CPPUNIT_ASSERT_EQUAL(String("Conclusion"), chapters[2].title()); - // Overwrite with different chapters - { - MP4::ChapterList chapters; - MP4::Chapter ch1; - ch1.startTime = 0; - ch1.title = "Part One"; - chapters.append(ch1); - - CPPUNIT_ASSERT(MP4::MP4ChapterList::write(filename.c_str(), chapters)); + // Overwrite with different chapters + f.setNeroChapters(MP4::ChapterList{ + MP4::Chapter("Part One", 0) + }); + CPPUNIT_ASSERT(f.save()); } // Verify overwrite { - MP4::ChapterList chapters = MP4::MP4ChapterList::read(filename.c_str()); + MP4::File f(filename.c_str()); + MP4::ChapterList chapters = f.neroChapters(); CPPUNIT_ASSERT_EQUAL(1U, chapters.size()); - CPPUNIT_ASSERT_EQUAL(String("Part One"), chapters[0].title); + CPPUNIT_ASSERT_EQUAL(String("Part One"), chapters[0].title()); } } @@ -958,32 +943,34 @@ public: // Write chapters { - MP4::ChapterList chapters; - MP4::Chapter ch1; - ch1.startTime = 0; - ch1.title = "Chapter 1"; - chapters.append(ch1); - - CPPUNIT_ASSERT(MP4::MP4ChapterList::write(filename.c_str(), chapters)); + MP4::File f(filename.c_str()); + f.setNeroChapters(MP4::ChapterList{ + MP4::Chapter("Chapter 1", 0) + }); + CPPUNIT_ASSERT(f.save()); } // Verify written { - MP4::ChapterList chapters = MP4::MP4ChapterList::read(filename.c_str()); + MP4::File f(filename.c_str()); + MP4::ChapterList chapters = f.neroChapters(); CPPUNIT_ASSERT_EQUAL(1U, chapters.size()); - } - // Remove chapters - CPPUNIT_ASSERT(MP4::MP4ChapterList::remove(filename.c_str())); + // Remove chapters + f.setNeroChapters(MP4::ChapterList()); + CPPUNIT_ASSERT(f.save()); + } // Verify removed { - MP4::ChapterList chapters = MP4::MP4ChapterList::read(filename.c_str()); + MP4::File f(filename.c_str()); + MP4::ChapterList chapters = f.neroChapters(); CPPUNIT_ASSERT(chapters.isEmpty()); - } - // Remove from file with no chapters should also succeed - CPPUNIT_ASSERT(MP4::MP4ChapterList::remove(filename.c_str())); + // Remove from file with no chapters should also succeed + f.setNeroChapters(MP4::ChapterList()); + CPPUNIT_ASSERT(f.save()); + } } void testChapterListWithExistingTags() @@ -998,51 +985,47 @@ public: CPPUNIT_ASSERT(f.isValid()); originalArtist = f.tag()->artist(); CPPUNIT_ASSERT(!originalArtist.isEmpty()); - } - // Write chapters - { - MP4::ChapterList chapters; - MP4::Chapter ch1; - ch1.startTime = 0; - ch1.title = "Intro"; - chapters.append(ch1); - - MP4::Chapter ch2; - ch2.startTime = 10000LL; // 10 seconds in ms - ch2.title = "Verse"; - chapters.append(ch2); - - CPPUNIT_ASSERT(MP4::MP4ChapterList::write(filename.c_str(), chapters)); + // Write chapters + f.setNeroChapters(MP4::ChapterList{ + MP4::Chapter("Intro", 0), + MP4::Chapter("Verse", 10000LL)}); + CPPUNIT_ASSERT(f.save()); } // Verify chapters are written AND existing tags are preserved { - MP4::ChapterList chapters = MP4::MP4ChapterList::read(filename.c_str()); - CPPUNIT_ASSERT_EQUAL(2U, chapters.size()); - CPPUNIT_ASSERT_EQUAL(String("Intro"), chapters[0].title); - CPPUNIT_ASSERT_EQUAL(String("Verse"), chapters[1].title); - MP4::File f(filename.c_str()); CPPUNIT_ASSERT(f.isValid()); + MP4::ChapterList chapters = f.neroChapters(); + CPPUNIT_ASSERT_EQUAL(2U, chapters.size()); + CPPUNIT_ASSERT_EQUAL(String("Intro"), chapters[0].title()); + CPPUNIT_ASSERT_EQUAL(String("Verse"), chapters[1].title()); CPPUNIT_ASSERT_EQUAL(originalArtist, f.tag()->artist()); + + // Remove chapters and verify tags still survive + f.setNeroChapters(MP4::ChapterList()); + CPPUNIT_ASSERT(f.save()); } - // Remove chapters and verify tags still survive - CPPUNIT_ASSERT(MP4::MP4ChapterList::remove(filename.c_str())); { MP4::File f(filename.c_str()); CPPUNIT_ASSERT(f.isValid()); CPPUNIT_ASSERT_EQUAL(originalArtist, f.tag()->artist()); + CPPUNIT_ASSERT(f.neroChapters().isEmpty()); } } void testChapterListReadEmpty() { // Reading from a file with no chpl atom should return empty list - MP4::ChapterList chapters = MP4::MP4ChapterList::read( - TEST_FILE_PATH_C("no-tags.m4a")); - CPPUNIT_ASSERT(chapters.isEmpty()); + ScopedFileCopy copy("no-tags", ".m4a"); + string filename = copy.fileName(); + + { + MP4::File f(filename.c_str()); + CPPUNIT_ASSERT(f.neroChapters().isEmpty()); + } } void testQTChapterListWrite() @@ -1052,41 +1035,33 @@ public: // File should have no QT chapters initially { - MP4::ChapterList chapters = MP4::MP4QTChapterList::read(filename.c_str()); + MP4::File f(filename.c_str()); + MP4::ChapterList chapters = f.qtChapters(); CPPUNIT_ASSERT(chapters.isEmpty()); } // Write chapters (times in 100-nanosecond units) { - MP4::ChapterList chapters; - MP4::Chapter ch1; - ch1.startTime = 0; - ch1.title = "Intro"; - chapters.append(ch1); - - MP4::Chapter ch2; - ch2.startTime = 15000LL; // 15 seconds in ms - ch2.title = "Verse"; - chapters.append(ch2); - - MP4::Chapter ch3; - ch3.startTime = 30000LL; // 30 seconds in ms - ch3.title = "Outro"; - chapters.append(ch3); - - CPPUNIT_ASSERT(MP4::MP4QTChapterList::write(filename.c_str(), chapters)); + MP4::File f(filename.c_str()); + f.setQtChapters(MP4::ChapterList{ + MP4::Chapter("Intro", 0), + MP4::Chapter("Verse", 15000LL), + MP4::Chapter("Outro", 30000LL) + }); + CPPUNIT_ASSERT(f.save()); } // Read back and verify { - MP4::ChapterList chapters = MP4::MP4QTChapterList::read(filename.c_str()); + MP4::File f(filename.c_str()); + MP4::ChapterList chapters = f.qtChapters(); CPPUNIT_ASSERT_EQUAL(3U, chapters.size()); - CPPUNIT_ASSERT_EQUAL(0LL, chapters[0].startTime); - CPPUNIT_ASSERT_EQUAL(String("Intro"), chapters[0].title); - CPPUNIT_ASSERT_EQUAL(15000LL, chapters[1].startTime); - CPPUNIT_ASSERT_EQUAL(String("Verse"), chapters[1].title); - CPPUNIT_ASSERT_EQUAL(30000LL, chapters[2].startTime); - CPPUNIT_ASSERT_EQUAL(String("Outro"), chapters[2].title); + CPPUNIT_ASSERT_EQUAL(0LL, chapters[0].startTime()); + CPPUNIT_ASSERT_EQUAL(String("Intro"), chapters[0].title()); + CPPUNIT_ASSERT_EQUAL(15000LL, chapters[1].startTime()); + CPPUNIT_ASSERT_EQUAL(String("Verse"), chapters[1].title()); + CPPUNIT_ASSERT_EQUAL(30000LL, chapters[2].startTime()); + CPPUNIT_ASSERT_EQUAL(String("Outro"), chapters[2].title()); } } @@ -1097,37 +1072,35 @@ public: // Write chapters first { - MP4::ChapterList chapters; - MP4::Chapter ch1; - ch1.startTime = 0; - ch1.title = "Chapter 1"; - chapters.append(ch1); - - MP4::Chapter ch2; - ch2.startTime = 10000LL; // 10 seconds in ms - ch2.title = "Chapter 2"; - chapters.append(ch2); - - CPPUNIT_ASSERT(MP4::MP4QTChapterList::write(filename.c_str(), chapters)); + MP4::File f(filename.c_str()); + f.setQtChapters(MP4::ChapterList{ + MP4::Chapter("Chapter 1", 0), + MP4::Chapter("Chapter 2", 10000LL) + }); + CPPUNIT_ASSERT(f.save()); } // Verify written { - MP4::ChapterList chapters = MP4::MP4QTChapterList::read(filename.c_str()); + MP4::File f(filename.c_str()); + MP4::ChapterList chapters = f.qtChapters(); CPPUNIT_ASSERT_EQUAL(2U, chapters.size()); - } - // Remove chapters - CPPUNIT_ASSERT(MP4::MP4QTChapterList::remove(filename.c_str())); + // Remove chapters + f.setQtChapters(MP4::ChapterList()); + CPPUNIT_ASSERT(f.save()); + } // Verify removed { - MP4::ChapterList chapters = MP4::MP4QTChapterList::read(filename.c_str()); + MP4::File f(filename.c_str()); + MP4::ChapterList chapters = f.qtChapters(); CPPUNIT_ASSERT(chapters.isEmpty()); - } - // Remove from file with no chapters should also succeed - CPPUNIT_ASSERT(MP4::MP4QTChapterList::remove(filename.c_str())); + // Remove from file with no chapters should also succeed + f.setQtChapters(MP4::ChapterList()); + CPPUNIT_ASSERT(f.save()); + } } void testQTChapterListWithExistingTags() @@ -1142,51 +1115,49 @@ public: CPPUNIT_ASSERT(f.isValid()); originalArtist = f.tag()->artist(); CPPUNIT_ASSERT(!originalArtist.isEmpty()); - } - // Write chapters - { - MP4::ChapterList chapters; - MP4::Chapter ch1; - ch1.startTime = 0; - ch1.title = "Intro"; - chapters.append(ch1); - - MP4::Chapter ch2; - ch2.startTime = 10000LL; // 10 seconds in ms - ch2.title = "Verse"; - chapters.append(ch2); - - CPPUNIT_ASSERT(MP4::MP4QTChapterList::write(filename.c_str(), chapters)); + // Write chapters + f.setQtChapters(MP4::ChapterList{ + MP4::Chapter("Intro", 0), + MP4::Chapter("Verse", 10000LL) + }); + CPPUNIT_ASSERT(f.save()); } // Verify chapters are written AND existing tags are preserved { - MP4::ChapterList chapters = MP4::MP4QTChapterList::read(filename.c_str()); - CPPUNIT_ASSERT_EQUAL(2U, chapters.size()); - CPPUNIT_ASSERT_EQUAL(String("Intro"), chapters[0].title); - CPPUNIT_ASSERT_EQUAL(String("Verse"), chapters[1].title); - MP4::File f(filename.c_str()); + MP4::ChapterList chapters = f.qtChapters(); + CPPUNIT_ASSERT_EQUAL(2U, chapters.size()); + CPPUNIT_ASSERT_EQUAL(String("Intro"), chapters[0].title()); + CPPUNIT_ASSERT_EQUAL(String("Verse"), chapters[1].title()); + CPPUNIT_ASSERT(f.isValid()); CPPUNIT_ASSERT_EQUAL(originalArtist, f.tag()->artist()); + + // Remove chapters and verify tags still survive + f.setQtChapters(MP4::ChapterList()); + CPPUNIT_ASSERT(f.save()); } - // Remove chapters and verify tags still survive - CPPUNIT_ASSERT(MP4::MP4QTChapterList::remove(filename.c_str())); { MP4::File f(filename.c_str()); CPPUNIT_ASSERT(f.isValid()); CPPUNIT_ASSERT_EQUAL(originalArtist, f.tag()->artist()); + CPPUNIT_ASSERT(f.qtChapters().isEmpty()); } } void testQTChapterListReadEmpty() { // Reading from a file with no chapter track should return empty list - MP4::ChapterList chapters = MP4::MP4QTChapterList::read( - TEST_FILE_PATH_C("no-tags.m4a")); - CPPUNIT_ASSERT(chapters.isEmpty()); + ScopedFileCopy copy("no-tags", ".m4a"); + string filename = copy.fileName(); + + { + MP4::File f(filename.c_str()); + CPPUNIT_ASSERT(f.qtChapters().isEmpty()); + } } void testQTChapterListOverwrite() @@ -1196,54 +1167,40 @@ public: // Write initial chapters { - MP4::ChapterList chapters; - MP4::Chapter ch1; - ch1.startTime = 0; - ch1.title = "Old1"; - chapters.append(ch1); - - MP4::Chapter ch2; - ch2.startTime = 5000LL; // 5 seconds in ms - ch2.title = "Old2"; - chapters.append(ch2); - - CPPUNIT_ASSERT(MP4::MP4QTChapterList::write(filename.c_str(), chapters)); + MP4::File f(filename.c_str()); + f.setQtChapters(MP4::ChapterList{ + MP4::Chapter("Old1", 0), + MP4::Chapter("Old2", 5000LL) + }); + CPPUNIT_ASSERT(f.save()); } // Verify initial { - MP4::ChapterList chapters = MP4::MP4QTChapterList::read(filename.c_str()); + MP4::File f(filename.c_str()); + MP4::ChapterList chapters = f.qtChapters(); CPPUNIT_ASSERT_EQUAL(2U, chapters.size()); } // Overwrite with different chapters { - MP4::ChapterList chapters; - MP4::Chapter ch1; - ch1.startTime = 0; - ch1.title = "New1"; - chapters.append(ch1); - - MP4::Chapter ch2; - ch2.startTime = 10000LL; // 10 seconds in ms - ch2.title = "New2"; - chapters.append(ch2); - - MP4::Chapter ch3; - ch3.startTime = 20000LL; // 20 seconds in ms - ch3.title = "New3"; - chapters.append(ch3); - - CPPUNIT_ASSERT(MP4::MP4QTChapterList::write(filename.c_str(), chapters)); + MP4::File f(filename.c_str()); + f.setQtChapters(MP4::ChapterList{ + MP4::Chapter("New1", 0), + MP4::Chapter("New2", 10000LL), + MP4::Chapter("New3", 20000LL) + }); + CPPUNIT_ASSERT(f.save()); } // Verify overwrite { - MP4::ChapterList chapters = MP4::MP4QTChapterList::read(filename.c_str()); + MP4::File f(filename.c_str()); + MP4::ChapterList chapters = f.qtChapters(); CPPUNIT_ASSERT_EQUAL(3U, chapters.size()); - CPPUNIT_ASSERT_EQUAL(String("New1"), chapters[0].title); - CPPUNIT_ASSERT_EQUAL(String("New2"), chapters[1].title); - CPPUNIT_ASSERT_EQUAL(String("New3"), chapters[2].title); + CPPUNIT_ASSERT_EQUAL(String("New1"), chapters[0].title()); + CPPUNIT_ASSERT_EQUAL(String("New2"), chapters[1].title()); + CPPUNIT_ASSERT_EQUAL(String("New3"), chapters[2].title()); } } @@ -1254,26 +1211,21 @@ public: // Write chapters at precise times { - MP4::ChapterList chapters; - MP4::Chapter ch1; - ch1.startTime = 0; - ch1.title = "Start"; - chapters.append(ch1); - - MP4::Chapter ch2; - ch2.startTime = 1500LL; // 1.5 seconds in ms - ch2.title = "Precise"; - chapters.append(ch2); - - CPPUNIT_ASSERT(MP4::MP4QTChapterList::write(filename.c_str(), chapters)); + MP4::File f(filename.c_str()); + f.setQtChapters(MP4::ChapterList{ + MP4::Chapter("Start", 0), + MP4::Chapter("Precise", 1500LL) + }); + CPPUNIT_ASSERT(f.save()); } // Read back and verify timestamps { - MP4::ChapterList chapters = MP4::MP4QTChapterList::read(filename.c_str()); + MP4::File f(filename.c_str()); + MP4::ChapterList chapters = f.qtChapters(); CPPUNIT_ASSERT_EQUAL(2U, chapters.size()); - CPPUNIT_ASSERT_EQUAL(0LL, chapters[0].startTime); - CPPUNIT_ASSERT_EQUAL(1500LL, chapters[1].startTime); + CPPUNIT_ASSERT_EQUAL(0LL, chapters[0].startTime()); + CPPUNIT_ASSERT_EQUAL(1500LL, chapters[1].startTime()); } } @@ -1284,140 +1236,29 @@ public: // Write chapters where first chapter is NOT at time 0 { - MP4::ChapterList chapters; - MP4::Chapter ch1; - ch1.startTime = 10000LL; // 10 seconds in ms - ch1.title = "One"; - chapters.append(ch1); - - MP4::Chapter ch2; - ch2.startTime = 20000LL; // 20 seconds in ms - ch2.title = "Two"; - chapters.append(ch2); - - MP4::Chapter ch3; - ch3.startTime = 30000LL; // 30 seconds in ms - ch3.title = "Three"; - chapters.append(ch3); - - CPPUNIT_ASSERT(MP4::MP4QTChapterList::write(filename.c_str(), chapters)); + MP4::File f(filename.c_str()); + f.setQtChapters(MP4::ChapterList{ + MP4::Chapter("One", 10000LL), + MP4::Chapter("Two", 20000LL), + MP4::Chapter("Three", 30000LL) + }); + CPPUNIT_ASSERT(f.save()); } // Read back -- dummy chapter at time 0 should be stripped { - MP4::ChapterList chapters = MP4::MP4QTChapterList::read(filename.c_str()); + MP4::File f(filename.c_str()); + MP4::ChapterList chapters = f.qtChapters(); CPPUNIT_ASSERT_EQUAL(3U, chapters.size()); - CPPUNIT_ASSERT_EQUAL(10000LL, chapters[0].startTime); - CPPUNIT_ASSERT_EQUAL(20000LL, chapters[1].startTime); - CPPUNIT_ASSERT_EQUAL(30000LL, chapters[2].startTime); - CPPUNIT_ASSERT_EQUAL(String("One"), chapters[0].title); - CPPUNIT_ASSERT_EQUAL(String("Two"), chapters[1].title); - CPPUNIT_ASSERT_EQUAL(String("Three"), chapters[2].title); - } - } - void testChapterListFileAPI() - { - ScopedFileCopy copy("no-tags", ".m4a"); - string filename = copy.fileName(); - - // Write chapters via the file-based API - { - MP4::File file(filename.c_str(), false); - CPPUNIT_ASSERT(file.isOpen() && file.isValid() && !file.readOnly()); - - MP4::ChapterList chapters; - MP4::Chapter ch1; - ch1.startTime = 0; - ch1.title = "Alpha"; - chapters.append(ch1); - - MP4::Chapter ch2; - ch2.startTime = 20000LL; // 20 seconds in ms - ch2.title = "Beta"; - chapters.append(ch2); - - CPPUNIT_ASSERT(MP4::MP4ChapterList::write(&file, chapters)); - } - - // Read back via the file-based API - { - MP4::File file(filename.c_str(), false); - CPPUNIT_ASSERT(file.isOpen() && file.isValid()); - - MP4::ChapterList chapters = MP4::MP4ChapterList::read(&file); - CPPUNIT_ASSERT_EQUAL(2U, chapters.size()); - CPPUNIT_ASSERT_EQUAL(0LL, chapters[0].startTime); - CPPUNIT_ASSERT_EQUAL(String("Alpha"), chapters[0].title); - CPPUNIT_ASSERT_EQUAL(20000LL, chapters[1].startTime); - CPPUNIT_ASSERT_EQUAL(String("Beta"), chapters[1].title); - } - - // Remove via the file-based API - { - MP4::File file(filename.c_str(), false); - CPPUNIT_ASSERT(file.isOpen() && file.isValid() && !file.readOnly()); - - CPPUNIT_ASSERT(MP4::MP4ChapterList::remove(&file)); - } - - // Verify removed - { - MP4::ChapterList chapters = MP4::MP4ChapterList::read(filename.c_str()); - CPPUNIT_ASSERT(chapters.isEmpty()); + CPPUNIT_ASSERT_EQUAL(10000LL, chapters[0].startTime()); + CPPUNIT_ASSERT_EQUAL(20000LL, chapters[1].startTime()); + CPPUNIT_ASSERT_EQUAL(30000LL, chapters[2].startTime()); + CPPUNIT_ASSERT_EQUAL(String("One"), chapters[0].title()); + CPPUNIT_ASSERT_EQUAL(String("Two"), chapters[1].title()); + CPPUNIT_ASSERT_EQUAL(String("Three"), chapters[2].title()); } } - void testQTChapterListFileAPI() - { - ScopedFileCopy copy("no-tags", ".m4a"); - string filename = copy.fileName(); - - // Write chapters via the file-based API - { - MP4::File file(filename.c_str(), false); - CPPUNIT_ASSERT(file.isOpen() && file.isValid() && !file.readOnly()); - - MP4::ChapterList chapters; - MP4::Chapter ch1; - ch1.startTime = 0; - ch1.title = "Alpha"; - chapters.append(ch1); - - MP4::Chapter ch2; - ch2.startTime = 20000LL; // 20 seconds in ms - ch2.title = "Beta"; - chapters.append(ch2); - - CPPUNIT_ASSERT(MP4::MP4QTChapterList::write(&file, chapters)); - } - - // Read back via the file-based API - { - MP4::File file(filename.c_str(), false); - CPPUNIT_ASSERT(file.isOpen() && file.isValid()); - - MP4::ChapterList chapters = MP4::MP4QTChapterList::read(&file); - CPPUNIT_ASSERT_EQUAL(2U, chapters.size()); - CPPUNIT_ASSERT_EQUAL(0LL, chapters[0].startTime); - CPPUNIT_ASSERT_EQUAL(String("Alpha"), chapters[0].title); - CPPUNIT_ASSERT_EQUAL(20000LL, chapters[1].startTime); - CPPUNIT_ASSERT_EQUAL(String("Beta"), chapters[1].title); - } - - // Remove via the file-based API - { - MP4::File file(filename.c_str(), false); - CPPUNIT_ASSERT(file.isOpen() && file.isValid() && !file.readOnly()); - - CPPUNIT_ASSERT(MP4::MP4QTChapterList::remove(&file)); - } - - // Verify removed - { - MP4::ChapterList chapters = MP4::MP4QTChapterList::read(filename.c_str()); - CPPUNIT_ASSERT(chapters.isEmpty()); - } - } }; CPPUNIT_TEST_SUITE_REGISTRATION(TestMP4);