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