mirror of
https://github.com/taglib/taglib.git
synced 2026-08-14 06:17:00 -04:00
Integrate MP4 chapters into MP4::File
This commit is contained in:
committed by
Ryan Francesconi
parent
0df52e3993
commit
78c7208bc9
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user