added properties to control position of tags when writing files

This commit is contained in:
Martchus 2015-11-07 15:23:36 +01:00
parent 6563a0c854
commit e7bd2185d5
9 changed files with 874 additions and 610 deletions

View File

@ -703,6 +703,52 @@ void MatroskaContainer::internalMakeFile()
addNotification(NotificationType::Critical, "No EBML elements could be found.", context);
throw InvalidDataException();
}
// check whether a rewrite is required
try {
// calculate size of tags
vector<MatroskaTagMaker> tagMaker;
uint64 tagElementsSize = 0;
for(auto &tag : tags()) {
tag->invalidateNotifications();
try {
tagMaker.emplace_back(tag->prepareMaking());
if(tagMaker.back().requiredSize() > 3) {
// a tag of 3 bytes size is empty and can be skipped
tagElementsSize += tagMaker.back().requiredSize();
}
} catch(Failure &) {
// nothing to do because notifications will be added anyways
}
addNotifications(*tag);
}
uint64 tagsSize = tagElementsSize ? 4 + EbmlElement::calculateSizeDenotationLength(tagElementsSize) + tagElementsSize : 0;
// calculate size of attachments
vector<MatroskaAttachmentMaker> attachmentMaker;
uint64 attachedFileElementsSize = 0;
for(auto &attachment : m_attachments) {
if(!attachment->isIgnored()) {
attachment->invalidateNotifications();
try {
attachmentMaker.emplace_back(attachment->prepareMaking());
if(attachmentMaker.back().requiredSize() > 3) {
// an attachment of 3 bytes size is empty and can be skipped
attachedFileElementsSize += attachmentMaker.back().requiredSize();
}
} catch(Failure &) {
// nothing to do because notifications will be added anyways
}
addNotifications(*attachment);
}
}
uint64 attachmentsSize = attachedFileElementsSize ? 4 + EbmlElement::calculateSizeDenotationLength(attachedFileElementsSize) + attachedFileElementsSize : 0;
// check the number of segments to be written
unsigned int lastSegmentIndex = static_cast<unsigned int>(-1);
for(; level0Element; level0Element = level0Element->nextSibling()) {
level0Element->parse();
if(level0Element->id() == MatroskaIds::Segment) {
++lastSegmentIndex;
}
}
// prepare rewriting the file
updateStatus("Preparing for rewriting Matroska/EBML file ...");
fileInfo().close(); // ensure the file is close before renaming it
@ -756,15 +802,10 @@ void MatroskaContainer::internalMakeFile()
uint64 segmentInfoElementDataSize;
MatroskaSeekInfo seekInfo;
MatroskaCuePositionUpdater cuesUpdater;
vector<MatroskaTagMaker> tagMaker;
uint64 tagElementsSize, tagsSize;
vector<MatroskaAttachmentMaker> attachmentMaker;
uint64 attachedFileElementsSize, attachmentsSize;
unsigned int segmentIndex = 0;
unsigned int index;
try {
for(; level0Element; level0Element = level0Element->nextSibling()) {
level0Element->parse();
for(level0Element = firstElement(); level0Element; level0Element = level0Element->nextSibling()) {
switch(level0Element->id()) {
case EbmlIds::Header:
break; // header is already written; skip header here
@ -777,40 +818,6 @@ void MatroskaContainer::internalMakeFile()
// prepare writing tags
// ensure seek info contains no old entries
seekInfo.clear();
// calculate size of tags
tagElementsSize = 0;
for(auto &tag : tags()) {
tag->invalidateNotifications();
try {
tagMaker.emplace_back(tag->prepareMaking());
if(tagMaker.back().requiredSize() > 3) {
// a tag of 3 bytes size is empty and can be skipped
tagElementsSize += tagMaker.back().requiredSize();
}
} catch(Failure &) {
// nothing to do because notifications will be added anyways
}
addNotifications(*tag);
}
tagsSize = tagElementsSize ? 4 + EbmlElement::calculateSizeDenotationLength(tagElementsSize) + tagElementsSize : 0;
// calculate size of attachments
attachedFileElementsSize = 0;
for(auto &attachment : m_attachments) {
if(!attachment->isIgnored()) {
attachment->invalidateNotifications();
try {
attachmentMaker.emplace_back(attachment->prepareMaking());
if(attachmentMaker.back().requiredSize() > 3) {
// an attachment of 3 bytes size is empty and can be skipped
attachedFileElementsSize += attachmentMaker.back().requiredSize();
}
} catch(Failure &) {
// nothing to do because notifications will be added anyways
}
addNotifications(*attachment);
}
}
attachmentsSize = attachedFileElementsSize ? 4 + EbmlElement::calculateSizeDenotationLength(attachedFileElementsSize) + attachedFileElementsSize : 0;
// parse cues
cuesUpdater.invalidateNotifications();
if((level1Element = level0Element->childById(MatroskaIds::Cues))) {
@ -880,6 +887,20 @@ void MatroskaContainer::internalMakeFile()
}
}
}
// all "Tags"- and "Attachments"-elements are written in either the first or the last segment
// and either before "Cues"- and "Cluster"-elements or after these elements
// depending on the desired tag position (at the front/at the end)
if(fileInfo().tagPosition() == TagPosition::BeforeData && segmentIndex == 0) {
// pretend writing "Tags"-element
if(tagsSize) {
// update offsets in "SeekHead"-element
if(seekInfo.push(0, MatroskaIds::Tags, currentOffset + elementSize)) {
goto calculateSegmentSize;
} else {
// add size of "Tags"-element
elementSize += tagsSize;
}
}
// pretend writing "Attachments"-element
if(attachmentsSize) {
// update offsets in "SeekHead"-element
@ -890,15 +911,6 @@ void MatroskaContainer::internalMakeFile()
elementSize += attachmentsSize;
}
}
// pretend writing "Tags"-element
if(tagsSize) {
// update offsets in "SeekHead"-element
if(seekInfo.push(0, MatroskaIds::Tags, currentOffset + elementSize)) {
goto calculateSegmentSize;
} else {
// add size of "Tags"-element
elementSize += tagsSize;
}
}
// pretend writing "Cues"-element
if(cuesPresent) {
@ -951,6 +963,28 @@ void MatroskaContainer::internalMakeFile()
}
}
}
if(fileInfo().tagPosition() == TagPosition::AfterData && segmentIndex == lastSegmentIndex) {
// pretend writing "Tags"-element
if(tagsSize) {
// update offsets in "SeekHead"-element
if(seekInfo.push(0, MatroskaIds::Tags, currentOffset + elementSize)) {
goto calculateSegmentSize;
} else {
// add size of "Tags"-element
elementSize += tagsSize;
}
}
// pretend writing "Attachments"-element
if(attachmentsSize) {
// update offsets in "SeekHead"-element
if(seekInfo.push(0, MatroskaIds::Attachments, currentOffset + elementSize)) {
goto calculateSegmentSize;
} else {
// add size of "Attachments"-element
elementSize += attachmentsSize;
}
}
}
// write "Segment"-element actually
updateStatus("Writing segment header ...");
outputWriter.writeUInt32BE(MatroskaIds::Segment);
@ -1006,6 +1040,17 @@ void MatroskaContainer::internalMakeFile()
level1Element->copyEntirely(outputStream);
}
}
if(fileInfo().tagPosition() == TagPosition::BeforeData && segmentIndex == 0) {
// write "Tags"-element
if(tagsSize) {
outputWriter.writeUInt32BE(MatroskaIds::Tags);
sizeLength = EbmlElement::makeSizeDenotation(tagElementsSize, buff);
outputStream.write(buff, sizeLength);
for(auto &maker : tagMaker) {
maker.make(outputStream);
}
// no need to add notifications; this has been done when creating the make
}
// write "Attachments"-element
if(attachmentsSize) {
outputWriter.writeUInt32BE(MatroskaIds::Attachments);
@ -1016,15 +1061,6 @@ void MatroskaContainer::internalMakeFile()
}
// no need to add notifications; this has been done when creating the make
}
// write "Tags"-element
if(tagsSize) {
outputWriter.writeUInt32BE(MatroskaIds::Tags);
sizeLength = EbmlElement::makeSizeDenotation(tagElementsSize, buff);
outputStream.write(buff, sizeLength);
for(auto &maker : tagMaker) {
maker.make(outputStream);
}
// no need to add notifications; this has been done when creating the make
}
// write "Cues"-element
if(cuesPresent) {
@ -1071,6 +1107,28 @@ void MatroskaContainer::internalMakeFile()
updatePercentage(static_cast<double>(static_cast<uint64>(outputStream.tellp()) - offset) / elementSize);
}
}
if(fileInfo().tagPosition() == TagPosition::AfterData && segmentIndex == lastSegmentIndex) {
// write "Tags"-element
if(tagsSize) {
outputWriter.writeUInt32BE(MatroskaIds::Tags);
sizeLength = EbmlElement::makeSizeDenotation(tagElementsSize, buff);
outputStream.write(buff, sizeLength);
for(auto &maker : tagMaker) {
maker.make(outputStream);
}
// no need to add notifications; this has been done when creating the make
}
// write "Attachments"-element
if(attachmentsSize) {
outputWriter.writeUInt32BE(MatroskaIds::Attachments);
sizeLength = EbmlElement::makeSizeDenotation(attachedFileElementsSize, buff);
outputStream.write(buff, sizeLength);
for(auto &maker : attachmentMaker) {
maker.make(outputStream);
}
// no need to add notifications; this has been done when creating the make
}
}
++segmentIndex; // increase the current segment index
currentOffset += 4 + sizeLength + elementSize; // increase current write offset by the size of the segment which has just been written
readOffset = level0Element->totalSize(); // increase the read offset by the size of the segment read from the orignial file
@ -1110,6 +1168,7 @@ void MatroskaContainer::internalMakeFile()
updatePercentage(100.0);
// flush output stream
outputStream.flush();
// handle errors which occured after renaming/creating backup file
} catch(OperationAbortedException &) {
setStream(outputStream);
reset();
@ -1129,6 +1188,14 @@ void MatroskaContainer::internalMakeFile()
BackupHelper::restoreOriginalFileFromBackupFile(fileInfo().path(), backupPath, outputStream, backupStream);
throw;
}
// handle errors which occured before renaming/creating backup file
} catch(Failure &) {
addNotification(NotificationType::Critical, "Parsing the original file failed.", context);
throw;
} catch(ios_base::failure &) {
addNotification(NotificationType::Critical, "An IO error occured when parsing the original file.", context);
throw;
}
}
}

