Tag Parser 12.4.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 // -> media size of original file
268 auto mediaSize = std::uint64_t();
269
270 // find relevant atoms in original file and determine media size
271 Mp4Atom *fileTypeAtom, *progressiveDownloadInfoAtom, *movieAtom, *firstMediaDataAtom, *firstMovieFragmentAtom /*, *userDataAtom*/;
272 Mp4Atom *level0Atom, *level1Atom, *level2Atom, *lastAtomToBeWritten = nullptr;
273 try {
274 // file type atom (mandatory)
275 if ((fileTypeAtom = firstElement()->siblingByIdIncludingThis(Mp4AtomIds::FileType, diag))) {
276 // buffer atom
277 fileTypeAtom->makeBuffer();
278 } else {
279 // throw error if missing
280 diag.emplace_back(DiagLevel::Critical, "Mandatory \"ftyp\"-atom not found in the source file.", context);
281 throw InvalidDataException();
282 }
283
284 // progressive download information atom (not mandatory)
285 if ((progressiveDownloadInfoAtom = firstElement()->siblingByIdIncludingThis(Mp4AtomIds::ProgressiveDownloadInformation, diag))) {
286 // buffer atom
287 progressiveDownloadInfoAtom->makeBuffer();
288 }
289
290 // movie atom (mandatory)
291 if (!(movieAtom = firstElement()->siblingByIdIncludingThis(Mp4AtomIds::Movie, diag))) {
292 // throw error if missing
293 diag.emplace_back(DiagLevel::Critical, "Mandatory \"moov\"-atom not found in the source file.", context);
294 throw InvalidDataException();
295 }
296
297 // movie fragment atom (indicates dash file)
298 if ((firstMovieFragmentAtom = firstElement()->siblingById(Mp4AtomIds::MovieFragment, diag))) {
299 // there is at least one movie fragment atom -> consider file being dash
300 // -> can not write chunk-by-chunk (currently)
301 if (writeChunkByChunk) {
302 diag.emplace_back(DiagLevel::Critical, "Writing chunk-by-chunk is not implemented for DASH files.", context);
304 }
305 // -> tags must be placed at the beginning
306 newTagPos = ElementPosition::BeforeData;
307 }
308
309 // media data atom (mandatory?)
310 // -> consider not only mdat as media data atom; consider everything not handled otherwise as media data
311 for (firstMediaDataAtom = nullptr, level0Atom = firstElement(); level0Atom; level0Atom = level0Atom->nextSibling()) {
312 level0Atom->parse(diag);
313 switch (level0Atom->id()) {
317 case Mp4AtomIds::Free:
318 case Mp4AtomIds::Skip:
319 continue;
320 default:
321 if (!firstMediaDataAtom) {
322 firstMediaDataAtom = level0Atom;
323 }
324 mediaSize += level0Atom->totalSize();
325 }
326 }
327
328 // determine current tag position
329 // -> since tags are nested in the movie atom its position is relevant here
330 if (firstMediaDataAtom) {
331 currentTagPos = firstMediaDataAtom->startOffset() < movieAtom->startOffset() ? ElementPosition::AfterData : ElementPosition::BeforeData;
332 if (newTagPos == ElementPosition::Keep) {
333 newTagPos = currentTagPos;
334 }
335 } else {
336 currentTagPos = ElementPosition::Keep;
337 }
338
339 // ensure index and tags are always placed at the beginning when dealing with DASH files
340 if (firstMovieFragmentAtom) {
341 if (initialNewTagPos == ElementPosition::AfterData) {
342 diag.emplace_back(
343 DiagLevel::Warning, "Sorry, but putting index/tags at the end is not possible when dealing with DASH files.", context);
344 }
345 initialNewTagPos = newTagPos = ElementPosition::BeforeData;
346 }
347
348 // user data atom (currently not used)
349 //userDataAtom = movieAtom->childById(Mp4AtomIds::UserData);
350
351 } catch (const NotImplementedException &) {
352 throw;
353
354 } catch (const Failure &) {
355 // can't ignore parsing errors here
356 diag.emplace_back(DiagLevel::Critical, "Unable to parse the overall atom structure of the source file.", context);
357 throw InvalidDataException();
358 }
359
360 progress.stopIfAborted();
361
362 // calculate sizes
363 // -> size of tags
364 vector<Mp4TagMaker> tagMaker;
365 std::uint64_t tagsSize = 0;
366 tagMaker.reserve(m_tags.size());
367 for (auto &tag : m_tags) {
368 try {
369 tagMaker.emplace_back(tag->prepareMaking(diag));
370 tagsSize += tagMaker.back().requiredSize();
371 } catch (const Failure &) {
372 }
373 }
374
375 // -> size of movie atom (contains track and tag information)
376calculateMovieAtomSize:
377 movieAtomSize = userDataAtomSize = 0;
378 try {
379 // add size of children
380 for (level0Atom = movieAtom; level0Atom; level0Atom = level0Atom->siblingById(Mp4AtomIds::Movie, diag)) {
381 for (level1Atom = level0Atom->firstChild(); level1Atom; level1Atom = level1Atom->nextSibling()) {
382 level1Atom->parse(diag);
383 switch (level1Atom->id()) {
385 try {
386 for (level2Atom = level1Atom->firstChild(); level2Atom; level2Atom = level2Atom->nextSibling()) {
387 level2Atom->parse(diag);
388 switch (level2Atom->id()) {
389 case Mp4AtomIds::Meta:
390 // ignore meta data here; it is added separately
391 break;
392 default:
393 // add size of unknown children of the user data atom
394 userDataAtomSize += level2Atom->totalSize();
395 level2Atom->makeBuffer();
396 }
397 }
398 } catch (const Failure &) {
399 // invalid children might be ignored as not mandatory
400 diag.emplace_back(
401 DiagLevel::Critical, "Unable to parse the children of \"udta\"-atom of the source file; ignoring them.", context);
402 }
403 break;
405 // ignore track atoms here; they are added separately
406 break;
407 default:
408 // add size of unknown children of the movie atom
409 movieAtomSize += level1Atom->totalSize();
410 level1Atom->makeBuffer();
411 }
412 }
413 }
414
415 // add size of meta data
416 if (userDataAtomSize += tagsSize) {
417 Mp4Atom::addHeaderSize(userDataAtomSize);
418 movieAtomSize += userDataAtomSize;
419 }
420
421 // add size of track atoms
422 for (const auto &track : tracks()) {
423 movieAtomSize += track->requiredSize(diag);
424 }
425
426 // add header size
427 Mp4Atom::addHeaderSize(movieAtomSize);
428 } catch (const Failure &) {
429 // can't ignore parsing errors here
430 diag.emplace_back(DiagLevel::Critical, "Unable to parse the children of \"moov\"-atom of the source file.", context);
431 throw InvalidDataException();
432 }
433
434 progress.stopIfAborted();
435
436 // check whether there are atoms to be voided after movie next sibling (only relevant when not rewriting)
437 if (!rewriteRequired) {
438 newPaddingEnd = 0;
439 std::uint64_t currentSum = 0;
440 for (level0Atom = firstMediaDataAtom; level0Atom; level0Atom = level0Atom->nextSibling()) {
441 level0Atom->parse(diag);
442 switch (level0Atom->id()) {
446 case Mp4AtomIds::Free:
447 case Mp4AtomIds::Skip:
448 // must void these if they occur "between" the media data
449 currentSum += level0Atom->totalSize();
450 break;
451 default:
452 newPaddingEnd += currentSum;
453 currentSum = 0;
454 lastAtomToBeWritten = level0Atom;
455 }
456 }
457 }
458
459 // calculate offset of padding
460 // -> file type atom
461 currentOffset = fileTypeAtom->totalSize();
462 // -> progressive download information atom
463 if (progressiveDownloadInfoAtom) {
464 currentOffset += progressiveDownloadInfoAtom->totalSize();
465 }
466 // -> if writing tags before data: movie atom (contains tag)
467 switch (newTagPos) {
470 currentOffset += movieAtomSize;
471 break;
472 default:;
473 }
474
475 // calculate padding if no rewrite is required; otherwise use the preferred padding
476calculatePadding:
477 if (rewriteRequired) {
478 newPadding = (fileInfo().preferredPadding() && fileInfo().preferredPadding() < 8 ? 8 : fileInfo().preferredPadding());
479 } else {
480 // check whether there is sufficiant space before the next atom
481 if (!(rewriteRequired = firstMediaDataAtom && currentOffset > firstMediaDataAtom->startOffset())) {
482 // there is sufficiant space
483 // -> check whether the padding is not between 1 and 7 bytes because that size range cannot be padded
484 // -> check whether the padding matches specifications
485 // min padding: says "at least ... byte should be reserved to prepend further tag info", so the padding at the end
486 // shouldn't be tanken into account (it can't be used to prepend further tag info)
487 // max padding: says "do not waste more than ... byte", so here all padding should be taken into account
488 newPadding = firstMediaDataAtom->startOffset() - currentOffset;
489 rewriteRequired = (newPadding > 0 && newPadding < 8) || newPadding < fileInfo().minPadding()
490 || (newPadding + newPaddingEnd) > fileInfo().maxPadding();
491 }
492 if (rewriteRequired) {
493 // can't put the tags before media data
494 if (!firstMovieFragmentAtom && !fileInfo().forceTagPosition() && !fileInfo().forceIndexPosition()
495 && newTagPos != ElementPosition::AfterData) {
496 // writing tag before media data is not forced, its not a DASH file and tags aren't already at the end
497 // -> try to put the tags at the end
498 newTagPos = ElementPosition::AfterData;
499 rewriteRequired = false;
500 } else {
501 // writing tag before media data is forced -> rewrite the file
502 // when rewriting anyways, ensure the preferred tag position is used
503 newTagPos = initialNewTagPos == ElementPosition::Keep ? currentTagPos : initialNewTagPos;
504 }
505 // in any case: recalculate padding
506 goto calculatePadding;
507 } else {
508 // tags can be put before the media data
509 // -> ensure newTagPos is not ElementPosition::Keep
510 if (newTagPos == ElementPosition::Keep) {
511 newTagPos = ElementPosition::BeforeData;
512 }
513 }
514 }
515
516 // calculate end offset of media data and check if any of the chunk offset tables need to be converted
517 currentOffset += newPadding + mediaSize;
518 if (auto changedChunkOffsetSize = false; currentOffset > std::numeric_limits<std::uint32_t>::max()) {
519 for (auto &track : tracks()) {
520 if (track->chunkOffsetSize() < 8) {
521 diag.emplace_back(DiagLevel::Information,
522 argsToString("Chunk offset table of track ", track->id(), " will not fit new offsets (up to ", currentOffset,
523 "). It will be converted to 64-bit."),
524 context);
526 changedChunkOffsetSize = true;
527 }
528 }
529 if (changedChunkOffsetSize) {
530 goto calculateMovieAtomSize;
531 }
532 }
533
534 // setup stream(s) for writing
535 // -> update status
536 progress.nextStepOrStop("Preparing streams ...");
537
538 // -> define variables needed to handle output stream and backup stream (required when rewriting the file)
539 string originalPath = fileInfo().path(), backupPath;
540 NativeFileStream &outputStream = fileInfo().stream();
541 NativeFileStream backupStream; // create a stream to open the backup/original file for the case rewriting the file is required
542 BinaryWriter outputWriter(&outputStream);
543
544 if (rewriteRequired) {
545 if (fileInfo().saveFilePath().empty()) {
546 // move current file to temp dir and reopen it as backupStream, recreate original file
547 try {
548 BackupHelper::createBackupFileCanonical(fileInfo().backupDirectory(), originalPath, backupPath, outputStream, backupStream);
549 // recreate original file, define buffer variables
550 outputStream.open(originalPath, ios_base::out | ios_base::binary | ios_base::trunc);
551 } catch (const std::ios_base::failure &failure) {
552 diag.emplace_back(
553 DiagLevel::Critical, argsToString("Creation of temporary file (to rewrite the original file) failed: ", failure.what()), context);
554 throw;
555 }
556 } else {
557 // open the current file as backupStream and create a new outputStream at the specified "save file path"
558 try {
559 backupStream.exceptions(ios_base::badbit | ios_base::failbit);
560 backupStream.open(BasicFileInfo::pathForOpen(fileInfo().path()).data(), ios_base::in | ios_base::binary);
561 fileInfo().close();
562 outputStream.open(BasicFileInfo::pathForOpen(fileInfo().saveFilePath()).data(), ios_base::out | ios_base::binary | ios_base::trunc);
563 } catch (const std::ios_base::failure &failure) {
564 diag.emplace_back(DiagLevel::Critical, argsToString("Opening streams to write output file failed: ", failure.what()), context);
565 throw;
566 }
567 }
568
569 // set backup stream as associated input stream since we need the original elements to write the new file
570 setStream(backupStream);
571
572 // TODO: reduce code duplication
573
574 } else { // !rewriteRequired
575 // ensure everything to make track atoms is buffered before altering the source file
576 for (const auto &track : tracks()) {
577 track->bufferTrackAtoms(diag);
578 }
579
580 // reopen original file to ensure it is opened for writing
581 try {
582 fileInfo().close();
583 outputStream.open(fileInfo().path(), ios_base::in | ios_base::out | ios_base::binary);
584 } catch (const std::ios_base::failure &failure) {
585 diag.emplace_back(DiagLevel::Critical, argsToString("Opening the file with write permissions failed: ", failure.what()), context);
586 throw;
587 }
588 }
589
590 // start actual writing
591 try {
592 // write header
593 progress.nextStepOrStop("Writing header and tags ...");
594 // -> make file type atom
595 fileTypeAtom->copyBuffer(outputStream);
596 fileTypeAtom->discardBuffer();
597 // -> make progressive download info atom
598 if (progressiveDownloadInfoAtom) {
599 progressiveDownloadInfoAtom->copyBuffer(outputStream);
600 progressiveDownloadInfoAtom->discardBuffer();
601 }
602
603 // set input/output streams of each track
604 for (auto &track : tracks()) {
605 // ensure the track reads from the original file
606 if (&track->inputStream() == &outputStream) {
607 track->setInputStream(backupStream);
608 }
609 // ensure the track writes to the output file
610 track->setOutputStream(outputStream);
611 }
612
613 // write movie atom / padding and media data
614 for (std::uint8_t pass = 0; pass != 2; ++pass) {
615 if (newTagPos == (pass ? ElementPosition::AfterData : ElementPosition::BeforeData)) {
616 // define function to write tracks
617 auto tracksWritten = false;
618 const auto writeTracks = [this, &diag, &tracksWritten] {
619 if (tracksWritten) {
620 return;
621 }
622 for (auto &track : tracks()) {
623 track->makeTrack(diag);
624 }
625 tracksWritten = true;
626 };
627
628 // define function to write user data
629 auto userDataWritten = false;
630 auto writeUserData = [level0Atom, level1Atom, level2Atom, movieAtom, &userDataWritten, userDataAtomSize, &outputStream, &outputWriter,
631 &tagMaker, &diag]() mutable {
632 if (userDataWritten || !userDataAtomSize) {
633 return;
634 }
635
636 // writer user data atom header
637 Mp4Atom::makeHeader(userDataAtomSize, Mp4AtomIds::UserData, outputWriter);
638
639 // write children of user data atom
640 bool metaAtomWritten = false;
641 for (level0Atom = movieAtom; level0Atom; level0Atom = level0Atom->siblingById(Mp4AtomIds::Movie, diag)) {
642 for (level1Atom = level0Atom->childById(Mp4AtomIds::UserData, diag); level1Atom;
643 level1Atom = level1Atom->siblingById(Mp4AtomIds::UserData, diag)) {
644 for (level2Atom = level1Atom->firstChild(); level2Atom; level2Atom = level2Atom->nextSibling()) {
645 switch (level2Atom->id()) {
646 case Mp4AtomIds::Meta:
647 // write meta atom
648 for (auto &maker : tagMaker) {
649 maker.make(outputStream, diag);
650 }
651 metaAtomWritten = true;
652 break;
653 default:
654 // write buffered data
655 level2Atom->copyBuffer(outputStream);
656 level2Atom->discardBuffer();
657 }
658 }
659 }
660 }
661
662 // write meta atom if not already written
663 if (!metaAtomWritten) {
664 for (auto &maker : tagMaker) {
665 maker.make(outputStream, diag);
666 }
667 }
668
669 userDataWritten = true;
670 };
671
672 // write movie atom
673 // -> write movie atom header
674 Mp4Atom::makeHeader(movieAtomSize, Mp4AtomIds::Movie, outputWriter);
675
676 // -> write children of movie atom preserving the original order
677 for (level0Atom = movieAtom; level0Atom; level0Atom = level0Atom->siblingById(Mp4AtomIds::Movie, diag)) {
678 for (level1Atom = level0Atom->firstChild(); level1Atom; level1Atom = level1Atom->nextSibling()) {
679 switch (level1Atom->id()) {
681 writeTracks();
682 break;
684 writeUserData();
685 break;
686 default:
687 // write buffered data
688 level1Atom->copyBuffer(outputStream);
689 level1Atom->discardBuffer();
690 }
691 }
692 }
693
694 // -> write tracks and user data atoms if not already happened within the loop
695 writeTracks();
696 writeUserData();
697
698 } else {
699 // write padding
700 if (newPadding) {
701 // write free atom header
702 if (newPadding < numeric_limits<std::uint32_t>::max()) {
703 outputWriter.writeUInt32BE(static_cast<std::uint32_t>(newPadding));
704 outputWriter.writeUInt32BE(Mp4AtomIds::Free);
705 newPadding -= 8;
706 } else {
707 outputWriter.writeUInt32BE(1);
708 outputWriter.writeUInt32BE(Mp4AtomIds::Free);
709 outputWriter.writeUInt64BE(newPadding);
710 newPadding -= 16;
711 }
712 MediaFileInfo::writePadding(outputStream, newPadding);
713 }
714
715 // write media data
716 if (rewriteRequired) {
717 for (level0Atom = firstMediaDataAtom; level0Atom; level0Atom = level0Atom->nextSibling()) {
718 level0Atom->parse(diag);
719 switch (level0Atom->id()) {
723 case Mp4AtomIds::Free:
724 case Mp4AtomIds::Skip:
725 break;
727 if (writeChunkByChunk) {
728 // write actual data separately when writing chunk-by-chunk
729 break;
730 } else {
731 // store media data offsets when not writing chunk-by-chunk to be able to update chunk offset table
732 origMediaDataOffsets.push_back(static_cast<std::int64_t>(level0Atom->startOffset()));
733 newMediaDataOffsets.push_back(outputStream.tellp());
734 }
735 [[fallthrough]];
736 default:
737 // update status
738 progress.updateStep("Writing atom: " + level0Atom->idToString());
739 // copy atom entirely and forward status update calls
740 level0Atom->copyEntirely(outputStream, diag, &progress);
741 }
742 }
743
744 // when writing chunk-by-chunk write media data now
745 if (writeChunkByChunk) {
746 // read chunk offset and chunk size table from the old file which are required to get chunks
747 progress.updateStep("Reading chunk offsets and sizes from the original file ...");
748 trackInfos.reserve(trackCount);
749 std::uint64_t totalChunkCount = 0;
750 std::uint64_t totalMediaDataSize = 0;
751 for (auto &track : tracks()) {
752 progress.stopIfAborted();
753
754 // emplace information
755 trackInfos.emplace_back(
756 &track->inputStream(), track->readChunkOffsets(fileInfo().isForcingFullParse(), diag), track->readChunkSizes(diag));
757
758 // check whether the chunks could be parsed correctly
759 const vector<std::uint64_t> &chunkOffsetTable = get<1>(trackInfos.back());
760 const vector<std::uint64_t> &chunkSizesTable = get<2>(trackInfos.back());
761 if (track->chunkCount() != chunkOffsetTable.size() || track->chunkCount() != chunkSizesTable.size()) {
762 diag.emplace_back(DiagLevel::Critical,
763 "Chunks of track " % numberToString<std::uint64_t, string>(track->id()) + " could not be parsed correctly.",
764 context);
765 }
766
767 // increase total chunk count and size
768 totalChunkCount += track->chunkCount();
769 totalMediaDataSize += std::accumulate(chunkSizesTable.cbegin(), chunkSizesTable.cend(), static_cast<std::uint64_t>(0u));
770 }
771
772 // write media data chunk-by-chunk
773 // -> write header of media data atom
774 Mp4Atom::addHeaderSize(totalMediaDataSize);
775 Mp4Atom::makeHeader(totalMediaDataSize, Mp4AtomIds::MediaData, outputWriter);
776
777 // -> copy chunks
778 CopyHelper<0x2000> copyHelper;
779 std::uint64_t chunkIndexWithinTrack = 0, totalChunksCopied = 0;
780 bool anyChunksCopied;
781 do {
782 progress.stopIfAborted();
783
784 // copy a chunk from each track
785 anyChunksCopied = false;
786 for (size_t trackIndex = 0; trackIndex < trackCount; ++trackIndex) {
787 // get source stream and tables for current track
788 auto &trackInfo = trackInfos[trackIndex];
789 istream &sourceStream = *get<0>(trackInfo);
790 vector<std::uint64_t> &chunkOffsetTable = get<1>(trackInfo);
791 const vector<std::uint64_t> &chunkSizesTable = get<2>(trackInfo);
792
793 // still chunks to be copied (of this track)?
794 if (chunkIndexWithinTrack < chunkOffsetTable.size() && chunkIndexWithinTrack < chunkSizesTable.size()) {
795 // copy chunk, update entry in chunk offset table
796 sourceStream.seekg(static_cast<streamoff>(chunkOffsetTable[chunkIndexWithinTrack]));
797 chunkOffsetTable[chunkIndexWithinTrack] = static_cast<std::uint64_t>(outputStream.tellp());
798 copyHelper.copy(sourceStream, outputStream, chunkSizesTable[chunkIndexWithinTrack]);
799
800 // update counter / status
801 anyChunksCopied = true;
802 ++totalChunksCopied;
803 }
804 }
805
806 // incrase chunk index within track, update progress percentage
807 if (!(++chunkIndexWithinTrack % 10)) {
808 progress.updateStepPercentage(static_cast<std::uint8_t>(totalChunksCopied * 100 / totalChunkCount));
809 }
810
811 } while (anyChunksCopied);
812 }
813
814 } else {
815 // can't just skip next movie sibling
816 for (level0Atom = firstMediaDataAtom; level0Atom; level0Atom = level0Atom->nextSibling()) {
817 level0Atom->parse(diag);
818 switch (level0Atom->id()) {
822 // must void these if they occur "between" the media data
823 outputStream.seekp(4, ios_base::cur);
824 outputWriter.writeUInt32BE(Mp4AtomIds::Free);
825 break;
826 default:
827 outputStream.seekp(static_cast<iostream::off_type>(level0Atom->totalSize()), ios_base::cur);
828 }
829 if (level0Atom == lastAtomToBeWritten) {
830 break;
831 }
832 }
833 }
834 }
835 }
836
837 // reparse what is written so far
838 progress.updateStep("Reparsing output file ...");
839 if (rewriteRequired) {
840 // report new size
841 fileInfo().reportSizeChanged(static_cast<std::uint64_t>(outputStream.tellp()));
842 // "save as path" is now the regular path
843 if (!fileInfo().saveFilePath().empty()) {
844 fileInfo().reportPathChanged(fileInfo().saveFilePath());
845 fileInfo().setSaveFilePath(string());
846 }
847 // the outputStream needs to be reopened to be able to read again
848 outputStream.close();
849 outputStream.open(BasicFileInfo::pathForOpen(fileInfo().path()).data(), ios_base::in | ios_base::out | ios_base::binary);
850 setStream(outputStream);
851 } else {
852 const auto newSize = static_cast<std::uint64_t>(outputStream.tellp());
853 if (newSize < fileInfo().size()) {
854 // file is smaller after the modification -> truncate
855 // -> close stream before truncating
856 outputStream.close();
857 // -> truncate file
858 auto ec = std::error_code();
859 std::filesystem::resize_file(makeNativePath(BasicFileInfo::pathForOpen(fileInfo().path())), newSize, ec);
860 if (!ec) {
861 fileInfo().reportSizeChanged(newSize);
862 } else {
863 diag.emplace_back(DiagLevel::Critical, "Unable to truncate the file: " + ec.message(), context);
864 }
865 // -> reopen the stream again
866 outputStream.open(BasicFileInfo::pathForOpen(fileInfo().path()).data(), ios_base::in | ios_base::out | ios_base::binary);
867 } else {
868 // file is longer after the modification -> just report new size
869 fileInfo().reportSizeChanged(newSize);
870 }
871 }
872
873 reset();
874 try {
875 parseTracks(diag, progress);
876 } catch (const OperationAbortedException &) {
877 throw;
878 } catch (const Failure &) {
879 diag.emplace_back(DiagLevel::Critical, "Unable to reparse the new file.", context);
880 throw;
881 }
882
883 if (rewriteRequired) {
884 // check whether the track count of the new file equals the track count of old file
885 if (trackCount != tracks().size()) {
886 diag.emplace_back(DiagLevel::Critical,
887 argsToString("Unable to update chunk offsets (\"stco\"/\"co64\"-atom): Number of tracks in the output file (", tracks().size(),
888 ") differs from the number of tracks in the original file (", trackCount, ")."),
889 context);
890 throw Failure();
891 }
892
893 // update chunk offset table
894 if (writeChunkByChunk) {
895 progress.updateStep("Updating chunk offset table for each track ...");
896 for (size_t trackIndex = 0; trackIndex != trackCount; ++trackIndex) {
897 const auto &track = tracks()[trackIndex];
898 const auto &chunkOffsetTable = get<1>(trackInfos[trackIndex]);
899 if (track->chunkCount() == chunkOffsetTable.size()) {
900 track->updateChunkOffsets(chunkOffsetTable);
901 } else {
902 diag.emplace_back(DiagLevel::Critical,
903 argsToString("Unable to update chunk offsets of track ", (trackIndex + 1),
904 ": Number of chunks in the output file differs from the number of chunks in the original file."),
905 context);
906 throw Failure();
907 }
908 }
909 } else {
910 progress.updateStep("Updating chunk offset table for each track ...");
911 updateOffsets(origMediaDataOffsets, newMediaDataOffsets, diag, progress);
912 }
913 }
914
915 // prevent deferring final write operations (to catch and handle possible errors here)
916 outputStream.flush();
917
918 // handle errors (which might have been occurred after renaming/creating backup file)
919 } catch (...) {
920 BackupHelper::handleFailureAfterFileModifiedCanonical(fileInfo(), originalPath, backupPath, outputStream, backupStream, diag, context);
921 }
922}
923
936void Mp4Container::updateOffsets(const std::vector<std::int64_t> &oldMdatOffsets, const std::vector<std::int64_t> &newMdatOffsets, Diagnostics &diag,
938{
939 // do NOT invalidate the status here since this method is internally called by internalMakeFile(), just update the status
940 const string context("updating MP4 container chunk offset table");
941 if (!firstElement()) {
942 diag.emplace_back(DiagLevel::Critical, "No MP4 atoms could be found.", context);
943 throw InvalidDataException();
944 }
945 // update "base-data-offset-present" of "tfhd"-atom (NOT tested properly)
946 try {
947 for (Mp4Atom *moofAtom = firstElement()->siblingById(Mp4AtomIds::MovieFragment, diag); moofAtom;
948 moofAtom = moofAtom->siblingById(Mp4AtomIds::MovieFragment, diag)) {
949 moofAtom->parse(diag);
950 try {
951 for (Mp4Atom *trafAtom = moofAtom->childById(Mp4AtomIds::TrackFragment, diag); trafAtom;
952 trafAtom = trafAtom->siblingById(Mp4AtomIds::TrackFragment, diag)) {
953 trafAtom->parse(diag);
954 int tfhdAtomCount = 0;
955 for (Mp4Atom *tfhdAtom = trafAtom->childById(Mp4AtomIds::TrackFragmentHeader, diag); tfhdAtom;
956 tfhdAtom = tfhdAtom->siblingById(Mp4AtomIds::TrackFragmentHeader, diag)) {
957 tfhdAtom->parse(diag);
958 ++tfhdAtomCount;
959 if (tfhdAtom->dataSize() < 8) {
960 diag.emplace_back(DiagLevel::Warning, "tfhd atom is truncated.", context);
961 continue;
962 }
963 stream().seekg(static_cast<iostream::off_type>(tfhdAtom->dataOffset()) + 1);
964 std::uint32_t flags = reader().readUInt24BE();
965 if (!(flags & 1)) {
966 continue;
967 }
968 if (tfhdAtom->dataSize() < 16) {
969 diag.emplace_back(DiagLevel::Warning, "tfhd atom (denoting base-data-offset-present) is truncated.", context);
970 continue;
971 }
972 stream().seekg(4, ios_base::cur); // skip track ID
973 std::uint64_t off = reader().readUInt64BE();
974 for (auto iOld = oldMdatOffsets.cbegin(), iNew = newMdatOffsets.cbegin(), end = oldMdatOffsets.cend(); iOld != end;
975 ++iOld, ++iNew) {
976 if (off < static_cast<std::uint64_t>(*iOld)) {
977 continue;
978 }
979 off += static_cast<std::uint64_t>(*iNew - *iOld);
980 stream().seekp(static_cast<iostream::off_type>(tfhdAtom->dataOffset()) + 8);
981 writer().writeUInt64BE(off);
982 break;
983 }
984 }
985 switch (tfhdAtomCount) {
986 case 0:
987 diag.emplace_back(DiagLevel::Warning, "traf atom doesn't contain mandatory tfhd atom.", context);
988 break;
989 case 1:
990 break;
991 default:
992 diag.emplace_back(
993 DiagLevel::Warning, "traf atom stores multiple tfhd atoms but it should only contain exactly one tfhd atom.", context);
994 }
995 }
996 } catch (const Failure &) {
997 diag.emplace_back(DiagLevel::Critical, "Unable to parse children of top-level atom moof.", context);
998 }
999 }
1000 } catch (const Failure &) {
1001 diag.emplace_back(DiagLevel::Critical, "Unable to parse top-level atom moof.", context);
1002 }
1003 // update each track
1004 for (auto &track : tracks()) {
1005 if (!track->isHeaderValid()) {
1006 try {
1007 track->parseHeader(diag, progress);
1008 } catch (const Failure &) {
1009 diag.emplace_back(DiagLevel::Warning,
1010 "The chunk offsets of track " % track->name() + " couldn't be updated because the track seems to be invalid..", context);
1011 throw;
1012 }
1013 }
1014 if (track->isHeaderValid()) {
1015 try {
1016 track->updateChunkOffsets(oldMdatOffsets, newMdatOffsets);
1017 } catch (const Failure &) {
1018 diag.emplace_back(DiagLevel::Warning, "The chunk offsets of track " % track->name() + " couldn't be updated.", context);
1019 throw;
1020 }
1021 }
1022 }
1023}
1024
1025} // 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.
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, IdType 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.
static void writePadding(std::ostream &outputStream, uint64_t size)
Writes the specified number of zeroes to outputStream.
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:444
Implementation of TagParser::AbstractTrack for the MP4 container.
std::uint32_t chunkCount() const
Returns the number of chunks denoted by the stco/co64 atom.
Definition mp4track.h:254
void setChunkOffsetSize(unsigned int chunkOffsetSize)
Sets the size of a single chunk offset denotation within the stco/co64 atom.
Definition mp4track.h:246
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:564
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:947
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:202
unsigned int chunkOffsetSize() const
Returns the size of a single chunk offset denotation within the stco/co64 atom.
Definition mp4track.h:237
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:53
Contains all classes and functions of the TagInfo library.
Definition aaccodebook.h:10
ElementPosition
Definition settings.h:13