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