591 lines
29 KiB
C++
591 lines
29 KiB
C++
#include "./mp4container.h"
|
|
#include "./mp4ids.h"
|
|
|
|
#include "../exceptions.h"
|
|
#include "../mediafileinfo.h"
|
|
#include "../backuphelper.h"
|
|
|
|
#include <c++utilities/io/binaryreader.h>
|
|
#include <c++utilities/io/binarywriter.h>
|
|
#include <c++utilities/io/copy.h>
|
|
#include <c++utilities/misc/memory.h>
|
|
|
|
#include <tuple>
|
|
|
|
using namespace std;
|
|
using namespace IoUtilities;
|
|
using namespace ConversionUtilities;
|
|
using namespace ChronoUtilities;
|
|
|
|
namespace Media {
|
|
|
|
/*!
|
|
* \class Media::Mp4Container
|
|
* \brief Implementation of GenericContainer<MediaFileInfo, Mp4Tag, Mp4Track, Mp4Atom>.
|
|
*/
|
|
|
|
/*!
|
|
* \brief Constructs a new container for the specified \a fileInfo at the specified \a startOffset.
|
|
*/
|
|
Mp4Container::Mp4Container(MediaFileInfo &fileInfo, uint64 startOffset) :
|
|
GenericContainer<MediaFileInfo, Mp4Tag, Mp4Track, Mp4Atom>(fileInfo, startOffset),
|
|
m_fragmented(false)
|
|
{}
|
|
|
|
/*!
|
|
* \brief Destroys the container.
|
|
*/
|
|
Mp4Container::~Mp4Container()
|
|
{}
|
|
|
|
void Mp4Container::reset()
|
|
{
|
|
GenericContainer<MediaFileInfo, Mp4Tag, Mp4Track, Mp4Atom>::reset();
|
|
m_fragmented = false;
|
|
}
|
|
|
|
void Mp4Container::internalParseHeader()
|
|
{
|
|
//const string context("parsing header of MP4 container"); will be used when generating notifications
|
|
m_firstElement = make_unique<Mp4Atom>(*this, startOffset());
|
|
m_firstElement->parse();
|
|
Mp4Atom *ftypAtom = m_firstElement->siblingById(Mp4AtomIds::FileType, true);
|
|
if(ftypAtom) {
|
|
stream().seekg(ftypAtom->dataOffset());
|
|
m_doctype = reader().readString(4);
|
|
m_version = reader().readUInt32BE();
|
|
} else {
|
|
m_doctype = "mp41";
|
|
m_version = 0;
|
|
}
|
|
}
|
|
|
|
void Mp4Container::internalParseTags()
|
|
{
|
|
const string context("parsing tags of MP4 container");
|
|
if(Mp4Atom *udtaAtom = firstElement()->subelementByPath({Mp4AtomIds::Movie, Mp4AtomIds::UserData})) {
|
|
Mp4Atom *metaAtom = udtaAtom->childById(Mp4AtomIds::Meta);
|
|
bool surplusMetaAtoms = false;
|
|
while(metaAtom) {
|
|
metaAtom->parse();
|
|
m_tags.emplace_back(make_unique<Mp4Tag>());
|
|
try {
|
|
m_tags.back()->parse(*metaAtom);
|
|
} catch(NoDataFoundException &) {
|
|
m_tags.pop_back();
|
|
}
|
|
metaAtom = metaAtom->siblingById(Mp4AtomIds::Meta, false);
|
|
if(metaAtom) {
|
|
surplusMetaAtoms = true;
|
|
}
|
|
if(!m_tags.empty()) {
|
|
break;
|
|
}
|
|
}
|
|
if(surplusMetaAtoms) {
|
|
addNotification(NotificationType::Warning, "udta atom contains multiple meta atoms. Surplus meta atoms will be ignored.", context);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Mp4Container::internalParseTracks()
|
|
{
|
|
invalidateStatus();
|
|
static const string context("parsing tracks of MP4 container");
|
|
try {
|
|
// get moov atom which holds track information
|
|
if(Mp4Atom *moovAtom = firstElement()->siblingById(Mp4AtomIds::Movie, true)) {
|
|
// get mvhd atom which holds overall track information
|
|
if(Mp4Atom *mvhdAtom = moovAtom->childById(Mp4AtomIds::MovieHeader)) {
|
|
if(mvhdAtom->dataSize() > 0) {
|
|
stream().seekg(mvhdAtom->dataOffset());
|
|
byte version = reader().readByte();
|
|
if((version == 1 && mvhdAtom->dataSize() >= 32) || (mvhdAtom->dataSize() >= 20)) {
|
|
stream().seekg(3, ios_base::cur); // skip flags
|
|
switch(version) {
|
|
case 0:
|
|
m_creationTime = DateTime::fromDate(1904, 1, 1) + TimeSpan::fromSeconds(reader().readUInt32BE());
|
|
m_modificationTime = DateTime::fromDate(1904, 1, 1) + TimeSpan::fromSeconds(reader().readUInt32BE());
|
|
m_timeScale = reader().readUInt32BE();
|
|
m_duration = TimeSpan::fromSeconds(static_cast<double>(reader().readUInt32BE()) / static_cast<double>(m_timeScale));
|
|
break;
|
|
case 1:
|
|
m_creationTime = DateTime::fromDate(1904, 1, 1) + TimeSpan::fromSeconds(reader().readUInt64BE());
|
|
m_modificationTime = DateTime::fromDate(1904, 1, 1) + TimeSpan::fromSeconds(reader().readUInt64BE());
|
|
m_timeScale = reader().readUInt32BE();
|
|
m_duration = TimeSpan::fromSeconds(static_cast<double>(reader().readUInt64BE()) / static_cast<double>(m_timeScale));
|
|
break;
|
|
default:
|
|
;
|
|
}
|
|
} else {
|
|
addNotification(NotificationType::Critical, "mvhd atom is truncated.", context);
|
|
}
|
|
} else {
|
|
addNotification(NotificationType::Critical, "mvhd atom is empty.", context);
|
|
}
|
|
} else {
|
|
addNotification(NotificationType::Critical, "mvhd atom is does not exist.", context);
|
|
}
|
|
// get mvex atom which holds default values for fragmented files
|
|
if(Mp4Atom *mehdAtom = moovAtom->subelementByPath({Mp4AtomIds::MovieExtends, Mp4AtomIds::MovieExtendsHeader})) {
|
|
m_fragmented = true;
|
|
if(mehdAtom->dataSize() > 0) {
|
|
stream().seekg(mehdAtom->dataOffset());
|
|
unsigned int durationSize = reader().readByte() == 1u ? 8u : 4u; // duration size depends on atom version
|
|
if(mehdAtom->dataSize() >= 4 + durationSize) {
|
|
stream().seekg(3, ios_base::cur); // skip flags
|
|
switch(durationSize) {
|
|
case 4u:
|
|
m_duration = TimeSpan::fromSeconds(static_cast<double>(reader().readUInt32BE()) / static_cast<double>(m_timeScale));
|
|
break;
|
|
case 8u:
|
|
m_duration = TimeSpan::fromSeconds(static_cast<double>(reader().readUInt64BE()) / static_cast<double>(m_timeScale));
|
|
break;
|
|
default:
|
|
;
|
|
}
|
|
} else {
|
|
addNotification(NotificationType::Warning, "mehd atom is truncated.", context);
|
|
}
|
|
}
|
|
}
|
|
// get first trak atoms which hold information for each track
|
|
Mp4Atom *trakAtom = moovAtom->childById(Mp4AtomIds::Track);
|
|
int trackNum = 1;
|
|
while(trakAtom) {
|
|
try {
|
|
trakAtom->parse();
|
|
} catch(Failure &) {
|
|
addNotification(NotificationType::Warning, "Unable to parse child atom of moov.", context);
|
|
}
|
|
// parse the trak atom using the Mp4Track class
|
|
m_tracks.emplace_back(make_unique<Mp4Track>(*trakAtom));
|
|
try { // try to parse header
|
|
m_tracks.back()->parseHeader();
|
|
} catch(Failure &) {
|
|
addNotification(NotificationType::Critical, "Unable to parse track " + ConversionUtilities::numberToString(trackNum) + ".", context);
|
|
}
|
|
trakAtom = trakAtom->siblingById(Mp4AtomIds::Track, false); // get next trak atom
|
|
++trackNum;
|
|
}
|
|
// get overall duration, creation time and modification time if not determined yet
|
|
if(m_duration.isNull() || m_modificationTime.isNull() || m_creationTime.isNull()) {
|
|
for(const auto &track : tracks()) {
|
|
if(track->duration() > m_duration) {
|
|
m_duration = track->duration();
|
|
}
|
|
if(track->modificationTime() > m_modificationTime) {
|
|
m_modificationTime = track->modificationTime();
|
|
}
|
|
if(track->creationTime() < m_creationTime) {
|
|
m_creationTime = track->creationTime();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch(Failure &) {
|
|
addNotification(NotificationType::Warning, "Unable to parse moov atom.", context);
|
|
}
|
|
}
|
|
|
|
void Mp4Container::internalMakeFile()
|
|
{
|
|
invalidateStatus();
|
|
static const string context("making MP4 container");
|
|
if(!firstElement()) {
|
|
addNotification(NotificationType::Critical, "No MP4 atoms could be found.", context);
|
|
throw InvalidDataException();
|
|
}
|
|
// prepare rewriting the file
|
|
bool writeChunkByChunk = m_tracksAltered;
|
|
updateStatus("Preparing for rewriting MP4 file ...");
|
|
fileInfo().close(); // ensure the file is close before renaming it
|
|
string backupPath;
|
|
fstream &outputStream = fileInfo().stream();
|
|
BinaryWriter outputWriter(&outputStream);
|
|
fstream backupStream; // create a stream to open the backup/original file
|
|
try {
|
|
BackupHelper::createBackupFile(fileInfo().path(), backupPath, backupStream);
|
|
// set backup stream as associated input stream since we still the original data/atoms to write the new file
|
|
setStream(backupStream);
|
|
// recreate original file
|
|
outputStream.open(fileInfo().path(), ios_base::out | ios_base::binary | ios_base::trunc);
|
|
// collect needed atoms
|
|
Mp4Atom *ftypAtom, *pdinAtom, *moovAtom;
|
|
try {
|
|
ftypAtom = firstElement()->siblingById(Mp4AtomIds::FileType, true); // mandatory
|
|
if(!ftypAtom) { // throw error if missing
|
|
addNotification(NotificationType::Critical, "Mandatory ftyp atom not found.", context);
|
|
}
|
|
pdinAtom = firstElement()->siblingById(Mp4AtomIds::ProgressiveDownloadInformation, true); // not mandatory
|
|
moovAtom = firstElement()->siblingById(Mp4AtomIds::Movie, true); // mandatory
|
|
if(!moovAtom) { // throw error if missing
|
|
addNotification(NotificationType::Critical, "Mandatory moov atom not found.", context);
|
|
throw InvalidDataException();
|
|
}
|
|
} catch (Failure &) {
|
|
addNotification(NotificationType::Critical, "Unable to parse the atom strucutre of the source file.", context);
|
|
throw InvalidDataException();
|
|
}
|
|
if(m_tags.size() > 1) {
|
|
addNotification(NotificationType::Warning, "There are multiple MP4-tags assigned. Only the first one will be attached to the file.", context);
|
|
}
|
|
// write all top-level atoms, header boxes be placed first
|
|
updateStatus("Writing header ...");
|
|
ftypAtom->copyEntirely(outputStream);
|
|
if(pdinAtom) {
|
|
pdinAtom->copyEntirely(outputStream);
|
|
}
|
|
ostream::pos_type newMoovOffset = outputStream.tellp();
|
|
Mp4Atom *udtaAtom = nullptr;
|
|
uint64 newUdtaOffset = 0u;
|
|
if(isAborted()) {
|
|
throw OperationAbortedException();
|
|
}
|
|
moovAtom->copyWithoutChilds(outputStream);
|
|
for(Mp4Atom *moovChildAtom = moovAtom->firstChild(); moovChildAtom; moovChildAtom = moovChildAtom->nextSibling()) { // write child atoms manually, because the child udta has to be altered/ignored
|
|
try {
|
|
moovChildAtom->parse();
|
|
} catch(Failure &) {
|
|
addNotification(NotificationType::Critical, "Unable to parse childs of moov atom of original file.", context);
|
|
throw InvalidDataException();
|
|
}
|
|
if(moovChildAtom->id() == Mp4AtomIds::UserData) {
|
|
// found a udta (user data) atom which childs hold tag infromation
|
|
if(!udtaAtom) {
|
|
udtaAtom = moovChildAtom;
|
|
// check if the udta atom needs to be written
|
|
bool writeUdtaAtom = !m_tags.empty(); // it has to be written only when a MP4 tag is assigned
|
|
if(!writeUdtaAtom) { // or when there is at least one child except the meta atom in the original file
|
|
try {
|
|
for(Mp4Atom *udtaChildAtom = udtaAtom->firstChild(); udtaChildAtom; udtaChildAtom = udtaChildAtom->nextSibling()) {
|
|
udtaChildAtom->parse();
|
|
if(udtaChildAtom->id() != Mp4AtomIds::Meta) {
|
|
writeUdtaAtom = true;
|
|
break;
|
|
}
|
|
}
|
|
} catch(Failure &) {
|
|
addNotification(NotificationType::Warning,
|
|
"Unable to parse childs of udta atom of original file. These invalid/unknown atoms will be ignored.", context);
|
|
}
|
|
}
|
|
if(writeUdtaAtom) {
|
|
updateStatus("Writing tag information ...");
|
|
newUdtaOffset = outputStream.tellp(); // save offset
|
|
udtaAtom->copyHeader(outputStream); // and write header
|
|
// write meta atom if there's a tag assigned
|
|
if(!m_tags.empty()) {
|
|
try {
|
|
m_tags.front()->make(outputStream);
|
|
} catch(Failure &) {
|
|
addNotification(NotificationType::Warning, "Unable to write meta atom (of assigned mp4 tag).", context);
|
|
}
|
|
addNotifications(*m_tags.front());
|
|
}
|
|
// write rest of the child atoms of udta atom
|
|
try {
|
|
for(Mp4Atom *udtaChildAtom = udtaAtom->firstChild(); udtaChildAtom; udtaChildAtom = udtaChildAtom->nextSibling()) {
|
|
udtaChildAtom->parse();
|
|
if(udtaChildAtom->id() != Mp4AtomIds::Meta) { // skip meta atoms here of course
|
|
udtaChildAtom->copyEntirely(outputStream);
|
|
}
|
|
}
|
|
} catch(Failure &) {
|
|
addNotification(NotificationType::Warning,
|
|
"Unable to parse childs of udta atom of original file. These will be ignored.", context);
|
|
}
|
|
// write correct size of udta atom
|
|
Mp4Atom::seekBackAndWriteAtomSize(outputStream, newUdtaOffset);
|
|
}
|
|
} else {
|
|
addNotification(NotificationType::Warning, "The source file has multiple udta atoms. Surplus atoms will be ignored.", context);
|
|
}
|
|
} else if(!writeChunkByChunk || moovChildAtom->id() != Mp4AtomIds::Track) {
|
|
// copy trak atoms only when not writing the data chunk-by-chunk
|
|
moovChildAtom->copyEntirely(outputStream);
|
|
}
|
|
}
|
|
// the original file has no udta atom but there is tag information to be written
|
|
if(!udtaAtom && !m_tags.empty()) {
|
|
updateStatus("Writing tag information ...");
|
|
newUdtaOffset = outputStream.tellp();
|
|
// write udta atom
|
|
outputWriter.writeUInt32BE(0); // the size will be written later
|
|
outputWriter.writeUInt32BE(Mp4AtomIds::UserData);
|
|
// write tags
|
|
try {
|
|
m_tags.front()->make(outputStream);
|
|
Mp4Atom::seekBackAndWriteAtomSize(outputStream, newUdtaOffset);
|
|
} catch(Failure &) {
|
|
addNotification(NotificationType::Warning, "Unable to write meta atom (of assigned mp4 tag).", context);
|
|
outputStream.seekp(-8, ios_base::cur);
|
|
}
|
|
}
|
|
// write trak atoms for each currently assigned track, this is only required when writing data chunk-by-chunk
|
|
if(writeChunkByChunk) {
|
|
updateStatus("Writing meta information for the tracks ...");
|
|
for(auto &track : tracks()) {
|
|
if(isAborted()) {
|
|
throw OperationAbortedException();
|
|
}
|
|
track->setOutputStream(outputStream);
|
|
track->makeTrack();
|
|
}
|
|
}
|
|
Mp4Atom::seekBackAndWriteAtomSize(outputStream, newMoovOffset);
|
|
// prepare for writing the actual data
|
|
vector<tuple<istream *, vector<uint64>, vector<uint64> > > trackInfos; // used when writing chunk-by-chunk
|
|
uint64 totalChunkCount; // used when writing chunk-by-chunk
|
|
vector<int64> origMdatOffsets; // used when simply copying mdat
|
|
vector<int64> newMdatOffsets; // used when simply copying mdat
|
|
// write other atoms
|
|
for(Mp4Atom *otherTopLevelAtom = firstElement(); otherTopLevelAtom; otherTopLevelAtom = otherTopLevelAtom->nextSibling()) {
|
|
if(isAborted()) {
|
|
throw OperationAbortedException();
|
|
}
|
|
try {
|
|
otherTopLevelAtom->parse();
|
|
} catch(Failure &) {
|
|
addNotification(NotificationType::Critical, "Unable to parse all top-level atoms of original file.", context);
|
|
throw InvalidDataException();
|
|
}
|
|
using namespace Mp4AtomIds;
|
|
switch(otherTopLevelAtom->id()) {
|
|
case FileType: case ProgressiveDownloadInformation: case Movie: case Free: case Skip:
|
|
break;
|
|
case MediaData:
|
|
if(writeChunkByChunk) {
|
|
break; // write actual data separately when writing chunk by chunk
|
|
} else {
|
|
// store the mdat offsets when not writing chunk by chunk to be able to update the stco tables
|
|
origMdatOffsets.push_back(otherTopLevelAtom->startOffset());
|
|
newMdatOffsets.push_back(outputStream.tellp());
|
|
}
|
|
default:
|
|
updateStatus("Writing " + otherTopLevelAtom->idToString() + " atom ...");
|
|
otherTopLevelAtom->forwardStatusUpdateCalls(this);
|
|
otherTopLevelAtom->copyEntirely(outputStream);
|
|
}
|
|
}
|
|
// when writing chunk by chunk the actual data needs to be written separately
|
|
if(writeChunkByChunk) {
|
|
// get the chunk offset and the chunk size table from the old file to be able to write single chunks later ...
|
|
updateStatus("Reading chunk offsets and sizes from the original file ...");
|
|
trackInfos.reserve(tracks().size());
|
|
totalChunkCount = 0;
|
|
for(auto &track : tracks()) {
|
|
if(&track->inputStream() == &outputStream) {
|
|
track->setInputStream(backupStream); // ensure the track reads from the original file
|
|
}
|
|
trackInfos.emplace_back(&track->inputStream(), track->readChunkOffsets(), track->readChunkSizes());
|
|
const vector<uint64> &chunkOffsetTable = get<1>(trackInfos.back());
|
|
const vector<uint64> &chunkSizesTable = get<2>(trackInfos.back());
|
|
totalChunkCount += track->chunkCount();
|
|
if(track->chunkCount() != chunkOffsetTable.size() || track->chunkCount() != chunkSizesTable.size()) {
|
|
addNotification(NotificationType::Critical, "Chunks of track " + numberToString<uint64, string>(track->id()) + " could not be parsed correctly.", context);
|
|
}
|
|
}
|
|
}
|
|
if(isAborted()) {
|
|
throw OperationAbortedException();
|
|
}
|
|
// reparse what is written so far
|
|
updateStatus("Reparsing output file ...");
|
|
outputStream.close(); // the outputStream needs to be reopened to be able to read again
|
|
outputStream.open(fileInfo().path(), ios_base::in | ios_base::out | ios_base::binary);
|
|
setStream(outputStream);
|
|
m_headerParsed = false;
|
|
m_tracks.clear();
|
|
m_tracksParsed = false;
|
|
m_tagsParsed = false;
|
|
try {
|
|
parseTracks();
|
|
} catch(Failure &) {
|
|
addNotification(NotificationType::Critical, "Unable to reparse the header of the new file.", context);
|
|
throw;
|
|
}
|
|
if(writeChunkByChunk) {
|
|
// checking parsed tracks
|
|
size_t trackCount = tracks().size();
|
|
if(trackCount != trackInfos.size()) {
|
|
if(trackCount > trackInfos.size()) {
|
|
trackCount = trackInfos.size();
|
|
}
|
|
addNotification(NotificationType::Critical, "The track meta data could not be written correctly. Trying to write the chunk data anyways.", context);
|
|
}
|
|
// writing single chunks is needed when tracks have been added or removed
|
|
updateStatus("Writing chunks to mdat atom ...");
|
|
outputStream.seekp(0, ios_base::end);
|
|
ostream::pos_type newMdatOffset = outputStream.tellp();
|
|
writer().writeUInt32BE(0); // denote 64 bit size
|
|
writer().writeUInt32BE(Mp4AtomIds::MediaData);
|
|
writer().writeUInt64BE(0); // write size of mdat atom later
|
|
CopyHelper<0x2000> copyHelper;
|
|
uint64 chunkIndex = 0;
|
|
uint64 totalChunksCopied = 0;
|
|
bool chunksCopied;
|
|
do {
|
|
if(isAborted()) {
|
|
throw OperationAbortedException();
|
|
}
|
|
chunksCopied = false;
|
|
for(size_t trackIndex = 0; trackIndex < trackCount; ++trackIndex) {
|
|
auto &track = tracks()[trackIndex];
|
|
auto &trackInfo = trackInfos[trackIndex];
|
|
istream &sourceStream = *get<0>(trackInfo);
|
|
const vector<uint64> &chunkOffsetTable = get<1>(trackInfo);
|
|
const vector<uint64> &chunkSizesTable = get<2>(trackInfo);
|
|
if(chunkIndex < chunkOffsetTable.size() && chunkIndex < chunkSizesTable.size()) {
|
|
sourceStream.seekg(chunkOffsetTable[chunkIndex]);
|
|
outputStream.seekp(0, ios_base::end);
|
|
uint64 chunkOffset = outputStream.tellp();
|
|
copyHelper.copy(sourceStream, outputStream, chunkSizesTable[chunkIndex]);
|
|
track->updateChunkOffset(chunkIndex, chunkOffset);
|
|
chunksCopied = true;
|
|
++totalChunksCopied;
|
|
}
|
|
}
|
|
++chunkIndex;
|
|
updatePercentage(static_cast<double>(totalChunksCopied) / totalChunkCount);
|
|
} while(chunksCopied);
|
|
outputStream.seekp(0, ios_base::end);
|
|
Mp4Atom::seekBackAndWriteAtomSize(outputStream, newMdatOffset, true);
|
|
} else {
|
|
// correct mdat offsets in the stco atom of each track when we've just copied the mdat atom
|
|
updateOffsets(origMdatOffsets, newMdatOffsets);
|
|
}
|
|
updatePercentage(100.0);
|
|
// dispose no longer used resources and flush output file stream
|
|
outputStream.flush();
|
|
} catch(OperationAbortedException &) {
|
|
setStream(outputStream);
|
|
m_tracksParsed = false;
|
|
m_headerParsed = false;
|
|
m_firstElement.reset();
|
|
m_tracks.clear();
|
|
addNotification(NotificationType::Information, "Rewriting the file to apply changed tag information has been aborted.", context);
|
|
BackupHelper::restoreOriginalFileFromBackupFile(fileInfo().path(), backupPath, outputStream, backupStream);
|
|
throw;
|
|
} catch(Failure &) {
|
|
setStream(outputStream);
|
|
m_tracksParsed = false;
|
|
m_headerParsed = false;
|
|
m_firstElement.reset();
|
|
m_tracks.clear();
|
|
addNotification(NotificationType::Critical, "Rewriting the file to apply changed tag information failed.", context);
|
|
BackupHelper::restoreOriginalFileFromBackupFile(fileInfo().path(), backupPath, outputStream, backupStream);
|
|
throw;
|
|
} catch(ios_base::failure &) {
|
|
setStream(outputStream);
|
|
m_tracksParsed = false;
|
|
m_headerParsed = false;
|
|
m_firstElement.reset();
|
|
m_tracks.clear();
|
|
addNotification(NotificationType::Critical, "An IO error occured when rewriting the file to apply changed tag information.", context);
|
|
BackupHelper::restoreOriginalFileFromBackupFile(fileInfo().path(), backupPath, outputStream, backupStream);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief Update the chunk offsets for each track of the file.
|
|
* \param oldMdatOffsets Specifies a vector holding the old offsets of the "mdat"-atoms.
|
|
* \param newMdatOffsets Specifies a vector holding the new offsets of the "mdat"-atoms.
|
|
*
|
|
* Internally uses Mp4Track::updateOffsets(). Offsets stored in the "tfhd"-atom are also
|
|
* updated (this is not tested yet since I don't have files using this atom).
|
|
*
|
|
* \throws Throws std::ios_base::failure when an IO error occurs.
|
|
* \throws Throws Media::Failure or a derived exception when a making
|
|
* error occurs.
|
|
*/
|
|
void Mp4Container::updateOffsets(const std::vector<int64> &oldMdatOffsets, const std::vector<int64> &newMdatOffsets)
|
|
{
|
|
// do NOT invalidate the status here since this method is internally called by internalMakeFile(), just update the status
|
|
updateStatus("Updating chunk offset table for each track ...");
|
|
const string context("updating MP4 container chunk offset table");
|
|
if(!firstElement()) {
|
|
addNotification(NotificationType::Critical, "No MP4 atoms could be found.", context);
|
|
throw InvalidDataException();
|
|
}
|
|
// update "base-data-offset-present" of tfhd atom (NOT tested properly)
|
|
try {
|
|
for(Mp4Atom *moofAtom = firstElement()->siblingById(Mp4AtomIds::MovieFragment, false);
|
|
moofAtom; moofAtom = moofAtom->siblingById(Mp4AtomIds::MovieFragment, false)) {
|
|
moofAtom->parse();
|
|
try {
|
|
for(Mp4Atom *trafAtom = moofAtom->childById(Mp4AtomIds::TrackFragment); trafAtom;
|
|
trafAtom = trafAtom->siblingById(Mp4AtomIds::TrackFragment, false)) {
|
|
trafAtom->parse();
|
|
int tfhdAtomCount = 0;
|
|
for(Mp4Atom *tfhdAtom = trafAtom->childById(Mp4AtomIds::TrackFragmentHeader); tfhdAtom;
|
|
tfhdAtom = tfhdAtom->siblingById(Mp4AtomIds::TrackFragmentHeader, false)) {
|
|
tfhdAtom->parse();
|
|
++tfhdAtomCount;
|
|
if(tfhdAtom->dataSize() >= 8) {
|
|
stream().seekg(tfhdAtom->dataOffset() + 1);
|
|
uint32 flags = reader().readUInt24BE();
|
|
if(flags & 1) {
|
|
if(tfhdAtom->dataSize() >= 16) {
|
|
stream().seekg(4, ios_base::cur); // skip track ID
|
|
uint64 off = reader().readUInt64BE();
|
|
for(auto iOld = oldMdatOffsets.cbegin(), iNew = newMdatOffsets.cbegin(), end = oldMdatOffsets.cend();
|
|
iOld != end; ++iOld, ++iNew) {
|
|
if(off >= static_cast<uint64>(*iOld)) {
|
|
off += (*iNew - *iOld);
|
|
stream().seekp(tfhdAtom->dataOffset() + 8);
|
|
writer().writeUInt64BE(off);
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
addNotification(NotificationType::Warning, "tfhd atom (denoting base-data-offset-present) is truncated.", context);
|
|
}
|
|
}
|
|
} else {
|
|
addNotification(NotificationType::Warning, "tfhd atom is truncated.", context);
|
|
}
|
|
}
|
|
switch(tfhdAtomCount) {
|
|
case 0:
|
|
addNotification(NotificationType::Warning, "traf atom doesn't contain mandatory tfhd atom.", context);
|
|
break;
|
|
case 1:
|
|
break;
|
|
default:
|
|
addNotification(NotificationType::Warning, "traf atom stores multiple tfhd atoms but it should only contain exactly one tfhd atom.", context);
|
|
}
|
|
}
|
|
} catch(Failure &) {
|
|
addNotification(NotificationType::Critical, "Unable to parse childs of top-level atom moof.", context);
|
|
}
|
|
}
|
|
} catch(Failure &) {
|
|
addNotification(NotificationType::Critical, "Unable to parse top-level atom moof.", context);
|
|
}
|
|
// update each track
|
|
for(auto &track : tracks()) {
|
|
if(isAborted()) {
|
|
throw OperationAbortedException();
|
|
}
|
|
if(!track->isHeaderValid()) {
|
|
try {
|
|
track->parseHeader();
|
|
} catch(Failure &) {
|
|
addNotification(NotificationType::Warning, "The chunk offsets of track " + track->name() + " couldn't be updated because the track seems to be invalid. The newly written file seems to be damaged.", context);
|
|
}
|
|
}
|
|
if(track->isHeaderValid()) {
|
|
try {
|
|
track->updateChunkOffsets(oldMdatOffsets, newMdatOffsets);
|
|
} catch(Failure &) {
|
|
addNotification(NotificationType::Warning, "The chunk offsets of track " + track->name() + " couldn't be updated. The altered file is damaged now, restore the backup!", context);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|