Introduce MatroskaTrackHeaderMaker

First step to implement applying changed track
meta-data for Matroska
This commit is contained in:
Martchus 2017-06-11 01:21:56 +02:00
parent f7ce96f4f9
commit 8d08314e3b
5 changed files with 189 additions and 40 deletions

View File

@ -816,11 +816,17 @@ void MatroskaContainer::internalMakeFile()
// define variables needed for precalculation of "Tags"- and "Attachments"-element
vector<MatroskaTagMaker> tagMaker;
tagMaker.reserve(tags().size());
uint64 tagElementsSize = 0;
uint64 tagsSize;
vector<MatroskaAttachmentMaker> attachmentMaker;
attachmentMaker.reserve(m_attachments.size());
uint64 attachedFileElementsSize = 0;
uint64 attachmentsSize;
vector<MatroskaTrackHeaderMaker> trackHeaderMaker;
trackHeaderMaker.reserve(tracks().size());
uint64 trackHeaderElementsSize = 0;
uint64 trackHeaderSize;
// define variables to store sizes, offsets and other information required to make a header and "Segment"-elements
// current segment index
@ -904,6 +910,23 @@ void MatroskaContainer::internalMakeFile()
}
attachmentsSize = attachedFileElementsSize ? 4 + EbmlElement::calculateSizeDenotationLength(attachedFileElementsSize) + attachedFileElementsSize : 0;
// calculate size of "Tracks"-element
for(auto &track : tracks()) {
track->invalidateNotifications();
try {
trackHeaderMaker.emplace_back(track->prepareMakingHeader());
if(trackHeaderMaker.back().requiredSize() > 3) {
// a track header of 3 bytes size is empty and can be skipped
trackHeaderElementsSize += trackHeaderMaker.back().requiredSize();
}
} catch(const Failure &) {
// nothing to do because notifications will be added anyways
}
addNotifications(*track);
}
trackHeaderSize = trackHeaderElementsSize ? 4 + EbmlElement::calculateSizeDenotationLength(trackHeaderElementsSize) + trackHeaderElementsSize : 0;
// inspect layout of original file
// - number of segments
// - position of tags relative to the media data
@ -1063,17 +1086,26 @@ calculateSegmentSize:
}
}
// pretend writing "Tracks"- and "Chapters"-element
for(const auto id : initializer_list<EbmlElement::identifierType>{MatroskaIds::Tracks, MatroskaIds::Chapters}) {
for(level1Element = level0Element->childById(id), index = 0; level1Element; level1Element = level1Element->siblingById(id), ++index) {
// update offset in "SeekHead"-element
if(segment.seekInfo.push(index, id, currentPosition + segment.totalDataSize)) {
goto calculateSegmentSize;
} else {
// add size of element
level1Element->makeBuffer();
segment.totalDataSize += level1Element->totalSize();
}
// pretend writing "Tracks"-element
if(trackHeaderSize) {
// update offsets in "SeekHead"-element
if(segment.seekInfo.push(0, MatroskaIds::Tracks, currentPosition + segment.totalDataSize)) {
goto calculateSegmentSize;
} else {
// add size of "Tracks"-element
segment.totalDataSize += trackHeaderSize;
}
}
// pretend writing "Chapters"-element
for(level1Element = level0Element->childById(MatroskaIds::Chapters), index = 0; level1Element; level1Element = level1Element->siblingById(MatroskaIds::Chapters), ++index) {
// update offset in "SeekHead"-element
if(segment.seekInfo.push(index, MatroskaIds::Chapters, currentPosition + segment.totalDataSize)) {
goto calculateSegmentSize;
} else {
// add size of element
level1Element->makeBuffer();
segment.totalDataSize += level1Element->totalSize();
}
}
@ -1502,7 +1534,6 @@ nonRewriteCalculations:
default:
level2Element->copyBuffer(outputStream);
level2Element->discardBuffer();
//level2Element->copyEntirely(outputStream);
}
}
// -> write "Title"-element
@ -1517,13 +1548,21 @@ nonRewriteCalculations:
EbmlElement::makeSimpleElement(outputStream, MatroskaIds::WrittingApp, appInfo, appInfoElementDataSize);
}
// write "Tracks"- and "Chapters"-element
for(const auto id : initializer_list<EbmlElement::identifierType>{MatroskaIds::Tracks, MatroskaIds::Chapters}) {
for(level1Element = level0Element->childById(id); level1Element; level1Element = level1Element->siblingById(id)) {
level1Element->copyBuffer(outputStream);
level1Element->discardBuffer();
//level1Element->copyEntirely(outputStream);
// write "Tracks"-element
if(trackHeaderElementsSize) {
outputWriter.writeUInt32BE(MatroskaIds::Tracks);
sizeLength = EbmlElement::makeSizeDenotation(trackHeaderElementsSize, buff);
outputStream.write(buff, sizeLength);
for(auto &maker : trackHeaderMaker) {
maker.make(outputStream);
}
// no need to add notifications; this has been done when creating the maker
}
// write "Chapters"-element
for(level1Element = level0Element->childById(MatroskaIds::Chapters); level1Element; level1Element = level1Element->siblingById(MatroskaIds::Chapters)) {
level1Element->copyBuffer(outputStream);
level1Element->discardBuffer();
}
if(newTagPos == ElementPosition::BeforeData && segmentIndex == 0) {
@ -1535,7 +1574,7 @@ nonRewriteCalculations:
for(auto &maker : tagMaker) {
maker.make(outputStream);
}
// no need to add notifications; this has been done when creating the make
// no need to add notifications; this has been done when creating the maker
}
// write "Attachments"-element
if(attachmentsSize) {
@ -1545,7 +1584,7 @@ nonRewriteCalculations:
for(auto &maker : attachmentMaker) {
maker.make(outputStream);
}
// no need to add notifications; this has been done when creating the make
// no need to add notifications; this has been done when creating the maker
}
}
@ -1562,12 +1601,11 @@ nonRewriteCalculations:
// write padding / "Void"-element
if(segment.newPadding) {
// calculate length
uint64 voidLength;
if(segment.newPadding < 64) {
sizeLength = 1;
*buff = (voidLength = segment.newPadding - 2) | 0x80;
*buff = static_cast<char>(voidLength = segment.newPadding - 2) | 0x80;
} else {
sizeLength = 8;
BE::getBytes(static_cast<uint64>((voidLength = segment.newPadding - 9) | 0x100000000000000), buff);
@ -1584,7 +1622,6 @@ nonRewriteCalculations:
// write media data / "Cluster"-elements
level1Element = level0Element->childById(MatroskaIds::Cluster);
if(rewriteRequired) {
// update status, check whether the operation has been aborted
if(isAborted()) {
throw OperationAbortedException();

View File

@ -115,11 +115,12 @@ void MatroskaTag::parse(EbmlElement &tagElement)
* \brief Prepares making.
* \returns Returns a MatroskaTagMaker object which can be used to actually make the tag.
* \remarks The tag must NOT be mutated after making is prepared when it is intended to actually
* make the tag using the make method of the returned object.
* make the tag using the make() method of the returned object.
* \throws Throws Media::Failure or a derived exception when a making error occurs.
*
* This method might be useful when it is necessary to know the size of the tag before making it.
* \sa make()
* \todo Make inline in next major release.
*/
MatroskaTagMaker MatroskaTag::prepareMaking()
{
@ -132,6 +133,7 @@ MatroskaTagMaker MatroskaTag::prepareMaking()
* \throws Throws Media::Failure or a derived exception when a making
* error occurs.
* \sa prepareMaking()
* \todo Make inline in next major release.
*/
void MatroskaTag::make(ostream &stream)
{

View File

@ -435,6 +435,7 @@ void MatroskaTrack::internalParseHeader()
addNotification(NotificationType::Critical, "AVC configuration is invalid.", context);
}
}
break;
default:
;
}
@ -458,4 +459,41 @@ void MatroskaTrack::internalParseHeader()
}
}
/*!
* \class Media::MatroskaTrackHeaderMaker
* \brief The MatroskaTrackHeaderMaker class helps writing Matroska "TrackEntry"-elements storing track header information.
*
* An instance can be obtained using the MatroskaTrack::prepareMakingHeader() method.
*/
/*!
* \brief Prepares making the header for the specified \a track.
* \sa See MatroskaTrack::prepareMakingHeader() for more information.
*/
MatroskaTrackHeaderMaker::MatroskaTrackHeaderMaker(const MatroskaTrack &track) :
m_track(track)
{
m_track.m_trackElement->makeBuffer();
}
/*!
* \brief Returns the associated tag.
*/
uint64 MatroskaTrackHeaderMaker::requiredSize() const
{
return m_track.m_trackElement->totalSize();
}
/*!
* \brief Saves the header for the track (specified when constructing the object) to the
* specified \a stream (makes a "TrackEntry"-element).
* \throws Throws std::ios_base::failure when an IO error occurs.
* \throws Throws Assumes the data is already validated and thus does NOT
* throw Media::Failure or a derived exception.
*/
void MatroskaTrackHeaderMaker::make(ostream &stream) const
{
m_track.m_trackElement->copyBuffer(stream);
}
}

View File

@ -7,10 +7,35 @@ namespace Media {
class EbmlElement;
class MatroskaContainer;
class MatroskaTrack;
class TAG_PARSER_EXPORT MatroskaTrackHeaderMaker
{
friend class MatroskaTrack;
public:
void make(std::ostream &stream) const;
const MatroskaTrack &track() const;
uint64 requiredSize() const;
private:
MatroskaTrackHeaderMaker(const MatroskaTrack &track);
const MatroskaTrack &m_track;
};
/*!
* \brief Returns the number of bytes which will be written when making the track.
*/
inline const MatroskaTrack &MatroskaTrackHeaderMaker::track() const
{
return m_track;
}
class TAG_PARSER_EXPORT MatroskaTrack : public AbstractTrack
{
friend class MatroskaContainer;
friend class MatroskaTrackHeaderMaker;
public:
MatroskaTrack(EbmlElement &trackElement);
@ -19,6 +44,8 @@ public:
TrackType type() const;
static MediaFormat codecIdToMediaFormat(const std::string &codecId);
MatroskaTrackHeaderMaker prepareMakingHeader() const;
void makeHeader(std::ostream &stream) const;
protected:
void internalParseHeader();
@ -27,6 +54,37 @@ private:
EbmlElement *m_trackElement;
};
/*!
* \brief Prepares making header.
* \returns Returns a MatroskaTrackHeaderMaker object which can be used to actually make the track
* header.
* \remarks The track must NOT be mutated after making is prepared when it is intended to actually
* make the tag using the make() method of the returned object.
* \throws Throws Media::Failure or a derived exception when a making error occurs.
*
* This method might be useful when it is necessary to know the size of the track header before making
* it.
* \sa make()
* \todo Make inline in next major release.
*/
inline MatroskaTrackHeaderMaker MatroskaTrack::prepareMakingHeader() const
{
return MatroskaTrackHeaderMaker(*this);
}
/*!
* \brief Writes header information to the specified \a stream (makes a "TrackEntry"-element).
* \throws Throws std::ios_base::failure when an IO error occurs.
* \throws Throws Media::Failure or a derived exception when a making
* error occurs.
* \sa prepareMaking()
* \todo Make inline in next major release.
*/
inline void MatroskaTrack::makeHeader(std::ostream &stream) const
{
prepareMakingHeader().make(stream);
}
}
#endif // MEDIA_MATROSKATRACK_H

View File

@ -33,7 +33,7 @@ void OverallTests::checkMkvTestfile1()
{
CPPUNIT_ASSERT(m_fileInfo.containerFormat() == ContainerFormat::Matroska);
const auto tracks = m_fileInfo.tracks();
CPPUNIT_ASSERT(tracks.size() == 2);
CPPUNIT_ASSERT_EQUAL(2_st, tracks.size());
for(const auto &track : tracks) {
switch(track->id()) {
case 2422994868:
@ -167,6 +167,17 @@ void OverallTests::checkMkvTestfile4()
CPPUNIT_ASSERT(track->format() == GeneralMediaFormat::Vorbis);
CPPUNIT_ASSERT(track->samplingFrequency() == 48000);
CPPUNIT_ASSERT(track->channelCount() == 2);
switch(m_tagStatus) {
case TagStatus::Original:
case TagStatus::Removed:
CPPUNIT_ASSERT_EQUAL("und"s, track->language());
break;
case TagStatus::TestMetaDataPresent:
// not implemented yet, so currently still undefined instead of German
CPPUNIT_ASSERT_EQUAL("und"s, track->language());
//CPPUNIT_ASSERT_EQUAL("ger"s, track->language());
break;
}
break;
default:
CPPUNIT_FAIL("unknown track ID");
@ -520,11 +531,18 @@ void OverallTests::checkMkvConstraints()
*/
void OverallTests::setMkvTestMetaData()
{
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
auto *container = static_cast<MatroskaContainer *>(m_fileInfo.container());
// change the present tag
const string fileName(m_fileInfo.fileName());
if(fileName == "test4.mkv") {
// test4.mkv has no tag, so one must be created first
m_fileInfo.container()->createTag(TagTarget(50));
container->createTag(TagTarget(50));
// also change language of track "3171450505" to German
MatroskaTrack *track = container->trackById(3171450505);
CPPUNIT_ASSERT(track);
track->setLanguage("ger");
} else if(fileName == "handbrake-chapters-2.mkv") {
// remove 2nd tag
m_fileInfo.removeTag(m_fileInfo.tags().at(1));
@ -535,21 +553,17 @@ void OverallTests::setMkvTestMetaData()
// add an additional tag targeting the first track
TagTarget::IdContainerType trackIds;
trackIds.emplace_back(m_fileInfo.tracks().at(0)->id());
if(Tag *newTag = m_fileInfo.container()->createTag(TagTarget(30, trackIds))) {
newTag->setValue(KnownField::Album, m_testAlbum);
newTag->setValue(KnownField::PartNumber, m_testPartNumber);
newTag->setValue(KnownField::TotalParts, m_testTotalParts);
} else {
CPPUNIT_FAIL("can not create tag");
}
Tag *newTag = container->createTag(TagTarget(30, trackIds));
CPPUNIT_ASSERT_MESSAGE("create tag", newTag);
newTag->setValue(KnownField::Album, m_testAlbum);
newTag->setValue(KnownField::PartNumber, m_testPartNumber);
newTag->setValue(KnownField::TotalParts, m_testTotalParts);
// assign an attachment
if(AbstractAttachment *attachment = m_fileInfo.container()->createAttachment()) {
attachment->setFile(TestUtilities::testFilePath("matroska_wave1/logo3_256x256.png"));
attachment->setMimeType("image/png");
attachment->setName("cover.jpg");
} else {
CPPUNIT_FAIL("can not create attachment");
}
AbstractAttachment *attachment = container->createAttachment();
CPPUNIT_ASSERT_MESSAGE("create attachment", attachment);
attachment->setFile(TestUtilities::testFilePath("matroska_wave1/logo3_256x256.png"));
attachment->setMimeType("image/png");
attachment->setName("cover.jpg");
}
/*!