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