Mp4FileProcessing.cs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164
  1. #define SUPPORT_SPHERICAL_VIDEO
  2. #define SUPPORT_STEREO_VIDEO
  3. //#define SUPPORT_DEBUGLOG
  4. using System.Collections;
  5. using System.Collections.Generic;
  6. using System.Threading;
  7. using UnityEngine;
  8. using System.IO;
  9. using System;
  10. //-----------------------------------------------------------------------------
  11. // Copyright 2012-2022 RenderHeads Ltd. All rights reserved.
  12. //-----------------------------------------------------------------------------
  13. namespace RenderHeads.Media.AVProMovieCapture
  14. {
  15. /*
  16. NOTE: For best flexibility this should be written using classes, but for now since we're only doing specific operations,
  17. we're just reading chunks on demand, writing them out in order making modifications as we go, keeping track of any
  18. increase chunk size increase and patching those chunks. Most chunks are simply byte copied.
  19. This was mainly written this way because we assume either the moov atom is moved before mdat atom, or the moov atom size
  20. changes due to injection and it's before mdat, both cases cannot be done in-place and require writing to a second file.
  21. This method doesn't work well for the case where the moov atom is after mdat, as this could be done in-place. In the future
  22. a hierarchy of chunk classes should be used for processing the moov chunk to make the code more flexible and readable.
  23. NOTE: Using the term Chunks to mean atoms/boxes
  24. Order of operations:
  25. 1) The root chunks are read in
  26. 2) Determined whether moov and mdat chunk positions need swapping, update mdatOffset (only if moov < mdat)
  27. 3) Process the moov chunk is, writing out recursively
  28. 4) For each moov/trak chunk:
  29. a) For each moov/trak/mdia/minf/stbl/stsd/(avc1|hev1|hvc1)
  30. i) inject visual sample descriptor extension atoms if needed
  31. ii) patch parent chunk sizes
  32. iii) update mdatOffset (only if moov < mdat)
  33. b) For each moov/trak/mdia/minf/stbl/co64 (only if mdatOffset > 0)
  34. i) write stub, add chunk to list to patch in step 5
  35. b) For each moov/trak/mdia/minf/stbl/stco (only if mdatOffset > 0)
  36. i) if adjusting largest offset with mdatOffset <= 0xffffffff:
  37. 1) write out stub co64, add chunk to list to patch in step 5
  38. 2) patch parent chunk sizes
  39. ii) if adjusting largest offset with mdatOffset < 0xffffffff:
  40. 1) write stub, add chunk to list to patch in step 5
  41. (NOTE: We only adjust offsets later because there can be multiple tracks, so mdatOffset can still be changing, and also the audio track could come before the vidoe track which would mean moov size grown from visual injection wouldn't need to affect audio offsets)
  42. 5) Go back over any saved stco/co64 chunks
  43. a) adjust offsets by mdatOffset (if needed)
  44. NOTE: It is SLIGHTLY possible that some files using 32-bit offsets (stco atom) can grow and require changing to 64-bit offsets (co64 atom), so we account for this.
  45. NOTE: Because we only have a single video track, after the visual sample descriptor atoms have been injected we're ready to determine whether stco's need to change to co64's, otherwise it would be more complicated
  46. NOTE: It's HIGHLY unlikely that any chunks within moov will get close to > 32bit, so we will ignore the edge case of the atom injection needing to adjust the atom size
  47. TODO: test with file just less than 4GB file, which grows to 4GB so can test the logic for 64-bit atom size values
  48. */
  49. /// <summary>
  50. /// This class is used to rearrange the chunks in an MP4 file to allow 'fast start',
  51. /// and also to inject various chunks related to stereo video mode and 360 video layout.
  52. /// </summary>
  53. // Reference: https://wiki.multimedia.cx/index.php/QuickTime_container
  54. // Reference: https://github.com/danielgtaylor/qtfaststart/blob/master/qtfaststart/processor.py
  55. // Reference: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFPreface/qtffPreface.html
  56. // Reference: https://github.com/google/spatial-media/blob/master/docs/spherical-video-v2-rfc.md
  57. // Reference: https://xhelmboyx.tripod.com/formats/mp4-layout.txt
  58. public class MP4FileProcessing
  59. {
  60. public struct Options
  61. {
  62. public bool applyFastStart;
  63. #if SUPPORT_STEREO_VIDEO
  64. public bool applyStereoMode;
  65. public StereoPacking stereoMode;
  66. #endif
  67. #if SUPPORT_SPHERICAL_VIDEO
  68. public bool applySphericalVideoLayout;
  69. public SphericalVideoLayout sphericalVideoLayout;
  70. #endif
  71. public bool applyMoveCaptureFile;
  72. public string finalCaptureFilePath;
  73. public bool HasOptions()
  74. {
  75. return (RequiresProcessing() || applyFastStart);
  76. }
  77. public bool RequiresProcessing()
  78. {
  79. return (applyFastStart || applyStereoMode || applySphericalVideoLayout);
  80. }
  81. public void ResetOptions()
  82. {
  83. applyFastStart = false;
  84. applyStereoMode = false;
  85. applySphericalVideoLayout = false;
  86. applyMoveCaptureFile = false;
  87. finalCaptureFilePath = null;
  88. }
  89. }
  90. private const int ChunkHeaderSize = 8;
  91. private const int ExtendedChunkHeaderSize = 16;
  92. private const int CopyBufferSize = 4096 * 16;
  93. //private readonly static uint Atom_ftyp = ChunkId("ftyp"); // file type
  94. private readonly static uint Atom_moov = ChunkId("moov"); // movie header
  95. private readonly static uint Atom_mdat = ChunkId("mdat"); // movie data
  96. private readonly static uint Atom_cmov = ChunkId("cmov"); // compressed movie data
  97. private readonly static uint Atom_trak = ChunkId("trak"); // track header
  98. private readonly static uint Atom_mdia = ChunkId("mdia"); // media
  99. private readonly static uint Atom_hdlr = ChunkId("hdlr"); // handler reference
  100. private readonly static uint Atom_minf = ChunkId("minf"); // media information
  101. private readonly static uint Atom_stbl = ChunkId("stbl"); // sample table
  102. private readonly static uint Atom_stco = ChunkId("stco"); // sample table chunk offsets (32-bit)
  103. private readonly static uint Atom_co64 = ChunkId("co64"); // sample table chunk offsets (64-bit)
  104. #if SUPPORT_STEREO_VIDEO || SUPPORT_SPHERICAL_VIDEO
  105. private readonly static uint Atom_stsd = ChunkId("stsd"); // sample table sample description
  106. private readonly static uint Atom_avc1 = ChunkId("avc1"); // video sample entry for H.264
  107. private readonly static uint Atom_hev1 = ChunkId("hev1"); // video sample entry for H.265/HEVC
  108. private readonly static uint Atom_hvc1 = ChunkId("hvc1"); // video sample entry for H.265/HEVC
  109. #endif
  110. #if SUPPORT_STEREO_VIDEO
  111. private readonly static uint Atom_st3d = ChunkId("st3d"); // stereoscopic 3D video
  112. #endif
  113. #if SUPPORT_SPHERICAL_VIDEO
  114. private readonly static uint Atom_uuid = ChunkId("uuid"); // unique id
  115. private readonly static uint Atom_sv3d = ChunkId("sv3d"); // spherical video
  116. private readonly static uint Atom_svhd = ChunkId("svhd"); // spherical video header
  117. private readonly static uint Atom_proj = ChunkId("proj"); // projection
  118. private readonly static uint Atom_prhd = ChunkId("prhd"); // projection header
  119. private readonly static uint Atom_equi = ChunkId("equi"); // equirectangular projection
  120. #endif
  121. private class Chunk
  122. {
  123. public uint id;
  124. public long size; // includes the size of the chunk header, so next chunk is at size+offset
  125. public long offset; // offset to the start of the chunk header in the source file
  126. public long headerSize; // Size of the header (either 8 or 12)
  127. public long writeOffset; // offset to the start of the chunk header in the dest file (currently only used for replacing stco with co64)
  128. };
  129. private BinaryReader _reader;
  130. private Stream _writeFile;
  131. private Options _options;
  132. private bool _requires64BitOffsets;
  133. private List<Chunk> _offsetChunks = new List<Chunk>(); // stco / co64 chunks
  134. private List<Chunk> _offsetUpgradeChunks = new List<Chunk>(); // Chunks that were stco that changed to co64
  135. public static ManualResetEvent ProcessFileAsync(string filePath, bool keepBackup, Options options)
  136. {
  137. if (!File.Exists(filePath))
  138. {
  139. Debug.LogError("File not found: " + filePath);
  140. return null;
  141. }
  142. ManualResetEvent syncEvent = new ManualResetEvent(false);
  143. Thread thread = new Thread(
  144. () =>
  145. {
  146. try
  147. {
  148. ProcessFile(filePath, keepBackup, options);
  149. }
  150. catch (System.Exception e)
  151. {
  152. Debug.LogException(e);
  153. }
  154. syncEvent.Set();
  155. }
  156. );
  157. thread.Start();
  158. return syncEvent;
  159. }
  160. public static bool ProcessFile(string filePath, bool keepBackup, Options options)
  161. {
  162. if (!File.Exists(filePath))
  163. {
  164. Debug.LogError("File not found: " + filePath);
  165. return false;
  166. }
  167. bool result = true;
  168. if(options.RequiresProcessing())
  169. {
  170. string tempPath = filePath + "-" + System.Guid.NewGuid() + ".temp";
  171. result = ProcessFile(filePath, tempPath, options);
  172. if (result)
  173. {
  174. string backupPath = filePath + "-" + System.Guid.NewGuid() + ".backup";
  175. File.Move(filePath, backupPath);
  176. File.Move(tempPath, filePath);
  177. if (!keepBackup)
  178. {
  179. File.Delete(backupPath);
  180. }
  181. }
  182. if (File.Exists(tempPath))
  183. {
  184. File.Delete(tempPath);
  185. }
  186. }
  187. if(result)
  188. {
  189. // Move the captured video somewhere else?
  190. if(options.applyMoveCaptureFile && options.finalCaptureFilePath != null)
  191. {
  192. File.Move(filePath, options.finalCaptureFilePath);
  193. }
  194. }
  195. return result;
  196. }
  197. public static bool ProcessFile(string srcPath, string dstPath, Options options)
  198. {
  199. if (!File.Exists(srcPath))
  200. {
  201. Debug.LogError("File not found: " + srcPath);
  202. return false;
  203. }
  204. using (Stream srcStream = new FileStream(srcPath, FileMode.Open, FileAccess.Read, FileShare.Read))
  205. {
  206. using (Stream dstStream = new FileStream(dstPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
  207. {
  208. MP4FileProcessing mp4 = new MP4FileProcessing(options);
  209. bool result = mp4.Process(srcStream, dstStream);
  210. mp4.Close();
  211. return result;
  212. }
  213. }
  214. }
  215. public MP4FileProcessing(Options options)
  216. {
  217. _options = options;
  218. }
  219. public bool Process(Stream srcStream, Stream dstStream)
  220. {
  221. Close();
  222. _reader = new BinaryReader(srcStream);
  223. List<Chunk> rootChunks = ReadChildChunks(null);
  224. Chunk chunk_moov = GetFirstChunkOfType(Atom_moov, rootChunks);
  225. Chunk chunk_mdat = GetFirstChunkOfType(Atom_mdat, rootChunks);
  226. if (chunk_moov == null || chunk_mdat == null)
  227. {
  228. Debug.LogError("can't find moov or mdat chunks");
  229. Close();
  230. return false;
  231. }
  232. if (ChunkContainsChildChunkWithId(chunk_moov, Atom_cmov))
  233. {
  234. Debug.LogError("moov chunk is compressed - unsupported");
  235. Close();
  236. return false;
  237. }
  238. uint mdatOffset = 0;
  239. if (_options.applyFastStart && chunk_moov.offset > chunk_mdat.offset)
  240. {
  241. // Swap moov and mdat order
  242. int index_moov = rootChunks.IndexOf(chunk_moov);
  243. int index_mdat = rootChunks.IndexOf(chunk_mdat);
  244. rootChunks[index_mdat] = chunk_moov;
  245. rootChunks[index_moov] = chunk_mdat;
  246. mdatOffset = (uint)chunk_moov.size;
  247. }
  248. bool is_moov_before_mdat = (rootChunks.IndexOf(chunk_moov) < rootChunks.IndexOf(chunk_mdat));
  249. _writeFile = dstStream;
  250. // Make an approximate worst case calculation of whether 64-bit offsets are required do to
  251. // possible moov size growth. In this case all stco chunks are rewritten as co64 chunks.
  252. // Our injected chunks are tiny, so just add 1024 as a safe approximation.
  253. _requires64BitOffsets = ((srcStream.Length + 1024) > 0xffffffff);
  254. // Copy and inject chunks
  255. foreach (Chunk chunk in rootChunks)
  256. {
  257. if (chunk != chunk_moov)
  258. {
  259. WriteChunk(chunk);
  260. }
  261. else
  262. {
  263. uint sizeIncrease = WriteChunkRecursive_moov(chunk_moov);
  264. if (is_moov_before_mdat)
  265. {
  266. // Only if moov is before mdat does moov size increase affect offsets
  267. mdatOffset += sizeIncrease;
  268. }
  269. }
  270. }
  271. DebugLog("total offset: " + mdatOffset);
  272. // Write and adjust offsets
  273. {
  274. foreach (Chunk chunk in _offsetChunks)
  275. {
  276. _writeFile.Position = chunk.writeOffset;
  277. if (chunk.id == Atom_stco)
  278. {
  279. WriteChunk_stco(chunk, mdatOffset);
  280. }
  281. else if (chunk.id == Atom_co64)
  282. {
  283. WriteChunk_co64(chunk, mdatOffset);
  284. }
  285. }
  286. foreach (Chunk chunk in _offsetUpgradeChunks)
  287. {
  288. _writeFile.Position = chunk.writeOffset;
  289. DebugLog("write offset: " + chunk.writeOffset);
  290. WriteChunk_co64_from_stco(chunk, mdatOffset);
  291. }
  292. }
  293. Close();
  294. Debug.Log("[AVProMovieCapture] File processing complete");
  295. return true;
  296. }
  297. public void Close()
  298. {
  299. _offsetChunks = new List<Chunk>();
  300. _offsetUpgradeChunks = new List<Chunk>();
  301. _writeFile = null;
  302. if (_reader != null)
  303. {
  304. _reader.Close();
  305. _reader = null;
  306. }
  307. }
  308. private static Chunk GetFirstChunkOfType(uint id, List<Chunk> chunks)
  309. {
  310. Chunk result = null;
  311. foreach (Chunk chunk in chunks)
  312. {
  313. if (chunk.id == id)
  314. {
  315. result = chunk;
  316. break;
  317. }
  318. }
  319. return result;
  320. }
  321. private List<Chunk> ReadChildChunks(Chunk parentChunk)
  322. {
  323. // Offset to start of parent chunk
  324. {
  325. long fileOffset = 0;
  326. if (parentChunk != null)
  327. {
  328. fileOffset = parentChunk.offset + parentChunk.headerSize;
  329. }
  330. _reader.BaseStream.Seek(fileOffset, SeekOrigin.Begin);
  331. }
  332. long chunkEnd = _reader.BaseStream.Length;
  333. if (parentChunk != null)
  334. {
  335. chunkEnd = parentChunk.offset + parentChunk.size;
  336. }
  337. return ReadChildChunks(chunkEnd);
  338. }
  339. private List<Chunk> ReadChildChunks(long chunkEndPosition)
  340. {
  341. List<Chunk> result = new List<Chunk>();
  342. if (_reader.BaseStream.Position < chunkEndPosition)
  343. {
  344. Chunk chunk = ReadChunkHeader();
  345. while (chunk != null && _reader.BaseStream.Position < chunkEndPosition)
  346. {
  347. result.Add(chunk);
  348. _reader.BaseStream.Seek(chunk.offset + chunk.size, SeekOrigin.Begin);
  349. chunk = ReadChunkHeader();
  350. }
  351. }
  352. return result;
  353. }
  354. private Chunk ReadChunkHeader()
  355. {
  356. Chunk chunk = null;
  357. // Make sure the minimum amount of data is available
  358. if ((_reader.BaseStream.Length - _reader.BaseStream.Position) >= ChunkHeaderSize)
  359. {
  360. chunk = new Chunk();
  361. chunk.offset = _reader.BaseStream.Position;
  362. chunk.headerSize = ChunkHeaderSize;
  363. chunk.size = ReadUInt32();
  364. chunk.id = _reader.ReadUInt32();
  365. if (chunk.size == 1)
  366. {
  367. // NOTE: '1' indicates we need to read the extended 64-bit size
  368. chunk.size = (long)ReadUInt64();
  369. chunk.headerSize = ExtendedChunkHeaderSize;
  370. }
  371. if (chunk.size == 0)
  372. {
  373. // NOTE: '0' indicates that this is the last chunk, so the size is the remainder of the file
  374. chunk.size = _reader.BaseStream.Length - chunk.offset;
  375. }
  376. }
  377. return chunk;
  378. }
  379. private bool ChunkContainsChildChunkWithId(Chunk chunk, uint id)
  380. {
  381. bool result = false;
  382. long endChunkPos = chunk.size + chunk.offset;
  383. _reader.BaseStream.Seek(chunk.offset, SeekOrigin.Begin);
  384. Chunk childChunk = ReadChunkHeader();
  385. while (childChunk != null && _reader.BaseStream.Position < endChunkPos)
  386. {
  387. if (childChunk.id == id)
  388. {
  389. result = true;
  390. break;
  391. }
  392. _reader.BaseStream.Seek(childChunk.offset + childChunk.size, SeekOrigin.Begin);
  393. childChunk = ReadChunkHeader();
  394. }
  395. return result;
  396. }
  397. private static string ChunkDesc(Chunk chunk)
  398. {
  399. string size = chunk.size + ((chunk.size > UInt32.MaxValue) ? "^" : "");
  400. string offset = chunk.offset + ((chunk.offset > UInt32.MaxValue) ? "^" : "");
  401. string end = chunk.size + chunk.offset + (((chunk.size + chunk.offset) > UInt32.MaxValue) ? "^" : "");
  402. string woffset = chunk.writeOffset + ((chunk.writeOffset > UInt32.MaxValue) ? "^" : "");
  403. return "<color=green><b>" + ChunkIdToString(chunk.id) + "</b></color> size:" + size + " offset:" + offset + " end:" + end + " write:" + woffset;
  404. }
  405. private void WriteChunk(Chunk chunk)
  406. {
  407. DebugLog("WriteChunk " + ChunkDesc(chunk));
  408. //if (chunk.id == Atom_mdat) return;
  409. _reader.BaseStream.Seek(chunk.offset, SeekOrigin.Begin);
  410. CopyBytes(chunk.size);
  411. }
  412. private void CopyChunkHeader(Chunk chunk)
  413. {
  414. DebugLog("CopyChunkHeader " + ChunkDesc(chunk));
  415. _reader.BaseStream.Seek(chunk.offset, SeekOrigin.Begin);
  416. CopyBytes(chunk.headerSize);
  417. }
  418. private void InjectChunkHeader(Chunk chunk)
  419. {
  420. DebugLog("InjectChunkHeader " + ChunkDesc(chunk));
  421. if (chunk.size < UInt32.MaxValue)
  422. {
  423. WriteUInt32((uint)chunk.size);
  424. }
  425. else
  426. {
  427. WriteUInt32(1);
  428. }
  429. WriteChunkId(chunk.id);
  430. if (chunk.size >= UInt32.MaxValue)
  431. {
  432. WriteUInt64((ulong)chunk.size);
  433. }
  434. }
  435. private void CopyBytes(long numBytes)
  436. {
  437. DebugLog(string.Format("Copying {0} bytes from {1} to {2}", numBytes, _reader.BaseStream.Position, _writeFile.Position));
  438. byte[] buffer = new byte[CopyBufferSize];
  439. long remaining = numBytes;
  440. Stream readStream = _reader.BaseStream;
  441. while (remaining > 0)
  442. {
  443. int byteCount = buffer.Length;
  444. if (remaining < buffer.Length)
  445. {
  446. byteCount = (int)remaining;
  447. }
  448. readStream.Read(buffer, 0, byteCount);
  449. _writeFile.Write(buffer, 0, byteCount);
  450. remaining -= byteCount;
  451. }
  452. }
  453. private void WriteZeros(long numBytes)
  454. {
  455. DebugLog(string.Format("Writing zero {0} bytes to {1}", numBytes, _writeFile.Position));
  456. byte[] buffer = new byte[CopyBufferSize];
  457. long remaining = numBytes;
  458. while (remaining > 0)
  459. {
  460. int byteCount = buffer.Length;
  461. if (remaining < buffer.Length)
  462. {
  463. byteCount = (int)remaining;
  464. }
  465. _writeFile.Write(buffer, 0, byteCount);
  466. remaining -= byteCount;
  467. }
  468. }
  469. private uint WriteChunkRecursive_moov(Chunk parentChunk)
  470. {
  471. uint childChunkSizeIncrease = 0;
  472. long chunkWritePosition = _writeFile.Position;
  473. CopyChunkHeader(parentChunk);
  474. DebugLog("write chunk " + ChunkIdToString(parentChunk.id) + " " + parentChunk.size);
  475. List<Chunk> children = ReadChildChunks(parentChunk);
  476. _reader.BaseStream.Seek(parentChunk.offset + parentChunk.headerSize, SeekOrigin.Begin);
  477. foreach (Chunk chunk in children)
  478. {
  479. if (chunk.id == Atom_stco)
  480. {
  481. DebugLog("stco " + ChunkDesc(chunk));
  482. // Just write a placeholder as it's updated later
  483. // May also convert the stco into a co64
  484. chunk.writeOffset = _writeFile.Position;
  485. if (!_requires64BitOffsets)
  486. {
  487. WriteZeros(chunk.size);
  488. _offsetChunks.Add(chunk);
  489. }
  490. else
  491. {
  492. childChunkSizeIncrease += InjectChunkStub_co64_from_stco(chunk);
  493. DebugLog("Increase InjectChunkStub_co64_from_stco " + childChunkSizeIncrease);
  494. _offsetUpgradeChunks.Add(chunk);
  495. }
  496. }
  497. else if (chunk.id == Atom_co64)
  498. {
  499. // Just write a placeholder as it's updated later
  500. chunk.writeOffset = _writeFile.Position;
  501. WriteZeros(chunk.size);
  502. _offsetChunks.Add(chunk);
  503. }
  504. #if SUPPORT_STEREO_VIDEO || SUPPORT_SPHERICAL_VIDEO
  505. else if (chunk.id == Atom_stsd)
  506. {
  507. childChunkSizeIncrease += WriteChunk_stsd(chunk);
  508. DebugLog("Increase WriteChunk_stsd " + childChunkSizeIncrease);
  509. }
  510. #endif
  511. // Hierarchy of atoms we're interested in:
  512. // [moov > trak > mdia > minf > stbl] >> [stco | co64]
  513. // [moov > trak > mdia > minf > stbl >> stsd] >> [avc1 | hev1 | hvc1]
  514. else if (chunk.id == Atom_trak ||
  515. chunk.id == Atom_mdia ||
  516. chunk.id == Atom_minf ||
  517. chunk.id == Atom_stbl)
  518. {
  519. // Recurse these chunks searching for interesting chunks
  520. childChunkSizeIncrease += WriteChunkRecursive_moov(chunk);
  521. DebugLog("Increase WriteChunkRecursive_moov " + childChunkSizeIncrease);
  522. }
  523. else
  524. {
  525. // We don't care about this chunk so just copy it
  526. WriteChunk(chunk);
  527. }
  528. }
  529. if (parentChunk.id == Atom_trak && _options.applySphericalVideoLayout && _options.sphericalVideoLayout == SphericalVideoLayout.Equirectangular360)
  530. {
  531. if (IsVideoTrack(parentChunk))
  532. {
  533. childChunkSizeIncrease += InjectChunk_uuid_GoogleSphericalVideoV1();
  534. DebugLog("Increase InjectChunk_uuid_GoogleSphericalVideoV1 " + childChunkSizeIncrease);
  535. }
  536. }
  537. if (childChunkSizeIncrease > 0)
  538. {
  539. DebugLog("> " + childChunkSizeIncrease);
  540. parentChunk.size += childChunkSizeIncrease;
  541. OverwriteChunkSize(parentChunk, chunkWritePosition);
  542. }
  543. return childChunkSizeIncrease;
  544. }
  545. private bool IsVideoTrack(Chunk trackChunk)
  546. {
  547. bool result = false;
  548. List<Chunk> chunks = ReadChildChunks(trackChunk);
  549. Chunk chunk_mdia = GetFirstChunkOfType(Atom_mdia, chunks);
  550. if (chunk_mdia != null)
  551. {
  552. chunks = ReadChildChunks(chunk_mdia);
  553. Chunk chunk_hdlr = GetFirstChunkOfType(Atom_hdlr, chunks);
  554. if (chunk_hdlr != null)
  555. {
  556. _reader.BaseStream.Position = chunk_hdlr.offset + chunk_hdlr.headerSize + 8;
  557. uint componentSubtype = ReadUInt32();
  558. result = (0x76696465 == componentSubtype);
  559. }
  560. }
  561. return result;
  562. }
  563. private void WriteChunk_stco(Chunk chunk, uint mdatByteOffset)
  564. {
  565. DebugLog("WriteChunk_stco");
  566. CopyChunkHeader(chunk);
  567. // Version & Flags
  568. CopyBytes(4);
  569. uint chunkOffsetCount = ReadUInt32();
  570. WriteUInt32(chunkOffsetCount);
  571. // Apply offsets
  572. for (int i = 0; i < chunkOffsetCount; i++)
  573. {
  574. long offset = ReadUInt32();
  575. offset += mdatByteOffset;
  576. WriteUInt32((uint)offset);
  577. }
  578. }
  579. private void WriteChunk_co64_from_stco(Chunk chunk, uint mdatByteOffset)
  580. {
  581. DebugLog("WriteChunk_co64_from_stco " + mdatByteOffset);
  582. InjectChunkHeader(chunk);
  583. _reader.BaseStream.Position = chunk.offset + chunk.headerSize;
  584. // Version & Flags
  585. CopyBytes(4);
  586. uint chunkOffsetCount = ReadUInt32();
  587. DebugLog("offsets: " + chunkOffsetCount);
  588. WriteUInt32(chunkOffsetCount);
  589. // Apply offsets
  590. for (int i = 0; i < chunkOffsetCount; i++)
  591. {
  592. ulong offset = ReadUInt32();
  593. offset += mdatByteOffset;
  594. WriteUInt64(offset);
  595. }
  596. }
  597. private void WriteChunk_co64(Chunk chunk, uint mdatByteOffset)
  598. {
  599. DebugLog("WriteChunk_co64");
  600. CopyChunkHeader(chunk);
  601. // Version & Flags
  602. CopyBytes(4);
  603. uint chunkOffsetCount = ReadUInt32();
  604. WriteUInt32(chunkOffsetCount);
  605. // Apply offsets
  606. for (int i = 0; i < chunkOffsetCount; i++)
  607. {
  608. ulong offset = ReadUInt64();
  609. offset += mdatByteOffset;
  610. WriteUInt64(offset);
  611. }
  612. }
  613. private uint InjectChunkStub_co64_from_stco(Chunk chunk)
  614. {
  615. chunk.id = Atom_co64;
  616. chunk.writeOffset = _writeFile.Position;
  617. CopyChunkHeader(chunk);
  618. // Version & Flags
  619. CopyBytes(4);
  620. // Stub count
  621. uint chunkOffsetCount = ReadUInt32();
  622. WriteUInt32(chunkOffsetCount);
  623. // Stub offsets
  624. long offsetsSize = chunkOffsetCount * sizeof(UInt64);
  625. WriteZeros(offsetsSize);
  626. long sizeIncrease = (offsetsSize / 2);
  627. // Calculate new size
  628. chunk.size += sizeIncrease;
  629. OverwriteChunkSize(chunk, chunk.writeOffset);
  630. return (uint)(sizeIncrease);
  631. }
  632. #if SUPPORT_STEREO_VIDEO || SUPPORT_SPHERICAL_VIDEO
  633. private uint WriteChunk_stsd(Chunk chunk)
  634. {
  635. uint chunkSizeIncrease = 0;
  636. long chunkWritePosition = _writeFile.Position;
  637. CopyChunkHeader(chunk);
  638. // Version & Flags
  639. CopyBytes(4);
  640. uint sampleDescCount = ReadUInt32();
  641. WriteUInt32(sampleDescCount);
  642. for (int i = 0; i < sampleDescCount; i++)
  643. {
  644. Chunk sampleDescriptor = ReadChunkHeader();
  645. DebugLog("header: " + ChunkIdToString(sampleDescriptor.id) + " " + sampleDescriptor.size);
  646. if (sampleDescriptor.id == Atom_avc1 ||
  647. sampleDescriptor.id == Atom_hev1 ||
  648. sampleDescriptor.id == Atom_hvc1)
  649. {
  650. #if true
  651. _reader.BaseStream.Seek(4 + 6 + 2, SeekOrigin.Current);
  652. ushort version = ReadUInt16();
  653. DebugLog("version: " + version);
  654. if (version == 0)
  655. {
  656. uint sampleDescriptorSizeIncrease = 0;
  657. long sampleDescriptorWritePosition = _writeFile.Position;
  658. CopyChunkHeader(sampleDescriptor);
  659. CopyBytes(78);
  660. long chunkEndPosition = sampleDescriptor.offset + sampleDescriptor.size;
  661. List<Chunk> sampleDescriptorExtensions = ReadChildChunks(chunkEndPosition);
  662. DebugLog("sampleDescriptorExtensions: " + sampleDescriptorExtensions.Count);
  663. bool hasWrittenST3D = false;
  664. bool hasWrittenSV3D = false;
  665. for (int j = 0; j < sampleDescriptorExtensions.Count; j++)
  666. {
  667. DebugLog("sampleDescriptorExtensions: " + ChunkIdToString(sampleDescriptorExtensions[j].id) + " > " + sampleDescriptorExtensions[j].size);
  668. if (sampleDescriptorExtensions[i].id == Atom_st3d)
  669. {
  670. /*
  671. // Modify existing chunk
  672. if (_options.applyStereoMode)
  673. {
  674. InjectChunk_st3d(Convert(_options.stereoMode));
  675. }*/
  676. Debug.LogWarning("st3d atom already exists");
  677. hasWrittenST3D = true;
  678. }
  679. else if (sampleDescriptorExtensions[i].id == Atom_sv3d)
  680. {
  681. Debug.LogWarning("sv3d atom already exists");
  682. hasWrittenSV3D = true;
  683. }
  684. else
  685. {
  686. WriteChunk(sampleDescriptorExtensions[j]);
  687. }
  688. }
  689. #if SUPPORT_STEREO_VIDEO
  690. if (!hasWrittenST3D && _options.applyStereoMode)
  691. {
  692. sampleDescriptorSizeIncrease += InjectChunk_st3d(Convert(_options.stereoMode));
  693. hasWrittenST3D = true;
  694. }
  695. #endif
  696. #if SUPPORT_SPHERICAL_VIDEO
  697. if (!hasWrittenSV3D && _options.applySphericalVideoLayout)
  698. {
  699. sampleDescriptorSizeIncrease += InjectChunk_sv3d(_options.sphericalVideoLayout);
  700. hasWrittenSV3D = true;
  701. }
  702. #endif
  703. if (sampleDescriptorSizeIncrease > 0)
  704. {
  705. sampleDescriptor.size += sampleDescriptorSizeIncrease;
  706. OverwriteChunkSize(sampleDescriptor, sampleDescriptorWritePosition);
  707. chunkSizeIncrease += sampleDescriptorSizeIncrease;
  708. DebugLog("Increasing size by " + sampleDescriptorSizeIncrease);
  709. }
  710. }
  711. else
  712. #endif
  713. {
  714. WriteChunk(sampleDescriptor);
  715. }
  716. }
  717. else
  718. {
  719. // We don't care about this chunk so just copy it
  720. WriteChunk(sampleDescriptor);
  721. }
  722. }
  723. DebugLog(chunk.offset + chunk.size + " left " + _reader.BaseStream.Position);
  724. if (chunkSizeIncrease > 0)
  725. {
  726. chunk.size += chunkSizeIncrease;
  727. OverwriteChunkSize(chunk, chunkWritePosition);
  728. }
  729. return chunkSizeIncrease;
  730. }
  731. #endif
  732. #if SUPPORT_STEREO_VIDEO
  733. internal enum StereoMode_st3d
  734. {
  735. Monoscopic = 0,
  736. Stereoscopic_TopBottom = 1,
  737. Stereoscopic_LeftRight = 2,
  738. Stereoscopic_Custom = 3,
  739. Stereoscopic_RightLeft = 4,
  740. }
  741. private static StereoMode_st3d Convert(StereoPacking mode)
  742. {
  743. StereoMode_st3d result = StereoMode_st3d.Monoscopic;
  744. switch (mode)
  745. {
  746. case StereoPacking.None:
  747. break;
  748. case StereoPacking.LeftRight:
  749. result = StereoMode_st3d.Stereoscopic_LeftRight;
  750. break;
  751. case StereoPacking.TopBottom:
  752. result = StereoMode_st3d.Stereoscopic_TopBottom;
  753. break;
  754. }
  755. return result;
  756. }
  757. private uint InjectChunk_st3d(StereoMode_st3d stereoMode)
  758. {
  759. DebugLog("InjectChunk_st3d");
  760. uint chunkSize = ChunkHeaderSize + 4 + sizeof(byte);
  761. WriteUInt32(chunkSize);
  762. WriteChunkId(Atom_st3d);
  763. // Version & Flags
  764. WriteUInt32(0);
  765. _writeFile.WriteByte((byte)stereoMode);
  766. return chunkSize;
  767. }
  768. #endif
  769. #if SUPPORT_SPHERICAL_VIDEO
  770. // sv3d/svhd
  771. // sv3d/proj/prhd
  772. // sv3d/proj/equi
  773. private uint InjectChunk_sv3d(SphericalVideoLayout layout)
  774. {
  775. Chunk chunk = new Chunk();
  776. chunk.offset = _writeFile.Position;
  777. chunk.id = Atom_sv3d;
  778. chunk.size = ChunkHeaderSize;
  779. InjectChunkHeader(chunk);
  780. chunk.size += InjectChunk_svhd("AVProMovieCapture");
  781. chunk.size += InjectChunk_proj(layout);
  782. OverwriteChunkSize(chunk, chunk.offset);
  783. return (uint)chunk.size;
  784. }
  785. private uint InjectChunk_uuid_GoogleSphericalVideoV1()
  786. {
  787. Chunk chunk = new Chunk();
  788. chunk.offset = _writeFile.Position;
  789. chunk.id = Atom_uuid;
  790. chunk.size = ChunkHeaderSize;
  791. InjectChunkHeader(chunk);
  792. WriteUInt32(0xffcc8263);
  793. WriteUInt32(0xf8554a93);
  794. WriteUInt32(0x8814587a);
  795. WriteUInt32(0x02521fdd);
  796. chunk.size += 4 * sizeof(UInt32);
  797. string xml = "<rdf:SphericalVideo xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:GSpherical=\"http://ns.google.com/videos/1.0/spherical/\"> <GSpherical:Spherical>true</GSpherical:Spherical> <GSpherical:Stitched>true</GSpherical:Stitched> <GSpherical:ProjectionType>equirectangular</GSpherical:ProjectionType> <GSpherical:StitchingSoftware>AVPro Movie Capture</GSpherical:StitchingSoftware> <GSpherical:StereoMode>{StereoMode}</GSpherical:StereoMode></rdf:SphericalVideo>";
  798. if (_options.applyStereoMode)
  799. {
  800. switch (_options.stereoMode)
  801. {
  802. case StereoPacking.None:
  803. xml = xml.Replace("{StereoMode}", "mono");
  804. break;
  805. case StereoPacking.LeftRight:
  806. xml = xml.Replace("{StereoMode}", "left-right");
  807. break;
  808. case StereoPacking.TopBottom:
  809. xml = xml.Replace("{StereoMode}", "top-bottom");
  810. break;
  811. }
  812. }
  813. else
  814. {
  815. xml = xml.Replace("{StereoMode}", "mono");
  816. }
  817. byte[] bytes = System.Text.Encoding.UTF8.GetBytes(xml);
  818. _writeFile.Write(bytes, 0, bytes.Length);
  819. chunk.size += bytes.Length;
  820. OverwriteChunkSize(chunk, chunk.offset);
  821. return (uint)chunk.size;
  822. }
  823. private uint InjectChunk_svhd(string toolname)
  824. {
  825. Chunk chunk = new Chunk();
  826. chunk.offset = _writeFile.Position;
  827. chunk.id = Atom_svhd;
  828. chunk.size = ChunkHeaderSize;
  829. InjectChunkHeader(chunk);
  830. // Version & Flags
  831. WriteUInt32(0);
  832. chunk.size += 4;
  833. foreach (char c in toolname)
  834. {
  835. _writeFile.WriteByte((byte)c);
  836. }
  837. _writeFile.WriteByte(0); // Null terminate
  838. chunk.size += (sizeof(byte) * (toolname.Length + 1));
  839. OverwriteChunkSize(chunk, chunk.offset);
  840. return (uint)chunk.size;
  841. }
  842. private uint InjectChunk_proj(SphericalVideoLayout layout)
  843. {
  844. Chunk chunk = new Chunk();
  845. chunk.offset = _writeFile.Position;
  846. chunk.id = Atom_proj;
  847. chunk.size = ChunkHeaderSize;
  848. InjectChunkHeader(chunk);
  849. chunk.size += InjectChunk_prhd();
  850. if (layout == SphericalVideoLayout.Equirectangular360)
  851. {
  852. chunk.size += InjectChunk_equi();
  853. }
  854. // TODO: add cubemap32 support here
  855. OverwriteChunkSize(chunk, chunk.offset);
  856. return (uint)chunk.size;
  857. }
  858. private uint InjectChunk_prhd()
  859. {
  860. Chunk chunk = new Chunk();
  861. chunk.offset = _writeFile.Position;
  862. chunk.id = Atom_prhd;
  863. chunk.size = ChunkHeaderSize;
  864. InjectChunkHeader(chunk);
  865. WriteUInt32(0); // Version & Flags
  866. WriteUInt32(0); // Yaw
  867. WriteUInt32(0); // Pitch
  868. WriteUInt32(0); // Roll
  869. chunk.size += sizeof(UInt32) * 4;
  870. OverwriteChunkSize(chunk, chunk.offset);
  871. return (uint)chunk.size;
  872. }
  873. private uint InjectChunk_equi()
  874. {
  875. Chunk chunk = new Chunk();
  876. chunk.offset = _writeFile.Position;
  877. chunk.id = Atom_equi;
  878. chunk.size = ChunkHeaderSize;
  879. InjectChunkHeader(chunk);
  880. WriteUInt32(0); // Version & Flags
  881. WriteUInt32(0); // Bounds top
  882. WriteUInt32(0); // Bounds bottom
  883. WriteUInt32(0); // Bounds left
  884. WriteUInt32(0); // Bounds right
  885. chunk.size += sizeof(UInt32) * 5;
  886. OverwriteChunkSize(chunk, chunk.offset);
  887. return (uint)chunk.size;
  888. }
  889. #endif
  890. private void OverwriteChunkSize(Chunk chunk, long writePosition)
  891. {
  892. long restoreWritePosition = _writeFile.Position;
  893. _writeFile.Position = writePosition;
  894. DebugLog("patch size " + ChunkIdToString(chunk.id) + " " + chunk.size + "@ " + _writeFile.Position);
  895. // TODO: Fix bug here if original size was < 32bit but the new size is more
  896. // This is HIGHLY unlikely though, as moov chunks should be nowhere near that large
  897. // and we aren't adjusting the size of mdat chunks
  898. WriteUInt32((uint)chunk.size);
  899. _writeFile.Seek(restoreWritePosition, SeekOrigin.Begin);
  900. }
  901. private UInt16 ReadUInt16()
  902. {
  903. byte[] data = _reader.ReadBytes(2);
  904. Array.Reverse(data);
  905. return BitConverter.ToUInt16(data, 0);
  906. }
  907. private UInt32 ReadUInt32()
  908. {
  909. byte[] data = _reader.ReadBytes(4);
  910. Array.Reverse(data);
  911. return BitConverter.ToUInt32(data, 0);
  912. }
  913. private UInt64 ReadUInt64()
  914. {
  915. byte[] data = _reader.ReadBytes(8);
  916. Array.Reverse(data);
  917. return BitConverter.ToUInt64(data, 0);
  918. }
  919. private void WriteUInt16(UInt16 value)
  920. {
  921. byte[] data = BitConverter.GetBytes(value);
  922. Array.Reverse(data);
  923. _writeFile.Write(data, 0, data.Length);
  924. }
  925. private void WriteChunkId(uint id)
  926. {
  927. WriteUInt32(id, false);
  928. }
  929. private void WriteUInt32(UInt32 value, bool isBigEndian = true)
  930. {
  931. byte[] data = BitConverter.GetBytes(value);
  932. if (isBigEndian)
  933. {
  934. Array.Reverse(data);
  935. }
  936. _writeFile.Write(data, 0, data.Length);
  937. }
  938. private void WriteUInt64(UInt64 value)
  939. {
  940. byte[] data = BitConverter.GetBytes(value);
  941. Array.Reverse(data);
  942. _writeFile.Write(data, 0, data.Length);
  943. }
  944. private static string ChunkIdToString(UInt32 id)
  945. {
  946. char a = (char)((id >> 0) & 255);
  947. char b = (char)((id >> 8) & 255);
  948. char c = (char)((id >> 16) & 255);
  949. char d = (char)((id >> 24) & 255);
  950. return string.Format("{0}{1}{2}{3}", a, b, c, d);
  951. }
  952. private static uint ChunkId(string id)
  953. {
  954. uint a = id[3];
  955. uint b = id[2];
  956. uint c = id[1];
  957. uint d = id[0];
  958. return (a << 24) | (b << 16) | (c << 8) | d;
  959. }
  960. [System.Diagnostics.Conditional("SUPPORT_DEBUGLOG")]
  961. private static void DebugLog(string message)
  962. {
  963. Debug.Log(message);
  964. }
  965. #if false && UNITY_EDITOR
  966. [UnityEditor.MenuItem("RenderHeads/Test MP4 Processing")]
  967. static void Test_Mp4Processing()
  968. {
  969. string path = "d:/video/video.mov";
  970. DateTime time = DateTime.Now;
  971. Options options = new Options();
  972. options.applyFastStart = true;
  973. options.applyStereoMode = true;
  974. options.stereoMode = StereoPacking.TopBottom;
  975. options.applySphericalVideoLayout = true;
  976. options.sphericalVideoLayout = SphericalVideoLayout.Equirectangular360;
  977. if (MP4FileProcessing.ProcessFile(path, true, options))
  978. {
  979. DateTime time2 = DateTime.Now;
  980. Debug.Log("success!");
  981. Debug.Log("Took: " + (time2 - time).TotalMilliseconds + "ms");
  982. }
  983. else
  984. {
  985. Debug.LogWarning("Did not modify file");
  986. }
  987. }
  988. #endif
  989. }
  990. }