Integrate MP4 chapters into MP4::File

This commit is contained in:
Urs Fleisch
2026-04-23 11:03:23 -07:00
committed by Ryan Francesconi
parent 0df52e3993
commit 78c7208bc9
12 changed files with 754 additions and 666 deletions
+6 -2
View File
@@ -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()
+79
View File
@@ -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<ChapterPrivate>())
{
d->title = title;
d->startTime = startTime;
}
MP4::Chapter::Chapter(const Chapter &other) :
d(std::make_unique<ChapterPrivate>(*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;
}
+98
View File
@@ -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 <memory>
#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<ChapterPrivate> d;
};
//! List of chapters.
using ChapterList = List<Chapter>;
} // namespace MP4
} // namespace TagLib
#endif
+44
View File
@@ -0,0 +1,44 @@
/**************************************************************************
copyright : (C) 2006 by Urs Fleisch
email : [email protected]
**************************************************************************/
/***************************************************************************
* 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;
}
+110
View File
@@ -0,0 +1,110 @@
/**************************************************************************
copyright : (C) 2006 by Urs Fleisch
email : [email protected]
**************************************************************************/
/***************************************************************************
* 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 <typename T>
ChapterList getChaptersLazy(std::unique_ptr<T> &holder, TagLib::File *file)
{
if (!holder) {
holder = std::make_unique<T>();
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 <typename T>
void setChaptersLazy(std::unique_ptr<T> &holder, const ChapterList& chapters)
{
if (!holder) {
holder = std::make_unique<T>();
}
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 <typename T>
bool saveChaptersIfModified(std::unique_ptr<T> &holder, TagLib::File *file)
{
if(holder && holder->isModified()) {
return holder->write(file);
}
return true;
}
} // namespace MP4
} // namespace TagLib
#endif
+27 -1
View File
@@ -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<MP4::Tag> tag;
std::unique_ptr<MP4::Atoms> atoms;
std::unique_ptr<MP4::Properties> properties;
std::unique_ptr<MP4::NeroChapterList> neroChapterList;
std::unique_ptr<MP4::QtChapterList> 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
+21
View File
@@ -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.
*
@@ -22,7 +22,7 @@
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "mp4chapterlist.h"
#include "mp4nerochapterlist.h"
#include <algorithm>
@@ -36,7 +36,7 @@ namespace
{
ByteVector renderAtom(const ByteVector &name, const ByteVector &data)
{
return ByteVector::fromUInt(static_cast<unsigned int>(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<unsigned int>(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<unsigned int>(titleBytes.size()), 255U);
ByteVector titleBytes = ch.title().data(String::UTF8);
const unsigned int titleLen = std::min(titleBytes.size(), 255U);
data.append(static_cast<char>(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<unsigned char>(data[pos++]);
const auto version = static_cast<unsigned char>(data[pos++]);
// Skip flags (3 bytes)
pos += 3;
@@ -187,13 +186,13 @@ namespace
if(pos >= data.size())
return chapters;
unsigned int count = static_cast<unsigned char>(data[pos++]);
const unsigned int count = static_cast<unsigned char>(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<unsigned char>(data[pos++]);
const unsigned int titleLen = static_cast<unsigned char>(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<offset_t>(chplAtom.size()) - oldLength;
const offset_t offset = existingChpl->offset();
const offset_t oldLength = existingChpl->length();
const offset_t delta = static_cast<offset_t>(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);
@@ -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<Chapter>;
/*!
* 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
+132 -176
View File
@@ -24,7 +24,6 @@
#include "mp4qtchapterlist.h"
#include <algorithm>
#include <cmath>
#include <vector>
@@ -41,7 +40,7 @@ namespace
ByteVector renderAtom(const ByteVector &name, const ByteVector &data)
{
return ByteVector::fromUInt(static_cast<unsigned int>(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<unsigned char>(data[8]);
const auto version = static_cast<unsigned char>(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<unsigned char>(tkhdData[8]);
if(version == 1 && tkhdData.size() >= 8 + 20 + 4) {
if(const auto version = static_cast<unsigned char>(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<unsigned char>(data[8]);
const auto version = static_cast<unsigned char>(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<unsigned char>(data[8]);
const auto version = static_cast<unsigned char>(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<unsigned char>(tkhdData[8]);
const auto version = static_cast<unsigned char>(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<unsigned int>(utf8.size());
const ByteVector utf8 = title.data(String::UTF8);
const unsigned int textLen = utf8.size();
ByteVector sample;
sample.append(ByteVector::fromShort(static_cast<short>(textLen)));
@@ -384,7 +381,7 @@ namespace
{
std::vector<unsigned int> sizes;
for(const auto &ch : chapters) {
unsigned int textLen = static_cast<unsigned int>(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<const char *>(entryBody),
@@ -443,7 +440,7 @@ namespace
ByteVector buildStts(const MP4::ChapterList &chapters, unsigned int timescale,
long long durationMs)
{
unsigned int count = static_cast<unsigned int>(chapters.size());
const unsigned int count = chapters.size();
if(count == 0)
return ByteVector();
@@ -453,7 +450,7 @@ namespace
static_cast<double>(timeMs) * static_cast<double>(timescale) / 1000.0 + 0.5);
};
unsigned int totalDuration = static_cast<unsigned int>(
const auto totalDuration = static_cast<unsigned int>(
static_cast<double>(durationMs) * static_cast<double>(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<unsigned int>(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<unsigned int>(chapters.size());
unsigned int totalDuration = static_cast<unsigned int>(
unsigned int count = chapters.size();
auto totalDuration = static_cast<unsigned int>(
static_cast<double>(durationMs) * static_cast<double>(timescale) / 1000.0 + 0.5);
// Single chunk offset -- all samples are contiguous starting at textDataOffset
unsigned int chunkOffset = static_cast<unsigned int>(textDataOffset);
auto chunkOffset = static_cast<unsigned int>(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<unsigned char>(data[8]);
if(version == 1 && data.size() >= 40) {
if(const auto version = static_cast<unsigned char>(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<SttsEntry> readStts(TagLib::File *file, MP4::Atom *chapterTrak)
{
std::vector<SttsEntry> 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<unsigned int> readStco(TagLib::File *file, MP4::Atom *chapterTrak)
{
std::vector<unsigned int> 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<unsigned int> chunkOffsets = readStco(file, chapterTrak);
const std::vector<unsigned int> chunkOffsets = readStco(file, chapterTrak);
if(chunkOffsets.empty())
return {};
@@ -837,12 +834,11 @@ namespace
};
std::vector<StscEntry> 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<unsigned int> sampleOffsets;
unsigned int totalChunks = static_cast<unsigned int>(chunkOffsets.size());
const auto totalChunks = static_cast<unsigned int>(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<unsigned int>(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<SttsEntry> sttsEntries = readStts(file, chapterTrak);
SampleSizeInfo sizeInfo = readStsz(file, chapterTrak);
std::vector<unsigned int> offsets = resolveSampleOffsets(file, chapterTrak, sizeInfo);
const std::vector<SttsEntry> sttsEntries = readStts(file, chapterTrak);
const SampleSizeInfo sizeInfo = readStsz(file, chapterTrak);
const std::vector<unsigned int> 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<long long>(
const auto startTimeMs = static_cast<long long>(
static_cast<double>(currentTime) * 1000.0 /
static_cast<double>(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<Atoms> 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<unsigned int> sampleSizes = calculateSampleSizes(workingChapters);
const std::vector<unsigned int> 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<offset_t>(trefAtom.size() + trakMeasure.size());
const auto totalInsert = static_cast<offset_t>(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<unsigned int>(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);
+8 -30
View File
@@ -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
+167 -326
View File
@@ -34,8 +34,6 @@
#include "mp4atom.h"
#include "mp4file.h"
#include "mp4itemfactory.h"
#include "mp4chapterlist.h"
#include "mp4qtchapterlist.h"
#include "plainfile.h"
#include <cppunit/extensions/HelperMacros.h>
#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);