Tag Parser 12.2.0
C++ library for reading and writing MP4 (iTunes), ID3, Vorbis, Opus, FLAC and Matroska tags
Loading...
Searching...
No Matches
mp4container.cpp
Go to the documentation of this file.
1#include "./mp4container.h"
2#include "./mp4ids.h"
3
4#include "../backuphelper.h"
5#include "../exceptions.h"
6#include "../mediafileinfo.h"
7
8#include <c++utilities/conversion/stringbuilder.h>
9#include <c++utilities/io/binaryreader.h>
10#include <c++utilities/io/binarywriter.h>
11#include <c++utilities/io/copy.h>
12#include <c++utilities/io/path.h>
13
14#include <filesystem>
15#include <memory>
16#include <numeric>
17#include <tuple>
18
19using namespace std;
20using namespace CppUtilities;
21
22namespace TagParser {
23
32const CppUtilities::DateTime Mp4Container::epoch = DateTime::fromDate(1904, 1, 1);
33
37Mp4Container::Mp4Container(MediaFileInfo &fileInfo, std::uint64_t startOffset)
38 : GenericContainer<MediaFileInfo, Mp4Tag, Mp4Track, Mp4Atom>(fileInfo, startOffset)
39 , m_fragmented(false)
40{
41}
42
46
52
54{
55 if (m_firstElement) {
56 const Mp4Atom *mediaDataAtom = m_firstElement->siblingById(Mp4AtomIds::MediaData, diag);
57 const Mp4Atom *userDataAtom = m_firstElement->subelementByPath(diag, Mp4AtomIds::Movie, Mp4AtomIds::UserData);
58 if (mediaDataAtom && userDataAtom) {
59 return userDataAtom->startOffset() < mediaDataAtom->startOffset() ? ElementPosition::BeforeData : ElementPosition::AfterData;
60 }
61 }
63}
64
66{
67 if (m_firstElement) {
68 const Mp4Atom *mediaDataAtom = m_firstElement->siblingById(Mp4AtomIds::MediaData, diag);
69 const Mp4Atom *movieAtom = m_firstElement->siblingById(Mp4AtomIds::Movie, diag);
70 if (mediaDataAtom && movieAtom) {
71 return movieAtom->startOffset() < mediaDataAtom->startOffset() ? ElementPosition::BeforeData : ElementPosition::AfterData;
72 }
73 }
75}
76
78{
79 CPP_UTILITIES_UNUSED(progress) //const string context("parsing header of MP4 container"); will be used when generating notifications
80 m_firstElement = make_unique<Mp4Atom>(*this, startOffset());
81 m_firstElement->parse(diag);
82 auto *const ftypAtom = m_firstElement->siblingByIdIncludingThis(Mp4AtomIds::FileType, diag);
83 if (!ftypAtom) {
84 m_doctype.clear();
85 m_version = 0;
86 return;
87 }
88 stream().seekg(static_cast<iostream::off_type>(ftypAtom->dataOffset()));
89 m_doctype = reader().readString(4);
90 m_version = reader().readUInt32BE();
91}
92
94{
95 CPP_UTILITIES_UNUSED(progress)
96 const string context("parsing tags of MP4 container");
98 if (!udtaAtom) {
99 return;
100 }
101 auto *metaAtom = udtaAtom->childById(Mp4AtomIds::Meta, diag);
102 bool surplusMetaAtoms = false;
103 while (metaAtom) {
104 metaAtom->parse(diag);
105 m_tags.emplace_back(make_unique<Mp4Tag>());
106 try {
107 m_tags.back()->parse(*metaAtom, diag);
108 } catch (const NoDataFoundException &) {
109 m_tags.pop_back();
110 }
111 if ((metaAtom = metaAtom->siblingById(Mp4AtomIds::Meta, diag))) {
112 surplusMetaAtoms = true;
113 }
114 if (!m_tags.empty()) {
115 break;
116 }
117 }
118 if (surplusMetaAtoms) {
119 diag.emplace_back(DiagLevel::Warning, "udta atom contains multiple meta atoms. Surplus meta atoms will be ignored.", context);
120 }
121}
122
124{
125 static const string context("parsing tracks of MP4 container");
126 try {
127 // get moov atom which holds track information
128 if (Mp4Atom *moovAtom = firstElement()->siblingByIdIncludingThis(Mp4AtomIds::Movie, diag)) {
129 // get mvhd atom which holds overall track information
130 if (Mp4Atom *mvhdAtom = moovAtom->childById(Mp4AtomIds::MovieHeader, diag)) {
131 if (mvhdAtom->dataSize() > 0) {
132 stream().seekg(static_cast<iostream::off_type>(mvhdAtom->dataOffset()));
133 std::uint8_t version = reader().readByte();
134 if ((version == 1 && mvhdAtom->dataSize() >= 32) || (mvhdAtom->dataSize() >= 20)) {
135 stream().seekg(3, ios_base::cur); // skip flags
136 switch (version) {
137 case 0:
138 m_creationTime = epoch + TimeSpan::fromSeconds(static_cast<TimeSpan::TickType>(reader().readUInt32BE()));
139 m_modificationTime = epoch + TimeSpan::fromSeconds(static_cast<TimeSpan::TickType>(reader().readUInt32BE()));
140 m_timeScale = reader().readUInt32BE();
141 m_duration = TimeSpan::fromSeconds(static_cast<TimeSpan::TickType>(reader().readUInt32BE()))
142 / static_cast<TimeSpan::TickType>(m_timeScale);
143 break;
144 case 1:
145 m_creationTime = epoch + TimeSpan::fromSeconds(static_cast<TimeSpan::TickType>(reader().readUInt64BE()));
146 m_modificationTime = epoch + TimeSpan::fromSeconds(static_cast<TimeSpan::TickType>(reader().readUInt64BE()));
147 m_timeScale = reader().readUInt32BE();
148 m_duration = TimeSpan::fromSeconds(static_cast<TimeSpan::TickType>(reader().readUInt64BE()))
149 / static_cast<TimeSpan::TickType>(m_timeScale);
150 break;
151 default:;
152 }
153 } else {
154 diag.emplace_back(DiagLevel::Critical, "mvhd atom is truncated.", context);
155 }
156 } else {
157 diag.emplace_back(DiagLevel::Critical, "mvhd atom is empty.", context);
158 }
159 } else {
160 diag.emplace_back(DiagLevel::Critical, "mvhd atom is does not exist.", context);
161 }
162 // get mvex atom which holds default values for fragmented files
164 m_fragmented = true;
165 if (mehdAtom->dataSize() > 0) {
166 stream().seekg(static_cast<iostream::off_type>(mehdAtom->dataOffset()));
167 unsigned int durationSize = reader().readByte() == 1u ? 8u : 4u; // duration size depends on atom version
168 if (mehdAtom->dataSize() >= 4 + durationSize) {
169 stream().seekg(3, ios_base::cur); // skip flags
170 switch (durationSize) {
171 case 4u:
172 m_duration = TimeSpan::fromSeconds(static_cast<double>(reader().readUInt32BE()) / static_cast<double>(m_timeScale));
173 break;
174 case 8u:
175 m_duration = TimeSpan::fromSeconds(static_cast<double>(reader().readUInt64BE()) / static_cast<double>(m_timeScale));
176 break;
177 default:;
178 }
179 } else {
180 diag.emplace_back(DiagLevel::Warning, "mehd atom is truncated.", context);
181 }
182 }
183 }
184 // get first trak atoms which hold information for each track
185 Mp4Atom *trakAtom = moovAtom->childById(Mp4AtomIds::Track, diag);
186 int trackNum = 1;
187 while (trakAtom) {
188 try {
189 trakAtom->parse(diag);
190 } catch (const Failure &) {
191 diag.emplace_back(DiagLevel::Warning, "Unable to parse child atom of moov.", context);
192 }
193 // parse the trak atom using the Mp4Track class
194 m_tracks.emplace_back(make_unique<Mp4Track>(*trakAtom));
195 try { // try to parse header
196 m_tracks.back()->parseHeader(diag, progress);
197 } catch (const Failure &) {
198 diag.emplace_back(DiagLevel::Critical, argsToString("Unable to parse track ", trackNum, '.'), context);
199 }
200 trakAtom = trakAtom->siblingById(Mp4AtomIds::Track, diag); // get next trak atom
201 ++trackNum;
202 }
203 // get overall duration, creation time and modification time if not determined yet
204 if (m_duration.isNull() || m_modificationTime.isNull() || m_creationTime.isNull()) {
205 for (const auto &track : tracks()) {
206 if (track->duration() > m_duration) {
208 }
211 }
214 }
215 }
216 }
217 }
218 } catch (const Failure &) {
219 diag.emplace_back(DiagLevel::Warning, "Unable to parse moov atom.", context);
220 }
221}
222
224{
225 static const string context("making MP4 container");
226 progress.updateStep("Calculating atom sizes and padding ...");
227
228 // basic validation of original file
229 if (!isHeaderParsed()) {
230 diag.emplace_back(DiagLevel::Critical, "The header has not been parsed yet.", context);
231 throw InvalidDataException();
232 }
233
234 // define variables needed to parse atoms of original file
235 if (!firstElement()) {
236 diag.emplace_back(DiagLevel::Critical, "No MP4 atoms could be found.", context);
237 throw InvalidDataException();
238 }
239
240 // define variables needed to manage file layout
241 // -> whether media data is written chunk by chunk (need to write chunk by chunk if tracks have been altered)
242 const bool writeChunkByChunk = m_tracksAltered;
243 // -> whether rewrite is required (always required when forced to rewrite or when tracks have been altered)
244 bool rewriteRequired = fileInfo().isForcingRewrite() || writeChunkByChunk || !fileInfo().saveFilePath().empty();
245 // -> use the preferred tag position/index position (force one wins, if both are force tag pos wins; might be changed later if none is forced)
246 ElementPosition initialNewTagPos
248 ElementPosition newTagPos = initialNewTagPos;
249 // -> current tag position (determined later)
250 ElementPosition currentTagPos;
251 // -> holds new padding (before actual data)
252 std::uint64_t newPadding;
253 // -> holds new padding (after actual data)
254 std::uint64_t newPaddingEnd;
255 // -> holds current offset
256 std::uint64_t currentOffset;
257 // -> holds track information, used when writing chunk-by-chunk
258 vector<tuple<istream *, vector<std::uint64_t>, vector<std::uint64_t>>> trackInfos;
259 // -> holds offsets of media data atoms in original file, used when simply copying mdat
260 vector<std::int64_t> origMediaDataOffsets;
261 // -> holds offsets of media data atoms in new file, used when simply copying mdat
262 vector<std::int64_t> newMediaDataOffsets;
263 // -> new size of movie atom and user data atom
264 std::uint64_t movieAtomSize, userDataAtomSize;
265 // -> track count of original file
266 const auto trackCount = this->trackCount();
267
268 // find relevant atoms in original file
269 Mp4Atom *fileTypeAtom, *progressiveDownloadInfoAtom, *movieAtom, *firstMediaDataAtom, *firstMovieFragmentAtom /*, *userDataAtom*/;
270 Mp4Atom *level0Atom, *level1Atom, *level2Atom, *lastAtomToBeWritten = nullptr;
271 try {
272 // file type atom (mandatory)
273 if ((fileTypeAtom = firstElement()->siblingByIdIncludingThis(Mp4AtomIds::FileType, diag))) {
274 // buffer atom
275 fileTypeAtom->makeBuffer();
276 } else {
277 // throw error if missing
278 diag.emplace_back(DiagLevel::Critical, "Mandatory \"ftyp\"-atom not found in the source file.", context);
279 throw InvalidDataException();
280 }
281
282 // progressive download information atom (not mandatory)
283 if ((progressiveDownloadInfoAtom = firstElement()->siblingByIdIncludingThis(Mp4AtomIds::ProgressiveDownloadInformation, diag))) {
284 // buffer atom
285 progressiveDownloadInfoAtom->makeBuffer();
286 }
287
288 // movie atom (mandatory)
289 if (!(movieAtom = firstElement()->siblingByIdIncludingThis(Mp4AtomIds::Movie, diag))) {
290 // throw error if missing
291 diag.emplace_back(DiagLevel::Critical, "Mandatory \"moov\"-atom not found in the source file.", context);
292 throw InvalidDataException();
293 }
294
295 // movie fragment atom (indicates dash file)
296 if ((firstMovieFragmentAtom = firstElement()->siblingById(Mp4AtomIds::MovieFragment, diag))) {
297 // there is at least one movie fragment atom -> consider file being dash
298 // -> can not write chunk-by-chunk (currently)
299 if (writeChunkByChunk) {
300 diag.emplace_back(DiagLevel::Critical, "Writing chunk-by-chunk is not implemented for DASH files.", context);
302 }
303 // -> tags must be placed at the beginning
304 newTagPos = ElementPosition::BeforeData;
305 }
306
307 // media data atom (mandatory?)
308 // -> consider not only mdat as media data atom; consider everything not handled otherwise as media data
309 for (firstMediaDataAtom = nullptr, level0Atom = firstElement(); level0Atom; level0Atom = level0Atom->nextSibling()) {
310 level0Atom->parse(diag);
311 switch (level0Atom->id()) {
315 case Mp4AtomIds::Free:
316 case Mp4AtomIds::Skip:
317 continue;
318 default:
319 firstMediaDataAtom = level0Atom;
320 }
321 break;
322 }
323
324 // determine current tag position
325 // -> since tags are nested in the movie atom its position is relevant here
326 if (firstMediaDataAtom) {
327 currentTagPos = firstMediaDataAtom->startOffset() < movieAtom->startOffset() ? ElementPosition::AfterData : ElementPosition::BeforeData;
328 if (newTagPos == ElementPosition::Keep) {
329 newTagPos = currentTagPos;
330 }
331 } else {
332 currentTagPos = ElementPosition::Keep;
333 }
334
335 // ensure index and tags are always placed at the beginning when dealing with DASH files
336 if (firstMovieFragmentAtom) {
337 if (initialNewTagPos == ElementPosition::AfterData) {
338 diag.emplace_back(
339 DiagLevel::Warning, "Sorry, but putting index/tags at the end is not possible when dealing with DASH files.", context);
340 }
341 initialNewTagPos = newTagPos = ElementPosition::BeforeData;
342 }
343
344 // user data atom (currently not used)
345 //userDataAtom = movieAtom->childById(Mp4AtomIds::UserData);
346
347 } catch (const NotImplementedException &) {
348 throw;
349
350 } catch (const Failure &) {
351 // can't ignore parsing errors here
352 diag.emplace_back(DiagLevel::Critical, "Unable to parse the overall atom structure of the source file.", context);
353 throw InvalidDataException();
354 }
355
356 progress.stopIfAborted();
357
358 // calculate sizes
359 // -> size of tags
360 vector<Mp4TagMaker> tagMaker;
361 std::uint64_t tagsSize = 0;
362 tagMaker.reserve(m_tags.size());
363 for (auto &tag : m_tags) {
364 try {
365 tagMaker.emplace_back(tag->prepareMaking(diag));
366 tagsSize += tagMaker.back().requiredSize();
367 } catch (const Failure &) {
368 }
369 }
370
371 // -> size of movie atom (contains track and tag information)
372 movieAtomSize = userDataAtomSize = 0;
373 try {
374 // add size of children
375 for (level0Atom = movieAtom; level0Atom; level0Atom = level0Atom->siblingById(Mp4AtomIds::Movie, diag)) {
376 for (level1Atom = level0Atom->firstChild(); level1Atom; level1Atom = level1Atom->nextSibling()) {
377 level1Atom->parse(diag);
378 switch (level1Atom->id()) {
380 try {
381 for (level2Atom = level1Atom->firstChild(); level2Atom; level2Atom = level2Atom->nextSibling()) {
382 level2Atom->parse(diag);
383 switch (level2Atom->id()) {
384 case Mp4AtomIds::Meta:
385 // ignore meta data here; it is added separately
386 break;
387 default:
388 // add size of unknown children of the user data atom
389 userDataAtomSize += level2Atom->totalSize();
390 level2Atom->makeBuffer();
391 }
392 }
393 } catch (const Failure &) {
394 // invalid children might be ignored as not mandatory
395 diag.emplace_back(
396 DiagLevel::Critical, "Unable to parse the children of \"udta\"-atom of the source file; ignoring them.", context);
397 }
398 break;
400 // ignore track atoms here; they are added separately
401 break;
402 default:
403 // add size of unknown children of the movie atom
404 movieAtomSize += level1Atom->totalSize();
405 level1Atom->makeBuffer();
406 }
407 }
408 }
409
410 // add size of meta data
411 if (userDataAtomSize += tagsSize) {
412 Mp4Atom::addHeaderSize(userDataAtomSize);
413 movieAtomSize += userDataAtomSize;
414 }
415
416 // add size of track atoms
417 for (const auto &track : tracks()) {
418 movieAtomSize += track->requiredSize(diag);
419 }
420
421 // add header size
422 Mp4Atom::addHeaderSize(movieAtomSize);
423 } catch (const Failure &) {
424 // can't ignore parsing errors here
425 diag.emplace_back(DiagLevel::Critical, "Unable to parse the children of \"moov\"-atom of the source file.", context);
426 throw InvalidDataException();
427 }
428
429 progress.stopIfAborted();
430
431 // check whether there are atoms to be voided after movie next sibling (only relevant when not rewriting)
432 if (!rewriteRequired) {
433 newPaddingEnd = 0;
434 std::uint64_t currentSum = 0;
435 for (level0Atom = firstMediaDataAtom; level0Atom; level0Atom = level0Atom->nextSibling()) {
436 level0Atom->parse(diag);
437 switch (level0Atom->id()) {
441 case Mp4AtomIds::Free:
442 case Mp4AtomIds::Skip:
443 // must void these if they occur "between" the media data
444 currentSum += level0Atom->totalSize();
445 break;
446 default:
447 newPaddingEnd += currentSum;
448 currentSum = 0;
449 lastAtomToBeWritten = level0Atom;
450 }
451 }
452 }
453
454 // calculate padding if no rewrite is required; otherwise use the preferred padding
455calculatePadding:
456 if (rewriteRequired) {
457 newPadding = (fileInfo().preferredPadding() && fileInfo().preferredPadding() < 8 ? 8 : fileInfo().preferredPadding());
458 } else {
459 // file type atom
460 currentOffset = fileTypeAtom->totalSize();
461
462 // progressive download information atom
463 if (progressiveDownloadInfoAtom) {
464 currentOffset += progressiveDownloadInfoAtom->totalSize();
465 }
466
467 // if writing tags before data: movie atom (contains tag)
468 switch (newTagPos) {
471 currentOffset += movieAtomSize;
472 break;
473 default:;
474 }
475
476 // check whether there is sufficiant space before the next atom
477 if (!(rewriteRequired = firstMediaDataAtom && currentOffset > firstMediaDataAtom->startOffset())) {
478 // there is sufficiant space
479 // -> check whether the padding matches specifications
480 // min padding: says "at least ... byte should be reserved to prepend further tag info", so the padding at the end
481 // shouldn't be tanken into account (it can't be used to prepend further tag info)
482 // max padding: says "do not waste more than ... byte", so here all padding should be taken into account
483 newPadding = firstMediaDataAtom->startOffset() - currentOffset;
484 rewriteRequired = (newPadding > 0 && newPadding < 8) || newPadding < fileInfo().minPadding()
485 || (newPadding + newPaddingEnd) > fileInfo().maxPadding();
486 }
487 if (rewriteRequired) {
488 // can't put the tags before media data
489 if (!firstMovieFragmentAtom && !fileInfo().forceTagPosition() && !fileInfo().forceIndexPosition()
490 && newTagPos != ElementPosition::AfterData) {
491 // writing tag before media data is not forced, its not a DASH file and tags aren't already at the end
492 // -> try to put the tags at the end
493 newTagPos = ElementPosition::AfterData;
494 rewriteRequired = false;
495 } else {
496 // writing tag before media data is forced -> rewrite the file
497 // when rewriting anyways, ensure the preferred tag position is used
498 newTagPos = initialNewTagPos == ElementPosition::Keep ? currentTagPos : initialNewTagPos;
499 }
500 // in any case: recalculate padding
501 goto calculatePadding;
502 } else {
503 // tags can be put before the media data
504 // -> ensure newTagPos is not ElementPosition::Keep
505 if (newTagPos == ElementPosition::Keep) {
506 newTagPos = ElementPosition::BeforeData;
507 }
508 }
509 }
510
511 // setup stream(s) for writing
512 // -> update status
513 progress.nextStepOrStop("Preparing streams ...");
514
515 // -> define variables needed to handle output stream and backup stream (required when rewriting the file)
516 string originalPath = fileInfo().path(), backupPath;
517 NativeFileStream &outputStream = fileInfo().stream();
518 NativeFileStream backupStream; // create a stream to open the backup/original file for the case rewriting the file is required
519 BinaryWriter outputWriter(&outputStream);
520
521 if (rewriteRequired) {
522 if (fileInfo().saveFilePath().empty()) {
523 // move current file to temp dir and reopen it as backupStream, recreate original file
524 try {
525 BackupHelper::createBackupFileCanonical(fileInfo().backupDirectory(), originalPath, backupPath, outputStream, backupStream);
526 // recreate original file, define buffer variables
527 outputStream.open(originalPath, ios_base::out | ios_base::binary | ios_base::trunc);
528 } catch (const std::ios_base::failure &failure) {
529 diag.emplace_back(
530 DiagLevel::Critical, argsToString("Creation of temporary file (to rewrite the original file) failed: ", failure.what()), context);
531 throw;
532 }
533 } else {
534 // open the current file as backupStream and create a new outputStream at the specified "save file path"
535 try {
536 backupStream.exceptions(ios_base::badbit | ios_base::failbit);
537 backupStream.open(BasicFileInfo::pathForOpen(fileInfo().path()).data(), ios_base::in | ios_base::binary);
538 fileInfo().close();
539 outputStream.open(BasicFileInfo::pathForOpen(fileInfo().saveFilePath()).data(), ios_base::out | ios_base::binary | ios_base::trunc);
540 } catch (const std::ios_base::failure &failure) {
541 diag.emplace_back(DiagLevel::Critical, argsToString("Opening streams to write output file failed: ", failure.what()), context);
542 throw;
543 }
544 }
545
546 // set backup stream as associated input stream since we need the original elements to write the new file
547 setStream(backupStream);
548
549 // TODO: reduce code duplication
550
551 } else { // !rewriteRequired
552 // ensure everything to make track atoms is buffered before altering the source file
553 for (const auto &track : tracks()) {
554 track->bufferTrackAtoms(diag);
555 }
556
557 // reopen original file to ensure it is opened for writing
558 try {
559 fileInfo().close();
560 outputStream.open(fileInfo().path(), ios_base::in | ios_base::out | ios_base::binary);
561 } catch (const std::ios_base::failure &failure) {
562 diag.emplace_back(DiagLevel::Critical, argsToString("Opening the file with write permissions failed: ", failure.what()), context);
563 throw;
564 }
565 }
566
567 // start actual writing
568 try {
569 // write header
570 progress.nextStepOrStop("Writing header and tags ...");
571 // -> make file type atom
572 fileTypeAtom->copyBuffer(outputStream);
573 fileTypeAtom->discardBuffer();
574 // -> make progressive download info atom
575 if (progressiveDownloadInfoAtom) {
576 progressiveDownloadInfoAtom->copyBuffer(outputStream);
577 progressiveDownloadInfoAtom->discardBuffer();
578 }
579
580 // set input/output streams of each track
581 for (auto &track : tracks()) {
582 // ensure the track reads from the original file
583 if (&track->inputStream() == &outputStream) {
584 track->setInputStream(backupStream);
585 }
586 // ensure the track writes to the output file
587 track->setOutputStream(outputStream);
588 }
589
590 // write movie atom / padding and media data
591 for (std::uint8_t pass = 0; pass != 2; ++pass) {
592 if (newTagPos == (pass ? ElementPosition::AfterData : ElementPosition::BeforeData)) {
593 // define function to write tracks
594 auto tracksWritten = false;
595 const auto writeTracks = [this, &diag, &tracksWritten] {
596 if (tracksWritten) {
597 return;
598 }
599 for (auto &track : tracks()) {
600 track->makeTrack(diag);
601 }
602 tracksWritten = true;
603 };
604
605 // define function to write user data
606 auto userDataWritten = false;
607 auto writeUserData = [level0Atom, level1Atom, level2Atom, movieAtom, &userDataWritten, userDataAtomSize, &outputStream, &outputWriter,
608 &tagMaker, &diag]() mutable {
609 if (userDataWritten || !userDataAtomSize) {
610 return;
611 }
612
613 // writer user data atom header
614 Mp4Atom::makeHeader(userDataAtomSize, Mp4AtomIds::UserData, outputWriter);
615
616 // write children of user data atom
617 bool metaAtomWritten = false;
618 for (level0Atom = movieAtom; level0Atom; level0Atom = level0Atom->siblingById(Mp4AtomIds::Movie, diag)) {
619 for (level1Atom = level0Atom->childById(Mp4AtomIds::UserData, diag); level1Atom;
620 level1Atom = level1Atom->siblingById(Mp4AtomIds::UserData, diag)) {
621 for (level2Atom = level1Atom->firstChild(); level2Atom; level2Atom = level2Atom->nextSibling()) {
622 switch (level2Atom->id()) {
623 case Mp4AtomIds::Meta:
624 // write meta atom
625 for (auto &maker : tagMaker) {
626 maker.make(outputStream, diag);
627 }
628 metaAtomWritten = true;
629 break;
630 default:
631 // write buffered data
632 level2Atom->copyBuffer(outputStream);
633 level2Atom->discardBuffer();
634 }
635 }
636 }
637 }
638
639 // write meta atom if not already written
640 if (!metaAtomWritten) {
641 for (auto &maker : tagMaker) {
642 maker.make(outputStream, diag);
643 }
644 }
645
646 userDataWritten = true;
647 };
648
649 // write movie atom
650 // -> write movie atom header
651 Mp4Atom::makeHeader(movieAtomSize, Mp4AtomIds::Movie, outputWriter);
652
653 // -> write children of movie atom preserving the original order
654 for (level0Atom = movieAtom; level0Atom; level0Atom = level0Atom->siblingById(Mp4AtomIds::Movie, diag)) {
655 for (level1Atom = level0Atom->firstChild(); level1Atom; level1Atom = level1Atom->nextSibling()) {
656 switch (level1Atom->id()) {
658 writeTracks();
659 break;
661 writeUserData();
662 break;
663 default:
664 // write buffered data
665 level1Atom->copyBuffer(outputStream);
666 level1Atom->discardBuffer();
667 }
668 }
669 }
670
671 // -> write tracks and user data atoms if not already happened within the loop
672 writeTracks();
673 writeUserData();
674
675 } else {
676 // write padding
677 if (newPadding) {
678 // write free atom header
679 if (newPadding < numeric_limits<std::uint32_t>::max()) {
680 outputWriter.writeUInt32BE(static_cast<std::uint32_t>(newPadding));
681 outputWriter.writeUInt32BE(Mp4AtomIds::Free);
682 newPadding -= 8;
683 } else {
684 outputWriter.writeUInt32BE(1);
685 outputWriter.writeUInt32BE(Mp4AtomIds::Free);
686 outputWriter.writeUInt64BE(newPadding);
687 newPadding -= 16;
688 }
689
690 // write zeroes
691 for (; newPadding; --newPadding) {
692 outputStream.put(0);
693 }
694 }
695
696 // write media data
697 if (rewriteRequired) {
698 for (level0Atom = firstMediaDataAtom; level0Atom; level0Atom = level0Atom->nextSibling()) {
699 level0Atom->parse(diag);
700 switch (level0Atom->id()) {
704 case Mp4AtomIds::Free:
705 case Mp4AtomIds::Skip:
706 break;
708 if (writeChunkByChunk) {
709 // write actual data separately when writing chunk-by-chunk
710 break;
711 } else {
712 // store media data offsets when not writing chunk-by-chunk to be able to update chunk offset table
713 origMediaDataOffsets.push_back(static_cast<std::int64_t>(level0Atom->startOffset()));
714 newMediaDataOffsets.push_back(outputStream.tellp());
715 }
716 [[fallthrough]];
717 default:
718 // update status
719 progress.updateStep("Writing atom: " + level0Atom->idToString());
720 // copy atom entirely and forward status update calls
721 level0Atom->copyEntirely(outputStream, diag, &progress);
722 }
723 }
724
725 // when writing chunk-by-chunk write media data now
726 if (writeChunkByChunk) {
727 // read chunk offset and chunk size table from the old file which are required to get chunks
728 progress.updateStep("Reading chunk offsets and sizes from the original file ...");
729 trackInfos.reserve(trackCount);
730 std::uint64_t totalChunkCount = 0;
731 std::uint64_t totalMediaDataSize = 0;
732 for (auto &track : tracks()) {
733 progress.stopIfAborted();
734
735 // emplace information
736 trackInfos.emplace_back(
737 &track->inputStream(), track->readChunkOffsets(fileInfo().isForcingFullParse(), diag), track->readChunkSizes(diag));
738
739 // check whether the chunks could be parsed correctly
740 const vector<std::uint64_t> &chunkOffsetTable = get<1>(trackInfos.back());
741 const vector<std::uint64_t> &chunkSizesTable = get<2>(trackInfos.back());
742 if (track->chunkCount() != chunkOffsetTable.size() || track->chunkCount() != chunkSizesTable.size()) {
743 diag.emplace_back(DiagLevel::Critical,
744 "Chunks of track " % numberToString<std::uint64_t, string>(track->id()) + " could not be parsed correctly.",
745 context);
746 }
747
748 // increase total chunk count and size
749 totalChunkCount += track->chunkCount();
750 totalMediaDataSize += std::accumulate(chunkSizesTable.cbegin(), chunkSizesTable.cend(), static_cast<std::uint64_t>(0u));
751 }
752
753 // write media data chunk-by-chunk
754 // -> write header of media data atom
755 Mp4Atom::addHeaderSize(totalMediaDataSize);
756 Mp4Atom::makeHeader(totalMediaDataSize, Mp4AtomIds::MediaData, outputWriter);
757
758 // -> copy chunks
759 CopyHelper<0x2000> copyHelper;
760 std::uint64_t chunkIndexWithinTrack = 0, totalChunksCopied = 0;
761 bool anyChunksCopied;
762 do {
763 progress.stopIfAborted();
764
765 // copy a chunk from each track
766 anyChunksCopied = false;
767 for (size_t trackIndex = 0; trackIndex < trackCount; ++trackIndex) {
768 // get source stream and tables for current track
769 auto &trackInfo = trackInfos[trackIndex];
770 istream &sourceStream = *get<0>(trackInfo);
771 vector<std::uint64_t> &chunkOffsetTable = get<1>(trackInfo);
772 const vector<std::uint64_t> &chunkSizesTable = get<2>(trackInfo);
773
774 // still chunks to be copied (of this track)?
775 if (chunkIndexWithinTrack < chunkOffsetTable.size() && chunkIndexWithinTrack < chunkSizesTable.size()) {
776 // copy chunk, update entry in chunk offset table
777 sourceStream.seekg(static_cast<streamoff>(chunkOffsetTable[chunkIndexWithinTrack]));
778 chunkOffsetTable[chunkIndexWithinTrack] = static_cast<std::uint64_t>(outputStream.tellp());
779 copyHelper.copy(sourceStream, outputStream, chunkSizesTable[chunkIndexWithinTrack]);
780
781 // update counter / status
782 anyChunksCopied = true;
783 ++totalChunksCopied;
784 }
785 }
786
787 // incrase chunk index within track, update progress percentage
788 if (!(++chunkIndexWithinTrack % 10)) {
789 progress.updateStepPercentage(static_cast<std::uint8_t>(totalChunksCopied * 100 / totalChunkCount));
790 }
791
792 } while (anyChunksCopied);
793 }
794
795 } else {
796 // can't just skip next movie sibling
797 for (level0Atom = firstMediaDataAtom; level0Atom; level0Atom = level0Atom->nextSibling()) {
798 level0Atom->parse(diag);
799 switch (level0Atom->id()) {
803 // must void these if they occur "between" the media data
804 outputStream.seekp(4, ios_base::cur);
805 outputWriter.writeUInt32BE(Mp4AtomIds::Free);
806 break;
807 default:
808 outputStream.seekp(static_cast<iostream::off_type>(level0Atom->totalSize()), ios_base::cur);
809 }
810 if (level0Atom == lastAtomToBeWritten) {
811 break;
812 }
813 }
814 }
815 }
816 }
817
818 // reparse what is written so far
819 progress.updateStep("Reparsing output file ...");
820 if (rewriteRequired) {
821 // report new size
822 fileInfo().reportSizeChanged(static_cast<std::uint64_t>(outputStream.tellp()));
823 // "save as path" is now the regular path
824 if (!fileInfo().saveFilePath().empty()) {
825 fileInfo().reportPathChanged(fileInfo().saveFilePath());
826 fileInfo().setSaveFilePath(string());
827 }
828 // the outputStream needs to be reopened to be able to read again
829 outputStream.close();
830 outputStream.open(BasicFileInfo::pathForOpen(fileInfo().path()).data(), ios_base::in | ios_base::out | ios_base::binary);
831 setStream(outputStream);
832 } else {
833 const auto newSize = static_cast<std::uint64_t>(outputStream.tellp());
834 if (newSize < fileInfo().size()) {
835 // file is smaller after the modification -> truncate
836 // -> close stream before truncating
837 outputStream.close();
838 // -> truncate file
839 auto ec = std::error_code();
840 std::filesystem::resize_file(makeNativePath(BasicFileInfo::pathForOpen(fileInfo().path())), newSize, ec);
841 if (!ec) {
842 fileInfo().reportSizeChanged(newSize);
843 } else {
844 diag.emplace_back(DiagLevel::Critical, "Unable to truncate the file: " + ec.message(), context);
845 }
846 // -> reopen the stream again
847 outputStream.open(BasicFileInfo::pathForOpen(fileInfo().path()).data(), ios_base::in | ios_base::out | ios_base::binary);
848 } else {
849 // file is longer after the modification -> just report new size
850 fileInfo().reportSizeChanged(newSize);
851 }
852 }
853
854 reset();
855 try {
856 parseTracks(diag, progress);
857 } catch (const OperationAbortedException &) {
858 throw;
859 } catch (const Failure &) {
860 diag.emplace_back(DiagLevel::Critical, "Unable to reparse the new file.", context);
861 throw;
862 }
863
864 if (rewriteRequired) {
865 // check whether the track count of the new file equals the track count of old file
866 if (trackCount != tracks().size()) {
867 diag.emplace_back(DiagLevel::Critical,
868 argsToString("Unable to update chunk offsets (\"stco\"/\"co64\"-atom): Number of tracks in the output file (", tracks().size(),
869 ") differs from the number of tracks in the original file (", trackCount, ")."),
870 context);
871 throw Failure();
872 }
873
874 // update chunk offset table
875 if (writeChunkByChunk) {
876 progress.updateStep("Updating chunk offset table for each track ...");
877 for (size_t trackIndex = 0; trackIndex != trackCount; ++trackIndex) {
878 const auto &track = tracks()[trackIndex];
879 const auto &chunkOffsetTable = get<1>(trackInfos[trackIndex]);
880 if (track->chunkCount() == chunkOffsetTable.size()) {
881 track->updateChunkOffsets(chunkOffsetTable);
882 } else {
883 diag.emplace_back(DiagLevel::Critical,
884 argsToString("Unable to update chunk offsets of track ", (trackIndex + 1),
885 ": Number of chunks in the output file differs from the number of chunks in the original file."),
886 context);
887 throw Failure();
888 }
889 }
890 } else {
891 progress.updateStep("Updating chunk offset table for each track ...");
892 updateOffsets(origMediaDataOffsets, newMediaDataOffsets, diag, progress);
893 }
894 }
895
896 // prevent deferring final write operations (to catch and handle possible errors here)
897 outputStream.flush();
898
899 // handle errors (which might have been occurred after renaming/creating backup file)
900 } catch (...) {
901 BackupHelper::handleFailureAfterFileModifiedCanonical(fileInfo(), originalPath, backupPath, outputStream, backupStream, diag, context);
902 }
903}
904
917void Mp4Container::updateOffsets(const std::vector<std::int64_t> &oldMdatOffsets, const std::vector<std::int64_t> &newMdatOffsets, Diagnostics &diag,
919{
920 // do NOT invalidate the status here since this method is internally called by internalMakeFile(), just update the status
921 const string context("updating MP4 container chunk offset table");
922 if (!firstElement()) {
923 diag.emplace_back(DiagLevel::Critical, "No MP4 atoms could be found.", context);
924 throw InvalidDataException();
925 }
926 // update "base-data-offset-present" of "tfhd"-atom (NOT tested properly)
927 try {
928 for (Mp4Atom *moofAtom = firstElement()->siblingById(Mp4AtomIds::MovieFragment, diag); moofAtom;
929 moofAtom = moofAtom->siblingById(Mp4AtomIds::MovieFragment, diag)) {
930 moofAtom->parse(diag);
931 try {
932 for (Mp4Atom *trafAtom = moofAtom->childById(Mp4AtomIds::TrackFragment, diag); trafAtom;
933 trafAtom = trafAtom->siblingById(Mp4AtomIds::TrackFragment, diag)) {
934 trafAtom->parse(diag);
935 int tfhdAtomCount = 0;
936 for (Mp4Atom *tfhdAtom = trafAtom->childById(Mp4AtomIds::TrackFragmentHeader, diag); tfhdAtom;
937 tfhdAtom = tfhdAtom->siblingById(Mp4AtomIds::TrackFragmentHeader, diag)) {
938 tfhdAtom->parse(diag);
939 ++tfhdAtomCount;
940 if (tfhdAtom->dataSize() < 8) {
941 diag.emplace_back(DiagLevel::Warning, "tfhd atom is truncated.", context);
942 continue;
943 }
944 stream().seekg(static_cast<iostream::off_type>(tfhdAtom->dataOffset()) + 1);
945 std::uint32_t flags = reader().readUInt24BE();
946 if (!(flags & 1)) {
947 continue;
948 }
949 if (tfhdAtom->dataSize() < 16) {
950 diag.emplace_back(DiagLevel::Warning, "tfhd atom (denoting base-data-offset-present) is truncated.", context);
951 continue;
952 }
953 stream().seekg(4, ios_base::cur); // skip track ID
954 std::uint64_t off = reader().readUInt64BE();
955 for (auto iOld = oldMdatOffsets.cbegin(), iNew = newMdatOffsets.cbegin(), end = oldMdatOffsets.cend(); iOld != end;
956 ++iOld, ++iNew) {
957 if (off < static_cast<std::uint64_t>(*iOld)) {
958 continue;
959 }
960 off += static_cast<std::uint64_t>(*iNew - *iOld);
961 stream().seekp(static_cast<iostream::off_type>(tfhdAtom->dataOffset()) + 8);
962 writer().writeUInt64BE(off);
963 break;
964 }
965 }
966 switch (tfhdAtomCount) {
967 case 0:
968 diag.emplace_back(DiagLevel::Warning, "traf atom doesn't contain mandatory tfhd atom.", context);
969 break;
970 case 1:
971 break;
972 default:
973 diag.emplace_back(
974 DiagLevel::Warning, "traf atom stores multiple tfhd atoms but it should only contain exactly one tfhd atom.", context);
975 }
976 }
977 } catch (const Failure &) {
978 diag.emplace_back(DiagLevel::Critical, "Unable to parse children of top-level atom moof.", context);
979 }
980 }
981 } catch (const Failure &) {
982 diag.emplace_back(DiagLevel::Critical, "Unable to parse top-level atom moof.", context);
983 }
984 // update each track
985 for (auto &track : tracks()) {
986 if (!track->isHeaderValid()) {
987 try {
988 track->parseHeader(diag, progress);
989 } catch (const Failure &) {
990 diag.emplace_back(DiagLevel::Warning,
991 "The chunk offsets of track " % track->name() + " couldn't be updated because the track seems to be invalid..", context);
992 throw;
993 }
994 }
995 if (track->isHeaderValid()) {
996 try {
997 track->updateChunkOffsets(oldMdatOffsets, newMdatOffsets);
998 } catch (const Failure &) {
999 diag.emplace_back(DiagLevel::Warning, "The chunk offsets of track " % track->name() + " couldn't be updated.", context);
1000 throw;
1001 }
1002 }
1003 }
1004}
1005
1006} // namespace TagParser
The AbortableProgressFeedback class provides feedback about an ongoing operation via callbacks.
void stopIfAborted() const
Throws an OperationAbortedException if aborted.
void nextStepOrStop(const std::string &step, std::uint8_t stepPercentage=0)
Throws an OperationAbortedException if aborted; otherwise the data for the next step is set.
CppUtilities::DateTime m_modificationTime
std::iostream & stream()
Returns the related stream.
std::uint64_t startOffset() const
Returns the start offset in the related stream.
void parseTracks(Diagnostics &diag, AbortableProgressFeedback &progress)
Parses the tracks of the file if not parsed yet.
std::uint64_t version() const
Returns the version if known; otherwise returns 0.
bool isHeaderParsed() const
Returns an indication whether the header has been parsed yet.
void setStream(std::iostream &stream)
Sets the related stream.
CppUtilities::BinaryWriter & writer()
Returns the related BinaryWriter.
CppUtilities::BinaryReader & reader()
Returns the related BinaryReader.
CppUtilities::DateTime m_creationTime
CppUtilities::TimeSpan m_duration
std::uint64_t id() const
Returns the track ID if known; otherwise returns 0.
const CppUtilities::DateTime & modificationTime() const
Returns the time of the last modification if known; otherwise returns a DateTime of zero ticks.
const CppUtilities::DateTime & creationTime() const
Returns the creation time if known; otherwise returns a DateTime of zero ticks.
std::istream & inputStream()
Returns the associated input stream.
void parseHeader(Diagnostics &diag, AbortableProgressFeedback &progress)
Parses technical information about the track from the header.
bool isHeaderValid() const
Returns an indication whether the track header is valid.
void setOutputStream(std::ostream &stream)
Assigns another output stream.
void setInputStream(std::istream &stream)
Assigns another input stream.
const CppUtilities::TimeSpan & duration() const
Returns the duration if known; otherwise returns a TimeSpan of zero ticks.
const std::string name() const
Returns the track name if known; otherwise returns an empty string.
void reportPathChanged(std::string_view newPath)
Call this function to report that the path changed.
const std::string & path() const
Returns the path of the current file.
std::uint64_t size() const
Returns size of the current file in bytes.
CppUtilities::NativeFileStream & stream()
Returns the std::fstream for the current instance.
void close()
A possibly opened std::fstream will be closed.
static std::string_view pathForOpen(std::string_view url)
Returns removes the "file:/" prefix from url to be able to pass it to functions like open(),...
void reportSizeChanged(std::uint64_t newSize)
Call this function to report that the size changed.
void updateStep(const std::string &step, std::uint8_t stepPercentage=0)
Updates the current step and invokes the first callback specified on construction.
void updateStepPercentage(std::uint8_t stepPercentage)
Updates the current step percentage and invokes the second callback specified on construction (or the...
The Diagnostics class is a container for DiagMessage.
The class inherits from std::exception and serves as base class for exceptions thrown by the elements...
The GenericContainer class helps parsing header, track, tag and chapter information of a file.
const std::vector< std::unique_ptr< Mp4Track > > & tracks() const
void reset() override
Discards all parsing results.
void copyEntirely(TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
Writes the entire element including all children to the specified targetStream.
void copyBuffer(TargetStream &targetStream)
Copies buffered data to targetStream.
std::uint64_t startOffset() const
Returns the start offset in the related stream.
void discardBuffer()
Discards buffered data.
const IdentifierType & id() const
Returns the element ID.
ImplementationType * childById(const IdentifierType &id, Diagnostics &diag)
Returns the first child with the specified id.
ImplementationType * nextSibling()
Returns the next sibling of the element.
ImplementationType * firstChild()
Returns the first child of the element.
ImplementationType * subelementByPath(Diagnostics &diag, IdentifierType item)
Returns the sub element for the specified path.
std::uint64_t totalSize() const
Returns the total size of the element.
void parse(Diagnostics &diag)
Parses the header information of the element which is read from the related stream at the start offse...
void makeBuffer()
Buffers the element (header and data).
ImplementationType * siblingById(const IdentifierType &id, Diagnostics &diag)
Returns the first sibling with the specified id.
The exception that is thrown when the data to be parsed or to be made seems invalid and therefore can...
The MediaFileInfo class allows to read and write tag information providing a container/tag format ind...
bool isForcingRewrite() const
Returns whether forcing rewriting (when applying changes) is enabled.
void setSaveFilePath(std::string_view saveFilePath)
Sets the "save file path".
bool forceIndexPosition() const
Returns whether indexPosition() is forced.
const std::string & saveFilePath() const
Returns the "save file path" which has been set using setSaveFilePath().
std::size_t maxPadding() const
Returns the maximum padding to be written before the data blocks when applying changes.
std::size_t preferredPadding() const
Returns the padding to be written before the data block when applying changes and the file needs to b...
std::size_t minPadding() const
Returns the minimum padding to be written before the data blocks when applying changes.
ElementPosition tagPosition() const
Returns the position (in the output file) where the tag information is written when applying changes.
bool forceTagPosition() const
Returns whether tagPosition() is forced.
ElementPosition indexPosition() const
Returns the position (in the output file) where the index is written when applying changes.
The Mp4Atom class helps to parse MP4 files.
static constexpr void addHeaderSize(std::uint64_t &dataSize)
Adds the header size to the specified data size.
Definition mp4atom.h:81
std::string idToString() const
Converts the specified atom ID to a printable string.
Definition mp4atom.h:67
static void makeHeader(std::uint64_t size, std::uint32_t id, CppUtilities::BinaryWriter &writer)
Writes an MP4 atom header to the specified stream.
Definition mp4atom.cpp:171
ElementPosition determineIndexPosition(Diagnostics &diag) const override
Determines the position of the index.
ElementPosition determineTagPosition(Diagnostics &diag) const override
Determines the position of the tags inside the file.
static const CppUtilities::DateTime epoch
Dates within MP4 tracks are expressed as the number of seconds since this date.
void reset() override
Discards all parsing results.
Mp4Container(MediaFileInfo &fileInfo, std::uint64_t startOffset)
Constructs a new container for the specified fileInfo at the specified startOffset.
void internalParseHeader(Diagnostics &diag, AbortableProgressFeedback &progress) override
Internally called to parse the header.
void internalParseTags(Diagnostics &diag, AbortableProgressFeedback &progress) override
Internally called to parse the tags.
void internalParseTracks(Diagnostics &diag, AbortableProgressFeedback &progress) override
Internally called to parse the tracks.
void internalMakeFile(Diagnostics &diag, AbortableProgressFeedback &progress) override
Internally called to make the file.
Implementation of TagParser::Tag for the MP4 container.
Mp4TagMaker prepareMaking(Diagnostics &diag)
Prepares making.
Definition mp4tag.cpp:432
Implementation of TagParser::AbstractTrack for the MP4 container.
std::uint32_t chunkCount() const
Returns the number of chunks denoted by the stco atom.
Definition mp4track.h:238
std::vector< std::uint64_t > readChunkSizes(TagParser::Diagnostics &diag)
Reads the chunk sizes from the stsz (sample sizes) and stsc (samples per chunk) atom.
Definition mp4track.cpp:561
void updateChunkOffsets(const std::vector< std::int64_t > &oldMdatOffsets, const std::vector< std::int64_t > &newMdatOffsets)
Updates the chunk offsets of the track.
Definition mp4track.cpp:944
std::uint64_t requiredSize(Diagnostics &diag) const
Returns the number of bytes written when calling makeTrack().
std::vector< std::uint64_t > readChunkOffsets(bool parseFragments, Diagnostics &diag)
Reads the chunk offsets from the stco atom and fragments if parseFragments is true.
Definition mp4track.cpp:197
void bufferTrackAtoms(Diagnostics &diag)
Buffers all atoms required by the makeTrack() method.
void makeTrack(Diagnostics &diag)
Makes the track entry ("trak"-atom) for the track.
The exception that is thrown when the data to be parsed holds no parsable information (e....
This exception is thrown when the an operation is invoked that has not been implemented yet.
The exception that is thrown when an operation has been stopped and thus not successfully completed b...
TAG_PARSER_EXPORT void handleFailureAfterFileModifiedCanonical(MediaFileInfo &fileInfo, const std::string &originalPath, const std::string &backupPath, CppUtilities::NativeFileStream &outputStream, CppUtilities::NativeFileStream &backupStream, Diagnostics &diag, const std::string &context="making file")
Handles a failure/abort which occurred after the file has been modified.
TAG_PARSER_EXPORT void createBackupFileCanonical(const std::string &backupDir, std::string &originalPath, std::string &backupPath, CppUtilities::NativeFileStream &originalStream, CppUtilities::NativeFileStream &backupStream)
Creates a backup file like createBackupFile() but canonicalizes originalPath before doing the backup.
@ ProgressiveDownloadInformation
Definition mp4ids.h:52
Contains all classes and functions of the TagInfo library.
Definition aaccodebook.h:10
ElementPosition
Definition settings.h:13