View File

@ -53,13 +53,12 @@ namespace Media {
/*!
* \class Media::MediaFileInfo
* \brief The MediaFileInfo class extends the BasicFileInfo class.
* \brief The MediaFileInfo class allows to read and write tag information providing
* a container/tag format independent interface.
*
* The MediaFileInfo class allows to read and edit meta information
* of MP3 files with ID3 tag and MP4 files with iTunes tag. It also
* provides some technical information about these types of files such
* as contained streams.
* A bunch of other file types (see Media::ContainerFormat) can be recognized.
* It also provides some technical information such as contained streams.
*
* For examples see "cli/mainfeatures.cpp" of the tageditor repository.
*/
/*!
@ -74,7 +73,11 @@ MediaFileInfo::MediaFileInfo() :
m_tagsParsingStatus(ParsingStatus::NotParsedYet),
m_chaptersParsingStatus(ParsingStatus::NotParsedYet),
m_attachmentsParsingStatus(ParsingStatus::NotParsedYet),
m_forceFullParse(MEDIAINFO_CPP_FORCE_FULL_PARSE)
m_forceFullParse(MEDIAINFO_CPP_FORCE_FULL_PARSE),
m_minPadding(0),
m_maxPadding(0),
m_tagPosition(TagPosition::BeforeData),
m_forceTagPosition(true)
{}
/*!
@ -92,7 +95,11 @@ MediaFileInfo::MediaFileInfo(const string &path) :
m_tagsParsingStatus(ParsingStatus::NotParsedYet),
m_chaptersParsingStatus(ParsingStatus::NotParsedYet),
m_attachmentsParsingStatus(ParsingStatus::NotParsedYet),
m_forceFullParse(MEDIAINFO_CPP_FORCE_FULL_PARSE)
m_forceFullParse(MEDIAINFO_CPP_FORCE_FULL_PARSE),
m_minPadding(0),
m_maxPadding(0),
m_tagPosition(TagPosition::BeforeData),
m_forceTagPosition(true)
{}
/*!
@ -181,7 +188,7 @@ startParsingSignature:
addNotifications(notifications);
break;
} case ContainerFormat::Ebml: {
unique_ptr<MatroskaContainer> container = make_unique<MatroskaContainer>(*this, m_containerOffset);
auto container = make_unique<MatroskaContainer>(*this, m_containerOffset);
NotificationList notifications;
try {
container->parseHeader();

View File

@ -40,6 +40,12 @@ enum class TagUsage
Never /**< tags of the type are never used; a possibly existing tag of the type is removed */
};
enum class TagPosition
{
BeforeData,
AfterData
};
/*!
* \brief The ParsingStatus enum specifies whether a certain part of the file (tracks, tags, ...) has
* been parsed yet and if what the parsing result is.
@ -135,6 +141,14 @@ public:
// methods to get, set object behaviour
bool isForcingFullParse() const;
void setForceFullParse(bool forceFullParse);
size_t minPadding() const;
void setMinPadding(size_t minPadding);
size_t maxPadding() const;
void setMaxPadding(size_t maxPadding);
TagPosition tagPosition() const;
void setTagPosition(TagPosition tagPosition);
bool forceTagPosition() const;
void setForceTagPosition(bool forceTagPosition);
protected:
virtual void invalidated();
@ -164,6 +178,10 @@ private:
ParsingStatus m_attachmentsParsingStatus;
// fields specifying object behaviour
bool m_forceFullParse;
size_t m_minPadding;
size_t m_maxPadding;
TagPosition m_tagPosition;
bool m_forceTagPosition;
};
/*!
@ -362,6 +380,99 @@ inline void MediaFileInfo::setForceFullParse(bool forceFullParse)
m_forceFullParse = forceFullParse;
}
/*!
* \brief Returns the minimum padding to be written before the data blocks when applying changes.
*
* Padding in front of the file allows adding additional fields afterwards whithout needing
* to rewrite the entire file or to put tag information at the end of the file.
*
* \sa maxPadding()
* \sa tagPosition()
* \sa setMinPadding()
*/
inline size_t MediaFileInfo::minPadding() const
{
return m_minPadding;
}
/*!
* \brief Sets the minimum padding to be written before the data blocks when applying changes.
* \remarks This value might be ignored if not supported by the container/tag format or the corresponding implementation.
* \sa minPadding()
*/
inline void MediaFileInfo::setMinPadding(size_t minPadding)
{
m_minPadding = minPadding;
}
/*!
* \brief Returns the maximum padding to be written before the data blocks when applying changes.
*
* Padding in front of the file allows adding additional fields afterwards whithout needing
* to rewrite the entire file or to put tag information at the end of the file.
*
* \sa minPadding()
* \sa tagPosition()
* \sa setMaxPadding()
*/
inline size_t MediaFileInfo::maxPadding() const
{
return m_maxPadding;
}
/*!
* \brief Sets the maximum padding to be written before the data blocks when applying changes.
* \remarks This value might be ignored if not supported by the container/tag format or the corresponding implementation.
* \sa maxPadding()
*/
inline void MediaFileInfo::setMaxPadding(size_t maxPadding)
{
m_maxPadding = maxPadding;
}
/*!
* \brief Returns the position (in the output file) where the tag information is written when applying changes.
* \sa setTagPosition()
*/
inline TagPosition MediaFileInfo::tagPosition() const
{
return m_tagPosition;
}
/*!
* \brief Sets the position (in the output file) where the tag information is written when applying changes.
*
* \remarks
* - If putting the tags at another position would prevent rewriting the entire file the specified position
* might not be used if forceTagPosition() is false.
* - However if the specified position is not supported by the container/tag format or by the implementation
* for the format it is ignored (even if forceTagPosition() is true).
*/
inline void MediaFileInfo::setTagPosition(TagPosition tagPosition)
{
m_tagPosition = tagPosition;
}
/*!
* \brief Returns whether tagPosition() is forced.
* \sa setForceTagPosition()
* \sa tagPosition(), setTagPosition()
*/
inline bool MediaFileInfo::forceTagPosition() const
{
return m_forceTagPosition;
}
/*!
* \brief Sets whether tagPosition() is forced.
* \sa forceTagPosition()
* \sa tagPosition(), setTagPosition()
*/
inline void MediaFileInfo::setForceTagPosition(bool forceTagPosition)
{
m_forceTagPosition = forceTagPosition;
}
}
#endif // MEDIAINFO_H

