123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164 |
- #define SUPPORT_SPHERICAL_VIDEO
- #define SUPPORT_STEREO_VIDEO
- //#define SUPPORT_DEBUGLOG
- using System.Collections;
- using System.Collections.Generic;
- using System.Threading;
- using UnityEngine;
- using System.IO;
- using System;
- //-----------------------------------------------------------------------------
- // Copyright 2012-2022 RenderHeads Ltd. All rights reserved.
- //-----------------------------------------------------------------------------
- namespace RenderHeads.Media.AVProMovieCapture
- {
- /*
- NOTE: For best flexibility this should be written using classes, but for now since we're only doing specific operations,
- we're just reading chunks on demand, writing them out in order making modifications as we go, keeping track of any
- increase chunk size increase and patching those chunks. Most chunks are simply byte copied.
-
- This was mainly written this way because we assume either the moov atom is moved before mdat atom, or the moov atom size
- changes due to injection and it's before mdat, both cases cannot be done in-place and require writing to a second file.
- 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
- a hierarchy of chunk classes should be used for processing the moov chunk to make the code more flexible and readable.
- NOTE: Using the term Chunks to mean atoms/boxes
- Order of operations:
- 1) The root chunks are read in
- 2) Determined whether moov and mdat chunk positions need swapping, update mdatOffset (only if moov < mdat)
- 3) Process the moov chunk is, writing out recursively
- 4) For each moov/trak chunk:
- a) For each moov/trak/mdia/minf/stbl/stsd/(avc1|hev1|hvc1)
- i) inject visual sample descriptor extension atoms if needed
- ii) patch parent chunk sizes
- iii) update mdatOffset (only if moov < mdat)
- b) For each moov/trak/mdia/minf/stbl/co64 (only if mdatOffset > 0)
- i) write stub, add chunk to list to patch in step 5
- b) For each moov/trak/mdia/minf/stbl/stco (only if mdatOffset > 0)
- i) if adjusting largest offset with mdatOffset <= 0xffffffff:
- 1) write out stub co64, add chunk to list to patch in step 5
- 2) patch parent chunk sizes
- ii) if adjusting largest offset with mdatOffset < 0xffffffff:
- 1) write stub, add chunk to list to patch in step 5
- (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)
- 5) Go back over any saved stco/co64 chunks
- a) adjust offsets by mdatOffset (if needed)
- 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.
- 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
- 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
- TODO: test with file just less than 4GB file, which grows to 4GB so can test the logic for 64-bit atom size values
- */
- /// <summary>
- /// This class is used to rearrange the chunks in an MP4 file to allow 'fast start',
- /// and also to inject various chunks related to stereo video mode and 360 video layout.
- /// </summary>
- // Reference: https://wiki.multimedia.cx/index.php/QuickTime_container
- // Reference: https://github.com/danielgtaylor/qtfaststart/blob/master/qtfaststart/processor.py
- // Reference: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFPreface/qtffPreface.html
- // Reference: https://github.com/google/spatial-media/blob/master/docs/spherical-video-v2-rfc.md
- // Reference: https://xhelmboyx.tripod.com/formats/mp4-layout.txt
- public class MP4FileProcessing
- {
- public struct Options
- {
- public bool applyFastStart;
- #if SUPPORT_STEREO_VIDEO
- public bool applyStereoMode;
- public StereoPacking stereoMode;
- #endif
- #if SUPPORT_SPHERICAL_VIDEO
- public bool applySphericalVideoLayout;
- public SphericalVideoLayout sphericalVideoLayout;
- #endif
- public bool applyMoveCaptureFile;
- public string finalCaptureFilePath;
- public bool HasOptions()
- {
- return (RequiresProcessing() || applyFastStart);
- }
- public bool RequiresProcessing()
- {
- return (applyFastStart || applyStereoMode || applySphericalVideoLayout);
- }
- public void ResetOptions()
- {
- applyFastStart = false;
- applyStereoMode = false;
- applySphericalVideoLayout = false;
- applyMoveCaptureFile = false;
- finalCaptureFilePath = null;
- }
- }
- private const int ChunkHeaderSize = 8;
- private const int ExtendedChunkHeaderSize = 16;
- private const int CopyBufferSize = 4096 * 16;
- //private readonly static uint Atom_ftyp = ChunkId("ftyp"); // file type
- private readonly static uint Atom_moov = ChunkId("moov"); // movie header
- private readonly static uint Atom_mdat = ChunkId("mdat"); // movie data
- private readonly static uint Atom_cmov = ChunkId("cmov"); // compressed movie data
- private readonly static uint Atom_trak = ChunkId("trak"); // track header
- private readonly static uint Atom_mdia = ChunkId("mdia"); // media
- private readonly static uint Atom_hdlr = ChunkId("hdlr"); // handler reference
- private readonly static uint Atom_minf = ChunkId("minf"); // media information
- private readonly static uint Atom_stbl = ChunkId("stbl"); // sample table
- private readonly static uint Atom_stco = ChunkId("stco"); // sample table chunk offsets (32-bit)
- private readonly static uint Atom_co64 = ChunkId("co64"); // sample table chunk offsets (64-bit)
- #if SUPPORT_STEREO_VIDEO || SUPPORT_SPHERICAL_VIDEO
- private readonly static uint Atom_stsd = ChunkId("stsd"); // sample table sample description
- private readonly static uint Atom_avc1 = ChunkId("avc1"); // video sample entry for H.264
- private readonly static uint Atom_hev1 = ChunkId("hev1"); // video sample entry for H.265/HEVC
- private readonly static uint Atom_hvc1 = ChunkId("hvc1"); // video sample entry for H.265/HEVC
- #endif
- #if SUPPORT_STEREO_VIDEO
- private readonly static uint Atom_st3d = ChunkId("st3d"); // stereoscopic 3D video
- #endif
- #if SUPPORT_SPHERICAL_VIDEO
- private readonly static uint Atom_uuid = ChunkId("uuid"); // unique id
- private readonly static uint Atom_sv3d = ChunkId("sv3d"); // spherical video
- private readonly static uint Atom_svhd = ChunkId("svhd"); // spherical video header
- private readonly static uint Atom_proj = ChunkId("proj"); // projection
- private readonly static uint Atom_prhd = ChunkId("prhd"); // projection header
- private readonly static uint Atom_equi = ChunkId("equi"); // equirectangular projection
- #endif
- private class Chunk
- {
- public uint id;
- public long size; // includes the size of the chunk header, so next chunk is at size+offset
- public long offset; // offset to the start of the chunk header in the source file
- public long headerSize; // Size of the header (either 8 or 12)
- public long writeOffset; // offset to the start of the chunk header in the dest file (currently only used for replacing stco with co64)
- };
- private BinaryReader _reader;
- private Stream _writeFile;
- private Options _options;
- private bool _requires64BitOffsets;
- private List<Chunk> _offsetChunks = new List<Chunk>(); // stco / co64 chunks
- private List<Chunk> _offsetUpgradeChunks = new List<Chunk>(); // Chunks that were stco that changed to co64
- public static ManualResetEvent ProcessFileAsync(string filePath, bool keepBackup, Options options)
- {
- if (!File.Exists(filePath))
- {
- Debug.LogError("File not found: " + filePath);
- return null;
- }
- ManualResetEvent syncEvent = new ManualResetEvent(false);
- Thread thread = new Thread(
- () =>
- {
- try
- {
- ProcessFile(filePath, keepBackup, options);
- }
- catch (System.Exception e)
- {
- Debug.LogException(e);
- }
- syncEvent.Set();
- }
- );
- thread.Start();
- return syncEvent;
- }
- public static bool ProcessFile(string filePath, bool keepBackup, Options options)
- {
- if (!File.Exists(filePath))
- {
- Debug.LogError("File not found: " + filePath);
- return false;
- }
- bool result = true;
- if(options.RequiresProcessing())
- {
- string tempPath = filePath + "-" + System.Guid.NewGuid() + ".temp";
-
- result = ProcessFile(filePath, tempPath, options);
- if (result)
- {
- string backupPath = filePath + "-" + System.Guid.NewGuid() + ".backup";
- File.Move(filePath, backupPath);
- File.Move(tempPath, filePath);
- if (!keepBackup)
- {
- File.Delete(backupPath);
- }
- }
- if (File.Exists(tempPath))
- {
- File.Delete(tempPath);
- }
- }
- if(result)
- {
- // Move the captured video somewhere else?
- if(options.applyMoveCaptureFile && options.finalCaptureFilePath != null)
- {
- File.Move(filePath, options.finalCaptureFilePath);
- }
- }
- return result;
- }
- public static bool ProcessFile(string srcPath, string dstPath, Options options)
- {
- if (!File.Exists(srcPath))
- {
- Debug.LogError("File not found: " + srcPath);
- return false;
- }
-
- using (Stream srcStream = new FileStream(srcPath, FileMode.Open, FileAccess.Read, FileShare.Read))
- {
- using (Stream dstStream = new FileStream(dstPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
- {
- MP4FileProcessing mp4 = new MP4FileProcessing(options);
- bool result = mp4.Process(srcStream, dstStream);
- mp4.Close();
- return result;
- }
- }
- }
- public MP4FileProcessing(Options options)
- {
- _options = options;
- }
- public bool Process(Stream srcStream, Stream dstStream)
- {
- Close();
- _reader = new BinaryReader(srcStream);
- List<Chunk> rootChunks = ReadChildChunks(null);
- Chunk chunk_moov = GetFirstChunkOfType(Atom_moov, rootChunks);
- Chunk chunk_mdat = GetFirstChunkOfType(Atom_mdat, rootChunks);
- if (chunk_moov == null || chunk_mdat == null)
- {
- Debug.LogError("can't find moov or mdat chunks");
- Close();
- return false;
- }
- if (ChunkContainsChildChunkWithId(chunk_moov, Atom_cmov))
- {
- Debug.LogError("moov chunk is compressed - unsupported");
- Close();
- return false;
- }
- uint mdatOffset = 0;
- if (_options.applyFastStart && chunk_moov.offset > chunk_mdat.offset)
- {
- // Swap moov and mdat order
- int index_moov = rootChunks.IndexOf(chunk_moov);
- int index_mdat = rootChunks.IndexOf(chunk_mdat);
- rootChunks[index_mdat] = chunk_moov;
- rootChunks[index_moov] = chunk_mdat;
- mdatOffset = (uint)chunk_moov.size;
- }
- bool is_moov_before_mdat = (rootChunks.IndexOf(chunk_moov) < rootChunks.IndexOf(chunk_mdat));
- _writeFile = dstStream;
- // Make an approximate worst case calculation of whether 64-bit offsets are required do to
- // possible moov size growth. In this case all stco chunks are rewritten as co64 chunks.
- // Our injected chunks are tiny, so just add 1024 as a safe approximation.
- _requires64BitOffsets = ((srcStream.Length + 1024) > 0xffffffff);
- // Copy and inject chunks
- foreach (Chunk chunk in rootChunks)
- {
- if (chunk != chunk_moov)
- {
- WriteChunk(chunk);
- }
- else
- {
- uint sizeIncrease = WriteChunkRecursive_moov(chunk_moov);
- if (is_moov_before_mdat)
- {
- // Only if moov is before mdat does moov size increase affect offsets
- mdatOffset += sizeIncrease;
- }
- }
- }
- DebugLog("total offset: " + mdatOffset);
- // Write and adjust offsets
- {
- foreach (Chunk chunk in _offsetChunks)
- {
- _writeFile.Position = chunk.writeOffset;
- if (chunk.id == Atom_stco)
- {
- WriteChunk_stco(chunk, mdatOffset);
- }
- else if (chunk.id == Atom_co64)
- {
- WriteChunk_co64(chunk, mdatOffset);
- }
- }
- foreach (Chunk chunk in _offsetUpgradeChunks)
- {
- _writeFile.Position = chunk.writeOffset;
- DebugLog("write offset: " + chunk.writeOffset);
- WriteChunk_co64_from_stco(chunk, mdatOffset);
- }
- }
- Close();
- Debug.Log("[AVProMovieCapture] File processing complete");
- return true;
- }
- public void Close()
- {
- _offsetChunks = new List<Chunk>();
- _offsetUpgradeChunks = new List<Chunk>();
- _writeFile = null;
- if (_reader != null)
- {
- _reader.Close();
- _reader = null;
- }
- }
- private static Chunk GetFirstChunkOfType(uint id, List<Chunk> chunks)
- {
- Chunk result = null;
- foreach (Chunk chunk in chunks)
- {
- if (chunk.id == id)
- {
- result = chunk;
- break;
- }
- }
- return result;
- }
- private List<Chunk> ReadChildChunks(Chunk parentChunk)
- {
- // Offset to start of parent chunk
- {
- long fileOffset = 0;
- if (parentChunk != null)
- {
- fileOffset = parentChunk.offset + parentChunk.headerSize;
- }
- _reader.BaseStream.Seek(fileOffset, SeekOrigin.Begin);
- }
- long chunkEnd = _reader.BaseStream.Length;
- if (parentChunk != null)
- {
- chunkEnd = parentChunk.offset + parentChunk.size;
- }
- return ReadChildChunks(chunkEnd);
- }
- private List<Chunk> ReadChildChunks(long chunkEndPosition)
- {
- List<Chunk> result = new List<Chunk>();
- if (_reader.BaseStream.Position < chunkEndPosition)
- {
- Chunk chunk = ReadChunkHeader();
- while (chunk != null && _reader.BaseStream.Position < chunkEndPosition)
- {
- result.Add(chunk);
- _reader.BaseStream.Seek(chunk.offset + chunk.size, SeekOrigin.Begin);
- chunk = ReadChunkHeader();
- }
- }
- return result;
- }
- private Chunk ReadChunkHeader()
- {
- Chunk chunk = null;
- // Make sure the minimum amount of data is available
- if ((_reader.BaseStream.Length - _reader.BaseStream.Position) >= ChunkHeaderSize)
- {
- chunk = new Chunk();
- chunk.offset = _reader.BaseStream.Position;
- chunk.headerSize = ChunkHeaderSize;
- chunk.size = ReadUInt32();
- chunk.id = _reader.ReadUInt32();
-
- if (chunk.size == 1)
- {
- // NOTE: '1' indicates we need to read the extended 64-bit size
- chunk.size = (long)ReadUInt64();
- chunk.headerSize = ExtendedChunkHeaderSize;
- }
- if (chunk.size == 0)
- {
- // NOTE: '0' indicates that this is the last chunk, so the size is the remainder of the file
- chunk.size = _reader.BaseStream.Length - chunk.offset;
- }
- }
- return chunk;
- }
- private bool ChunkContainsChildChunkWithId(Chunk chunk, uint id)
- {
- bool result = false;
- long endChunkPos = chunk.size + chunk.offset;
- _reader.BaseStream.Seek(chunk.offset, SeekOrigin.Begin);
- Chunk childChunk = ReadChunkHeader();
- while (childChunk != null && _reader.BaseStream.Position < endChunkPos)
- {
- if (childChunk.id == id)
- {
- result = true;
- break;
- }
- _reader.BaseStream.Seek(childChunk.offset + childChunk.size, SeekOrigin.Begin);
- childChunk = ReadChunkHeader();
- }
- return result;
- }
- private static string ChunkDesc(Chunk chunk)
- {
- string size = chunk.size + ((chunk.size > UInt32.MaxValue) ? "^" : "");
- string offset = chunk.offset + ((chunk.offset > UInt32.MaxValue) ? "^" : "");
- string end = chunk.size + chunk.offset + (((chunk.size + chunk.offset) > UInt32.MaxValue) ? "^" : "");
- string woffset = chunk.writeOffset + ((chunk.writeOffset > UInt32.MaxValue) ? "^" : "");
- return "<color=green><b>" + ChunkIdToString(chunk.id) + "</b></color> size:" + size + " offset:" + offset + " end:" + end + " write:" + woffset;
- }
- private void WriteChunk(Chunk chunk)
- {
- DebugLog("WriteChunk " + ChunkDesc(chunk));
- //if (chunk.id == Atom_mdat) return;
- _reader.BaseStream.Seek(chunk.offset, SeekOrigin.Begin);
- CopyBytes(chunk.size);
- }
- private void CopyChunkHeader(Chunk chunk)
- {
- DebugLog("CopyChunkHeader " + ChunkDesc(chunk));
- _reader.BaseStream.Seek(chunk.offset, SeekOrigin.Begin);
- CopyBytes(chunk.headerSize);
- }
- private void InjectChunkHeader(Chunk chunk)
- {
- DebugLog("InjectChunkHeader " + ChunkDesc(chunk));
- if (chunk.size < UInt32.MaxValue)
- {
- WriteUInt32((uint)chunk.size);
- }
- else
- {
- WriteUInt32(1);
- }
- WriteChunkId(chunk.id);
- if (chunk.size >= UInt32.MaxValue)
- {
- WriteUInt64((ulong)chunk.size);
- }
- }
- private void CopyBytes(long numBytes)
- {
- DebugLog(string.Format("Copying {0} bytes from {1} to {2}", numBytes, _reader.BaseStream.Position, _writeFile.Position));
- byte[] buffer = new byte[CopyBufferSize];
- long remaining = numBytes;
- Stream readStream = _reader.BaseStream;
- while (remaining > 0)
- {
- int byteCount = buffer.Length;
- if (remaining < buffer.Length)
- {
- byteCount = (int)remaining;
- }
- readStream.Read(buffer, 0, byteCount);
- _writeFile.Write(buffer, 0, byteCount);
- remaining -= byteCount;
- }
- }
- private void WriteZeros(long numBytes)
- {
- DebugLog(string.Format("Writing zero {0} bytes to {1}", numBytes, _writeFile.Position));
- byte[] buffer = new byte[CopyBufferSize];
- long remaining = numBytes;
- while (remaining > 0)
- {
- int byteCount = buffer.Length;
- if (remaining < buffer.Length)
- {
- byteCount = (int)remaining;
- }
- _writeFile.Write(buffer, 0, byteCount);
- remaining -= byteCount;
- }
- }
- private uint WriteChunkRecursive_moov(Chunk parentChunk)
- {
- uint childChunkSizeIncrease = 0;
- long chunkWritePosition = _writeFile.Position;
- CopyChunkHeader(parentChunk);
- DebugLog("write chunk " + ChunkIdToString(parentChunk.id) + " " + parentChunk.size);
- List<Chunk> children = ReadChildChunks(parentChunk);
- _reader.BaseStream.Seek(parentChunk.offset + parentChunk.headerSize, SeekOrigin.Begin);
- foreach (Chunk chunk in children)
- {
- if (chunk.id == Atom_stco)
- {
- DebugLog("stco " + ChunkDesc(chunk));
- // Just write a placeholder as it's updated later
- // May also convert the stco into a co64
- chunk.writeOffset = _writeFile.Position;
- if (!_requires64BitOffsets)
- {
- WriteZeros(chunk.size);
- _offsetChunks.Add(chunk);
- }
- else
- {
- childChunkSizeIncrease += InjectChunkStub_co64_from_stco(chunk);
- DebugLog("Increase InjectChunkStub_co64_from_stco " + childChunkSizeIncrease);
- _offsetUpgradeChunks.Add(chunk);
- }
- }
- else if (chunk.id == Atom_co64)
- {
- // Just write a placeholder as it's updated later
- chunk.writeOffset = _writeFile.Position;
- WriteZeros(chunk.size);
- _offsetChunks.Add(chunk);
- }
- #if SUPPORT_STEREO_VIDEO || SUPPORT_SPHERICAL_VIDEO
- else if (chunk.id == Atom_stsd)
- {
- childChunkSizeIncrease += WriteChunk_stsd(chunk);
- DebugLog("Increase WriteChunk_stsd " + childChunkSizeIncrease);
- }
- #endif
- // Hierarchy of atoms we're interested in:
- // [moov > trak > mdia > minf > stbl] >> [stco | co64]
- // [moov > trak > mdia > minf > stbl >> stsd] >> [avc1 | hev1 | hvc1]
- else if (chunk.id == Atom_trak ||
- chunk.id == Atom_mdia ||
- chunk.id == Atom_minf ||
- chunk.id == Atom_stbl)
- {
- // Recurse these chunks searching for interesting chunks
- childChunkSizeIncrease += WriteChunkRecursive_moov(chunk);
- DebugLog("Increase WriteChunkRecursive_moov " + childChunkSizeIncrease);
- }
- else
- {
- // We don't care about this chunk so just copy it
- WriteChunk(chunk);
- }
- }
- if (parentChunk.id == Atom_trak && _options.applySphericalVideoLayout && _options.sphericalVideoLayout == SphericalVideoLayout.Equirectangular360)
- {
- if (IsVideoTrack(parentChunk))
- {
- childChunkSizeIncrease += InjectChunk_uuid_GoogleSphericalVideoV1();
- DebugLog("Increase InjectChunk_uuid_GoogleSphericalVideoV1 " + childChunkSizeIncrease);
- }
- }
- if (childChunkSizeIncrease > 0)
- {
- DebugLog("> " + childChunkSizeIncrease);
- parentChunk.size += childChunkSizeIncrease;
- OverwriteChunkSize(parentChunk, chunkWritePosition);
- }
- return childChunkSizeIncrease;
- }
- private bool IsVideoTrack(Chunk trackChunk)
- {
- bool result = false;
- List<Chunk> chunks = ReadChildChunks(trackChunk);
- Chunk chunk_mdia = GetFirstChunkOfType(Atom_mdia, chunks);
- if (chunk_mdia != null)
- {
- chunks = ReadChildChunks(chunk_mdia);
- Chunk chunk_hdlr = GetFirstChunkOfType(Atom_hdlr, chunks);
- if (chunk_hdlr != null)
- {
- _reader.BaseStream.Position = chunk_hdlr.offset + chunk_hdlr.headerSize + 8;
- uint componentSubtype = ReadUInt32();
- result = (0x76696465 == componentSubtype);
- }
- }
- return result;
- }
- private void WriteChunk_stco(Chunk chunk, uint mdatByteOffset)
- {
- DebugLog("WriteChunk_stco");
- CopyChunkHeader(chunk);
- // Version & Flags
- CopyBytes(4);
- uint chunkOffsetCount = ReadUInt32();
- WriteUInt32(chunkOffsetCount);
- // Apply offsets
- for (int i = 0; i < chunkOffsetCount; i++)
- {
- long offset = ReadUInt32();
- offset += mdatByteOffset;
- WriteUInt32((uint)offset);
- }
- }
- private void WriteChunk_co64_from_stco(Chunk chunk, uint mdatByteOffset)
- {
- DebugLog("WriteChunk_co64_from_stco " + mdatByteOffset);
- InjectChunkHeader(chunk);
- _reader.BaseStream.Position = chunk.offset + chunk.headerSize;
- // Version & Flags
- CopyBytes(4);
- uint chunkOffsetCount = ReadUInt32();
- DebugLog("offsets: " + chunkOffsetCount);
- WriteUInt32(chunkOffsetCount);
- // Apply offsets
- for (int i = 0; i < chunkOffsetCount; i++)
- {
- ulong offset = ReadUInt32();
- offset += mdatByteOffset;
- WriteUInt64(offset);
- }
- }
- private void WriteChunk_co64(Chunk chunk, uint mdatByteOffset)
- {
- DebugLog("WriteChunk_co64");
- CopyChunkHeader(chunk);
- // Version & Flags
- CopyBytes(4);
- uint chunkOffsetCount = ReadUInt32();
- WriteUInt32(chunkOffsetCount);
- // Apply offsets
- for (int i = 0; i < chunkOffsetCount; i++)
- {
- ulong offset = ReadUInt64();
- offset += mdatByteOffset;
- WriteUInt64(offset);
- }
- }
- private uint InjectChunkStub_co64_from_stco(Chunk chunk)
- {
- chunk.id = Atom_co64;
- chunk.writeOffset = _writeFile.Position;
- CopyChunkHeader(chunk);
- // Version & Flags
- CopyBytes(4);
-
- // Stub count
- uint chunkOffsetCount = ReadUInt32();
- WriteUInt32(chunkOffsetCount);
- // Stub offsets
- long offsetsSize = chunkOffsetCount * sizeof(UInt64);
- WriteZeros(offsetsSize);
- long sizeIncrease = (offsetsSize / 2);
- // Calculate new size
- chunk.size += sizeIncrease;
- OverwriteChunkSize(chunk, chunk.writeOffset);
- return (uint)(sizeIncrease);
- }
- #if SUPPORT_STEREO_VIDEO || SUPPORT_SPHERICAL_VIDEO
- private uint WriteChunk_stsd(Chunk chunk)
- {
- uint chunkSizeIncrease = 0;
- long chunkWritePosition = _writeFile.Position;
- CopyChunkHeader(chunk);
- // Version & Flags
- CopyBytes(4);
- uint sampleDescCount = ReadUInt32();
- WriteUInt32(sampleDescCount);
- for (int i = 0; i < sampleDescCount; i++)
- {
- Chunk sampleDescriptor = ReadChunkHeader();
- DebugLog("header: " + ChunkIdToString(sampleDescriptor.id) + " " + sampleDescriptor.size);
- if (sampleDescriptor.id == Atom_avc1 ||
- sampleDescriptor.id == Atom_hev1 ||
- sampleDescriptor.id == Atom_hvc1)
- {
- #if true
- _reader.BaseStream.Seek(4 + 6 + 2, SeekOrigin.Current);
- ushort version = ReadUInt16();
- DebugLog("version: " + version);
- if (version == 0)
- {
- uint sampleDescriptorSizeIncrease = 0;
- long sampleDescriptorWritePosition = _writeFile.Position;
- CopyChunkHeader(sampleDescriptor);
- CopyBytes(78);
- long chunkEndPosition = sampleDescriptor.offset + sampleDescriptor.size;
- List<Chunk> sampleDescriptorExtensions = ReadChildChunks(chunkEndPosition);
- DebugLog("sampleDescriptorExtensions: " + sampleDescriptorExtensions.Count);
- bool hasWrittenST3D = false;
- bool hasWrittenSV3D = false;
- for (int j = 0; j < sampleDescriptorExtensions.Count; j++)
- {
- DebugLog("sampleDescriptorExtensions: " + ChunkIdToString(sampleDescriptorExtensions[j].id) + " > " + sampleDescriptorExtensions[j].size);
- if (sampleDescriptorExtensions[i].id == Atom_st3d)
- {
- /*
- // Modify existing chunk
- if (_options.applyStereoMode)
- {
- InjectChunk_st3d(Convert(_options.stereoMode));
- }*/
- Debug.LogWarning("st3d atom already exists");
- hasWrittenST3D = true;
- }
- else if (sampleDescriptorExtensions[i].id == Atom_sv3d)
- {
- Debug.LogWarning("sv3d atom already exists");
- hasWrittenSV3D = true;
- }
- else
- {
- WriteChunk(sampleDescriptorExtensions[j]);
- }
- }
- #if SUPPORT_STEREO_VIDEO
- if (!hasWrittenST3D && _options.applyStereoMode)
- {
- sampleDescriptorSizeIncrease += InjectChunk_st3d(Convert(_options.stereoMode));
-
- hasWrittenST3D = true;
- }
- #endif
- #if SUPPORT_SPHERICAL_VIDEO
- if (!hasWrittenSV3D && _options.applySphericalVideoLayout)
- {
- sampleDescriptorSizeIncrease += InjectChunk_sv3d(_options.sphericalVideoLayout);
- hasWrittenSV3D = true;
- }
- #endif
- if (sampleDescriptorSizeIncrease > 0)
- {
- sampleDescriptor.size += sampleDescriptorSizeIncrease;
- OverwriteChunkSize(sampleDescriptor, sampleDescriptorWritePosition);
- chunkSizeIncrease += sampleDescriptorSizeIncrease;
- DebugLog("Increasing size by " + sampleDescriptorSizeIncrease);
- }
- }
- else
- #endif
- {
- WriteChunk(sampleDescriptor);
- }
- }
- else
- {
- // We don't care about this chunk so just copy it
- WriteChunk(sampleDescriptor);
- }
- }
- DebugLog(chunk.offset + chunk.size + " left " + _reader.BaseStream.Position);
- if (chunkSizeIncrease > 0)
- {
- chunk.size += chunkSizeIncrease;
- OverwriteChunkSize(chunk, chunkWritePosition);
- }
- return chunkSizeIncrease;
- }
- #endif
- #if SUPPORT_STEREO_VIDEO
- internal enum StereoMode_st3d
- {
- Monoscopic = 0,
- Stereoscopic_TopBottom = 1,
- Stereoscopic_LeftRight = 2,
- Stereoscopic_Custom = 3,
- Stereoscopic_RightLeft = 4,
- }
- private static StereoMode_st3d Convert(StereoPacking mode)
- {
- StereoMode_st3d result = StereoMode_st3d.Monoscopic;
- switch (mode)
- {
- case StereoPacking.None:
- break;
- case StereoPacking.LeftRight:
- result = StereoMode_st3d.Stereoscopic_LeftRight;
- break;
- case StereoPacking.TopBottom:
- result = StereoMode_st3d.Stereoscopic_TopBottom;
- break;
- }
- return result;
- }
- private uint InjectChunk_st3d(StereoMode_st3d stereoMode)
- {
- DebugLog("InjectChunk_st3d");
- uint chunkSize = ChunkHeaderSize + 4 + sizeof(byte);
- WriteUInt32(chunkSize);
- WriteChunkId(Atom_st3d);
- // Version & Flags
- WriteUInt32(0);
- _writeFile.WriteByte((byte)stereoMode);
- return chunkSize;
- }
- #endif
- #if SUPPORT_SPHERICAL_VIDEO
- // sv3d/svhd
- // sv3d/proj/prhd
- // sv3d/proj/equi
- private uint InjectChunk_sv3d(SphericalVideoLayout layout)
- {
- Chunk chunk = new Chunk();
- chunk.offset = _writeFile.Position;
- chunk.id = Atom_sv3d;
- chunk.size = ChunkHeaderSize;
- InjectChunkHeader(chunk);
- chunk.size += InjectChunk_svhd("AVProMovieCapture");
- chunk.size += InjectChunk_proj(layout);
- OverwriteChunkSize(chunk, chunk.offset);
- return (uint)chunk.size;
- }
- private uint InjectChunk_uuid_GoogleSphericalVideoV1()
- {
- Chunk chunk = new Chunk();
- chunk.offset = _writeFile.Position;
- chunk.id = Atom_uuid;
- chunk.size = ChunkHeaderSize;
- InjectChunkHeader(chunk);
- WriteUInt32(0xffcc8263);
- WriteUInt32(0xf8554a93);
- WriteUInt32(0x8814587a);
- WriteUInt32(0x02521fdd);
- chunk.size += 4 * sizeof(UInt32);
- 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>";
- if (_options.applyStereoMode)
- {
- switch (_options.stereoMode)
- {
- case StereoPacking.None:
- xml = xml.Replace("{StereoMode}", "mono");
- break;
- case StereoPacking.LeftRight:
- xml = xml.Replace("{StereoMode}", "left-right");
- break;
- case StereoPacking.TopBottom:
- xml = xml.Replace("{StereoMode}", "top-bottom");
- break;
- }
- }
- else
- {
- xml = xml.Replace("{StereoMode}", "mono");
- }
- byte[] bytes = System.Text.Encoding.UTF8.GetBytes(xml);
- _writeFile.Write(bytes, 0, bytes.Length);
- chunk.size += bytes.Length;
- OverwriteChunkSize(chunk, chunk.offset);
- return (uint)chunk.size;
- }
- private uint InjectChunk_svhd(string toolname)
- {
- Chunk chunk = new Chunk();
- chunk.offset = _writeFile.Position;
- chunk.id = Atom_svhd;
- chunk.size = ChunkHeaderSize;
- InjectChunkHeader(chunk);
- // Version & Flags
- WriteUInt32(0);
- chunk.size += 4;
- foreach (char c in toolname)
- {
- _writeFile.WriteByte((byte)c);
- }
- _writeFile.WriteByte(0); // Null terminate
- chunk.size += (sizeof(byte) * (toolname.Length + 1));
- OverwriteChunkSize(chunk, chunk.offset);
- return (uint)chunk.size;
- }
- private uint InjectChunk_proj(SphericalVideoLayout layout)
- {
- Chunk chunk = new Chunk();
- chunk.offset = _writeFile.Position;
- chunk.id = Atom_proj;
- chunk.size = ChunkHeaderSize;
- InjectChunkHeader(chunk);
- chunk.size += InjectChunk_prhd();
- if (layout == SphericalVideoLayout.Equirectangular360)
- {
- chunk.size += InjectChunk_equi();
- }
- // TODO: add cubemap32 support here
- OverwriteChunkSize(chunk, chunk.offset);
- return (uint)chunk.size;
- }
- private uint InjectChunk_prhd()
- {
- Chunk chunk = new Chunk();
- chunk.offset = _writeFile.Position;
- chunk.id = Atom_prhd;
- chunk.size = ChunkHeaderSize;
- InjectChunkHeader(chunk);
- WriteUInt32(0); // Version & Flags
- WriteUInt32(0); // Yaw
- WriteUInt32(0); // Pitch
- WriteUInt32(0); // Roll
- chunk.size += sizeof(UInt32) * 4;
- OverwriteChunkSize(chunk, chunk.offset);
- return (uint)chunk.size;
- }
- private uint InjectChunk_equi()
- {
- Chunk chunk = new Chunk();
- chunk.offset = _writeFile.Position;
- chunk.id = Atom_equi;
- chunk.size = ChunkHeaderSize;
- InjectChunkHeader(chunk);
-
- WriteUInt32(0); // Version & Flags
- WriteUInt32(0); // Bounds top
- WriteUInt32(0); // Bounds bottom
- WriteUInt32(0); // Bounds left
- WriteUInt32(0); // Bounds right
- chunk.size += sizeof(UInt32) * 5;
- OverwriteChunkSize(chunk, chunk.offset);
- return (uint)chunk.size;
- }
- #endif
- private void OverwriteChunkSize(Chunk chunk, long writePosition)
- {
- long restoreWritePosition = _writeFile.Position;
- _writeFile.Position = writePosition;
- DebugLog("patch size " + ChunkIdToString(chunk.id) + " " + chunk.size + "@ " + _writeFile.Position);
- // TODO: Fix bug here if original size was < 32bit but the new size is more
- // This is HIGHLY unlikely though, as moov chunks should be nowhere near that large
- // and we aren't adjusting the size of mdat chunks
- WriteUInt32((uint)chunk.size);
- _writeFile.Seek(restoreWritePosition, SeekOrigin.Begin);
- }
- private UInt16 ReadUInt16()
- {
- byte[] data = _reader.ReadBytes(2);
- Array.Reverse(data);
- return BitConverter.ToUInt16(data, 0);
- }
- private UInt32 ReadUInt32()
- {
- byte[] data = _reader.ReadBytes(4);
- Array.Reverse(data);
- return BitConverter.ToUInt32(data, 0);
- }
- private UInt64 ReadUInt64()
- {
- byte[] data = _reader.ReadBytes(8);
- Array.Reverse(data);
- return BitConverter.ToUInt64(data, 0);
- }
- private void WriteUInt16(UInt16 value)
- {
- byte[] data = BitConverter.GetBytes(value);
- Array.Reverse(data);
- _writeFile.Write(data, 0, data.Length);
- }
- private void WriteChunkId(uint id)
- {
- WriteUInt32(id, false);
- }
- private void WriteUInt32(UInt32 value, bool isBigEndian = true)
- {
- byte[] data = BitConverter.GetBytes(value);
- if (isBigEndian)
- {
- Array.Reverse(data);
- }
- _writeFile.Write(data, 0, data.Length);
- }
- private void WriteUInt64(UInt64 value)
- {
- byte[] data = BitConverter.GetBytes(value);
- Array.Reverse(data);
- _writeFile.Write(data, 0, data.Length);
- }
- private static string ChunkIdToString(UInt32 id)
- {
- char a = (char)((id >> 0) & 255);
- char b = (char)((id >> 8) & 255);
- char c = (char)((id >> 16) & 255);
- char d = (char)((id >> 24) & 255);
- return string.Format("{0}{1}{2}{3}", a, b, c, d);
- }
- private static uint ChunkId(string id)
- {
- uint a = id[3];
- uint b = id[2];
- uint c = id[1];
- uint d = id[0];
- return (a << 24) | (b << 16) | (c << 8) | d;
- }
- [System.Diagnostics.Conditional("SUPPORT_DEBUGLOG")]
- private static void DebugLog(string message)
- {
- Debug.Log(message);
- }
- #if false && UNITY_EDITOR
- [UnityEditor.MenuItem("RenderHeads/Test MP4 Processing")]
- static void Test_Mp4Processing()
- {
- string path = "d:/video/video.mov";
- DateTime time = DateTime.Now;
- Options options = new Options();
- options.applyFastStart = true;
- options.applyStereoMode = true;
- options.stereoMode = StereoPacking.TopBottom;
- options.applySphericalVideoLayout = true;
- options.sphericalVideoLayout = SphericalVideoLayout.Equirectangular360;
- if (MP4FileProcessing.ProcessFile(path, true, options))
- {
- DateTime time2 = DateTime.Now;
- Debug.Log("success!");
- Debug.Log("Took: " + (time2 - time).TotalMilliseconds + "ms");
- }
- else
- {
- Debug.LogWarning("Did not modify file");
- }
- }
- #endif
- }
- }
|