View File

@ -78,7 +78,7 @@ void Mp4Atom::internalParse()
}
m_id = reader().readUInt32BE();
m_idLength = 4;
if(dataSize() == 1) { // atom denotes 64-bit size
if(m_dataSize == 1) { // atom denotes 64-bit size
m_dataSize = reader().readUInt64BE();
m_sizeLength = 12; // 4 bytes indicate long size denotation + 8 bytes for actual size denotation
if(dataSize() < 16 && m_dataSize != 1) {
@ -116,24 +116,37 @@ void Mp4Atom::internalParse()
* \brief This function helps to write the atom size after writing an atom to a stream.
* \param stream Specifies the stream.
* \param startOffset Specifies the start offset of the atom.
* \param denote64BitSize Specifies whether the atom denotes its size with a 64-bit unsigned integer.
*
* This function seeks back to the start offset and writes the difference between the
* previous offset and the start offset as 32-bit unsigned integer to the \a stream.
* Then it seeks back to the previous offset.
*/
void Mp4Atom::seekBackAndWriteAtomSize(std::ostream &stream, const ostream::pos_type &startOffset, bool denote64BitSize)
void Mp4Atom::seekBackAndWriteAtomSize(std::ostream &stream, const ostream::pos_type &startOffset)
{
ostream::pos_type currentOffset = stream.tellp();
stream.seekp(startOffset);
BinaryWriter writer(&stream);
if(denote64BitSize) {
writer.writeUInt32BE(0);
writer.writeUInt32BE(currentOffset - startOffset);
stream.seekp(currentOffset);
}
/*!
* \brief This function helps to write the atom size after writing an atom to a stream.
* \param stream Specifies the stream.
* \param startOffset Specifies the start offset of the atom.
*
* This function seeks back to the start offset and writes the difference between the
* previous offset and the start offset as 64-bit unsigned integer to the \a stream.
* Then it seeks back to the previous offset.
*/
void Mp4Atom::seekBackAndWriteAtomSize64(std::ostream &stream, const ostream::pos_type &startOffset)
{
ostream::pos_type currentOffset = stream.tellp();
stream.seekp(startOffset);
BinaryWriter writer(&stream);
writer.writeUInt32BE(1);
stream.seekp(4, ios_base::cur);
writer.writeUInt64BE(currentOffset - startOffset);
} else {
writer.writeUInt32BE(currentOffset - startOffset);
}
stream.seekp(currentOffset);
}

View File

@ -66,7 +66,8 @@ public:
bool isPadding() const;
uint64 firstChildOffset() const;
static void seekBackAndWriteAtomSize(std::ostream &stream, const std::ostream::pos_type &startOffset, bool denote64BitSize = false);
static void seekBackAndWriteAtomSize(std::ostream &stream, const std::ostream::pos_type &startOffset);
static void seekBackAndWriteAtomSize64(std::ostream &stream, const std::ostream::pos_type &startOffset);
protected:
Mp4Atom(containerType& container, uint64 startOffset, uint64 maxSize);

View File

@ -211,17 +211,17 @@ void Mp4Container::internalMakeFile()
setStream(backupStream);
// recreate original file
outputStream.open(fileInfo().path(), ios_base::out | ios_base::binary | ios_base::trunc);
// collect needed atoms
// collect needed atoms from the original file
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);
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);
addNotification(NotificationType::Critical, "Mandatory \"moov\"-atom not found.", context);
throw InvalidDataException();
}
} catch (Failure &) {
@ -231,20 +231,29 @@ void Mp4Container::internalMakeFile()
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
// write all top-level atoms
updateStatus("Writing header ...");
// write "ftype"-atom
ftypAtom->copyEntirely(outputStream);
// write "pdin"-atom ("progressive download information")
if(pdinAtom) {
pdinAtom->copyEntirely(outputStream);
}
// prepare for writing the actual data
vector<tuple<istream *, vector<uint64>, vector<uint64> > > trackInfos; // used when writing chunk-by-chunk
uint64 totalChunkCount = 0; // used when writing chunk-by-chunk
vector<int64> origMdatOffsets; // used when simply copying mdat
vector<int64> newMdatOffsets; // used when simply copying mdat
auto trackCount = tracks().size();
for(byte pass = 0; pass != 2; ++pass) {
if(fileInfo().tagPosition() == (pass ? TagPosition::AfterData : TagPosition::BeforeData)) {
// write "moov"-atom (contains track and tag information)
ostream::pos_type newMoovOffset = outputStream.tellp();
Mp4Atom *udtaAtom = nullptr;
uint64 newUdtaOffset = 0u;
if(isAborted()) {
throw OperationAbortedException();
}
// -> write child atoms manually, because the child "udta" has to be altered/ignored
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
for(Mp4Atom *moovChildAtom = moovAtom->firstChild(); moovChildAtom; moovChildAtom = moovChildAtom->nextSibling()) {
try {
moovChildAtom->parse();
} catch(Failure &) {
@ -252,12 +261,14 @@ void Mp4Container::internalMakeFile()
throw InvalidDataException();
}
if(moovChildAtom->id() == Mp4AtomIds::UserData) {
// found a udta (user data) atom which childs hold tag infromation
// found a "udta" (user data) atom which child "meta" holds tag information
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
// check whether the "udta"-atom needs to be written
// it has to be written only when an MP4 tag is assigned
bool writeUdtaAtom = !m_tags.empty();
// or when there is at least one child except the meta atom in the original file
if(!writeUdtaAtom) {
try {
for(Mp4Atom *udtaChildAtom = udtaAtom->firstChild(); udtaChildAtom; udtaChildAtom = udtaChildAtom->nextSibling()) {
udtaChildAtom->parse();
@ -268,7 +279,7 @@ void Mp4Container::internalMakeFile()
}
} catch(Failure &) {
addNotification(NotificationType::Warning,
"Unable to parse childs of udta atom of original file. These invalid/unknown atoms will be ignored.", context);
"Unable to parse childs of \"udta\"-atom atom of original file. These invalid/unknown atoms will be ignored.", context);
}
}
if(writeUdtaAtom) {
@ -284,30 +295,31 @@ void Mp4Container::internalMakeFile()
}
addNotifications(*m_tags.front());
}
// write rest of the child atoms of udta atom
// 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
// skip "meta"-atoms here of course
if(udtaChildAtom->id() != Mp4AtomIds::Meta) {
udtaChildAtom->copyEntirely(outputStream);
}
}
} catch(Failure &) {
addNotification(NotificationType::Warning,
"Unable to parse childs of udta atom of original file. These will be ignored.", context);
"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);
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
// 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
// -> 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();
@ -320,27 +332,19 @@ void Mp4Container::internalMakeFile()
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
// -> 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
} else {
// write other atoms and "mdat"-atom (holds actual data)
for(Mp4Atom *otherTopLevelAtom = firstElement(); otherTopLevelAtom; otherTopLevelAtom = otherTopLevelAtom->nextSibling()) {
if(isAborted()) {
throw OperationAbortedException();
@ -373,11 +377,14 @@ void Mp4Container::internalMakeFile()
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;
trackInfos.reserve(trackCount);
for(auto &track : tracks()) {
if(isAborted()) {
throw OperationAbortedException();
}
// ensure the track reads from the original file
if(&track->inputStream() == &outputStream) {
track->setInputStream(backupStream); // ensure the track reads from the original file
track->setInputStream(backupStream);
}
trackInfos.emplace_back(&track->inputStream(), track->readChunkOffsets(), track->readChunkSizes());
const vector<uint64> &chunkOffsetTable = get<1>(trackInfos.back());
@ -387,13 +394,49 @@ void Mp4Container::internalMakeFile()
addNotification(NotificationType::Critical, "Chunks of track " + numberToString<uint64, string>(track->id()) + " could not be parsed correctly.", 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(1); // denote 64 bit size
outputWriter.writeUInt32BE(Mp4AtomIds::MediaData);
outputWriter.writeUInt64BE(0); // write size of mdat atom later
CopyHelper<0x2000> copyHelper;
uint64 chunkIndex = 0;
uint64 totalChunksCopied = 0;
bool chunksCopied;
do {
if(isAborted()) {
throw OperationAbortedException();
}
// reparse what is written so far
chunksCopied = false;
for(size_t trackIndex = 0; trackIndex < trackCount; ++trackIndex) {
//auto &track = tracks()[trackIndex];
auto &trackInfo = trackInfos[trackIndex];
istream &sourceStream = *get<0>(trackInfo);
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);
chunkOffsetTable[chunkIndex] = 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::seekBackAndWriteAtomSize64(outputStream, newMdatOffset);
}
}
}
// reparse new file
updateStatus("Reparsing output file ...");
outputStream.close(); // the outputStream needs to be reopened to be able to read again
outputStream.close(); // 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;
@ -406,54 +449,27 @@ void Mp4Container::internalMakeFile()
addNotification(NotificationType::Critical, "Unable to reparse the header of the new file.", context);
throw;
}
// update chunk offsets in the "stco"-atom of each track
if(trackCount != tracks().size()) {
stringstream error;
error << "Unable to update chunk offsets (\"stco\"-atom): Number of tracks in the output file (" << tracks().size()
<< ") differs from the number of tracks in the original file (" << trackCount << ").";
addNotification(NotificationType::Critical, error.str(), context);
throw Failure();
}
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;
updateStatus("Updating chunk offset table for each track ...");
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);
auto &chunkOffsetTable = get<1>(trackInfos[trackIndex]);
if(track->chunkCount() == chunkOffsetTable.size()) {
track->updateChunkOffsets(chunkOffsetTable);
} else {
addNotification(NotificationType::Critical, "Unable to update chunk offsets of track " + numberToString(trackIndex + 1) + ": Number of chunks in the output file differs from the number of chunks in the orignal file.", context);
throw Failure();
}
}
} 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);
@ -494,7 +510,7 @@ void Mp4Container::internalMakeFile()
* \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
* Uses internally 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.
@ -510,7 +526,7 @@ void Mp4Container::updateOffsets(const std::vector<int64> &oldMdatOffsets, const
addNotification(NotificationType::Critical, "No MP4 atoms could be found.", context);
throw InvalidDataException();
}
// update "base-data-offset-present" of tfhd atom (NOT tested properly)
// 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)) {
@ -574,14 +590,16 @@ void Mp4Container::updateOffsets(const std::vector<int64> &oldMdatOffsets, const
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);
addNotification(NotificationType::Warning, "The chunk offsets of track " + track->name() + " couldn't be updated because the track seems to be invalid..", context);
throw;
}
}
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);
addNotification(NotificationType::Warning, "The chunk offsets of track " + track->name() + " couldn't be updated.", context);
throw;
}
}
}

View File

@ -290,6 +290,9 @@ uint32 mpeg4SamplingFrequencyTable[] = {
24000, 22050, 16000, 12000, 11025, 8000, 7350
};
/*!
* \brief Encapsulates all supported MPEG-4 channel configurations.
*/
namespace Mpeg4ChannelConfigs {
/*!

View File

@ -780,8 +780,8 @@ std::unique_ptr<Mpeg4VideoSpecificConfig> Mp4Track::parseVideoSpecificConfig(Sta
}
/*!
* \brief Updates the chunk offsets of the track. This is necessary when the mdat atom (which contains
* the actual chunk data) is moved.
* \brief Updates the chunk offsets of the track. This is necessary when the "mdat"-atom
* (which contains the actual chunk data) is moved.
* \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.
*
@ -849,10 +849,53 @@ void Mp4Track::updateChunkOffsets(const vector<int64> &oldMdatOffsets, const vec
}
}
/*!
* \brief Updates the chunk offsets of the track. This is necessary when the "mdat"-atom
* (which contains the actual chunk data) is moved.
* \param chunkOffsets Specifies the new chunk offset table.
*
* \throws Throws InvalidDataException when
* - there is no stream assigned.
* - the header has been considered as invalid when parsing the header information.
* - the size of \a chunkOffsets does not match chunkCount().
* - there is no atom holding these offsets.
* - the ID of the atom holding these offsets is not "stco" or "co64".
*/
void Mp4Track::updateChunkOffsets(const std::vector<uint64> &chunkOffsets)
{
if(!isHeaderValid() || !m_ostream || !m_istream || !m_stcoAtom) {
throw InvalidDataException();
}
if(chunkOffsets.size() != chunkCount()) {
throw InvalidDataException();
}
m_ostream->seekp(m_stcoAtom->dataOffset() + 8);
switch(m_stcoAtom->id()) {
case Mp4AtomIds::ChunkOffset:
for(auto offset : chunkOffsets) {
m_writer.writeUInt32BE(offset);
}
break;
case Mp4AtomIds::ChunkOffset64:
for(auto offset : chunkOffsets) {
m_writer.writeUInt64BE(offset);
}
default:
throw InvalidDataException();
}
}
/*!
* \brief Updates a particular chunk offset.
* \param chunkIndex Specifies the index of the chunk offset to be updated.
* \param offset Specifies the new chunk offset.
* \remarks This method seems to be obsolete.
* \throws Throws InvalidDataException when
* - there is no stream assigned.
* - the header has been considered as invalid when parsing the header information.
* - \a chunkIndex is not less than chunkCount().
* - there is no atom holding these offsets.
* - the ID of the atom holding these offsets is not "stco" or "co64".
*/
void Mp4Track::updateChunkOffset(uint32 chunkIndex, uint64 offset)
{
@ -1006,7 +1049,7 @@ void Mp4Track::makeMedia()
// write minf atom
makeMediaInfo();
// write size (of mdia atom)
Mp4Atom::seekBackAndWriteAtomSize(outputStream(), mdiaStartOffset, false);
Mp4Atom::seekBackAndWriteAtomSize(outputStream(), mdiaStartOffset);
}
/*!
@ -1060,7 +1103,7 @@ void Mp4Track::makeMediaInfo()
// write stbl atom
makeSampleTable();
// write size (of minf atom)
Mp4Atom::seekBackAndWriteAtomSize(outputStream(), minfStartOffset, false);
Mp4Atom::seekBackAndWriteAtomSize(outputStream(), minfStartOffset);
}
/*!
@ -1123,7 +1166,7 @@ void Mp4Track::makeSampleTable()
// write subs atom (sub-sample information)
// write size (of stbl atom)
Mp4Atom::seekBackAndWriteAtomSize(outputStream(), stblStartOffset, false);
Mp4Atom::seekBackAndWriteAtomSize(outputStream(), stblStartOffset);
}
void Mp4Track::internalParseHeader()

View File

@ -156,6 +156,7 @@ public:
// methods to update chunk offsets
void updateChunkOffsets(const std::vector<int64> &oldMdatOffsets, const std::vector<int64> &newMdatOffsets);
void updateChunkOffsets(const std::vector<uint64> &chunkOffsets);
void updateChunkOffset(uint32 chunkIndex, uint64 offset);
protected: