NewN_UAVPlane/Assets/Plugins/RenderHeads/AVProMovieCapture/Runtime/Scripts/Internal/Mp4FileProcessing.cs

448 lines
11 KiB
C#

using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using System.IO;
using System;
//-----------------------------------------------------------------------------
// Copyright 2012-2021 RenderHeads Ltd. All rights reserved.
//-----------------------------------------------------------------------------
namespace RenderHeads.Media.AVProMovieCapture
{
// 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
public class MP4FileProcessing
{
private const int ChunkHeaderSize = 8;
private const int CopyBufferSize = 4096*16;
//private static uint Atom_ftyp = ChunkId("ftyp"); // file type
private static uint Atom_moov = ChunkId("moov"); // movie header
private static uint Atom_mdat = ChunkId("mdat"); // movie data
private static uint Atom_cmov = ChunkId("cmov"); // compressed movie data
private static uint Atom_trak = ChunkId("trak"); // track header
private static uint Atom_mdia = ChunkId("mdia"); // media
private static uint Atom_minf = ChunkId("minf"); // media information
private static uint Atom_stbl = ChunkId("stbl"); // sample table
private static uint Atom_stco = ChunkId("stco"); // sample table chunk offsets (32-bit)
private static uint Atom_co64 = ChunkId("co64"); // sample table chunk offsets (64-bit)
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;
};
private BinaryReader _reader;
private Stream _writeFile;
public static ManualResetEvent ApplyFastStartAsync(string filePath, bool keepBackup)
{
if (!File.Exists(filePath))
{
Debug.LogError("File not found: " + filePath);
return null;
}
ManualResetEvent syncEvent = new ManualResetEvent(false);
Thread thread = new Thread(
() =>
{
try
{
ApplyFastStart(filePath, keepBackup);
}
catch (System.Exception e)
{
UnityEngine.Debug.LogException(e);
}
syncEvent.Set();
}
);
thread.Start();
return syncEvent;
}
public static bool ApplyFastStart(string filePath, bool keepBackup)
{
if (!File.Exists(filePath))
{
Debug.LogError("File not found: " + filePath);
return false;
}
string tempPath = filePath + "-" + System.Guid.NewGuid() + ".temp";
bool result = ApplyFastStart(filePath, tempPath);
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);
}
return result;
}
public static bool ApplyFastStart(string srcPath, string dstPath)
{
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();
bool result = mp4.Open(srcStream, dstStream);
mp4.Close();
return result;
}
}
}
public bool Open(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);
bool canReorder = (chunk_moov != null && chunk_mdat != null);
bool isReorderNeeded = false;
if (canReorder)
{
isReorderNeeded = (chunk_moov.offset > chunk_mdat.offset);
if (isReorderNeeded)
{
if (ChunkContainsChildChunkWithId(chunk_moov, Atom_cmov))
{
Debug.LogError("moov chunk is compressed - unsupported");
canReorder = false;
}
}
else
{
Debug.Log("No reordering needed");
}
}
else
{
Debug.LogWarning("no chunk tags found - incorrect file format?");
}
// Reorder so that "moov" chunk is before any "mdat" chunks
if (canReorder && isReorderNeeded)
{
Debug.Assert(chunk_moov.offset > chunk_mdat.offset);
ulong bytesOffset = (ulong)(chunk_moov.size);
//Debug.Log("offseting by: " + bytesOffset);
_writeFile = dstStream;
foreach (Chunk chunk in rootChunks)
{
if (chunk == chunk_mdat)
{
WriteChunk_moov(chunk_moov, bytesOffset);
WriteChunk(chunk);
}
else if (chunk != chunk_moov)
{
WriteChunk(chunk);
}
}
return true;
}
return false;
}
public void Close()
{
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 + ChunkHeaderSize;
}
_reader.BaseStream.Seek(fileOffset, SeekOrigin.Begin);
}
long chunkEnd = _reader.BaseStream.Length;
if (parentChunk != null)
{
chunkEnd = parentChunk.offset + parentChunk.size;
}
List<Chunk> result = new List<Chunk>();
if (_reader.BaseStream.Position < chunkEnd)
{
Chunk chunk = ReadChunkHeader();
while (chunk != null && _reader.BaseStream.Position < chunkEnd)
{
result.Add(chunk);
_reader.BaseStream.Seek(chunk.offset + chunk.size, SeekOrigin.Begin);
chunk = ReadChunkHeader();
}
}
return result;
}
private Chunk ReadChunkHeader()
{
Chunk chunk = null;
if ((_reader.BaseStream.Length - _reader.BaseStream.Position) >= ChunkHeaderSize)
{
chunk = new Chunk();
chunk.offset = _reader.BaseStream.Position;
chunk.size = ReadUInt32();
chunk.id = _reader.ReadUInt32();
if (chunk.size == 1)
{
// NOTE: One indicates we need to read the extended 64-bit size
chunk.size = (long)ReadUInt64();
}
if (chunk.size == 0)
{
// NOTE: Zero 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 void WriteChunk(Chunk chunk)
{
_reader.BaseStream.Seek(chunk.offset, SeekOrigin.Begin);
CopyBytes(chunk.size);
}
private void WriteChunkHeader(Chunk chunk)
{
_reader.BaseStream.Seek(chunk.offset, SeekOrigin.Begin);
// TODO: potential bug as ChunkHeaderSize could actually by 16 instead of 8 in cases where extended size is used
CopyBytes(ChunkHeaderSize);
}
private void CopyBytes(long numBytes)
{
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 WriteChunk_moov(Chunk parentChunk, ulong byteOffset)
{
// Hierarchy of atoms to apply offsets to moov > trak > mdia > minf > stbl > "co64, stco"
WriteChunkHeader(parentChunk);
List<Chunk> children = ReadChildChunks(parentChunk);
// TODO: potential bug as ChunkHeaderSize could actually by 16 instead of 8 in cases where extended size is used
_reader.BaseStream.Seek(parentChunk.offset + ChunkHeaderSize, SeekOrigin.Begin);
foreach (Chunk chunk in children)
{
if (chunk.id == Atom_stco)
{
WriteChunkHeader(chunk);
CopyBytes(4);
// Apply offsets
uint chunkCount = ReadUInt32();
WriteUInt32(chunkCount);
for (int i = 0; i < chunkCount; i++)
{
// TODO: potential bug here if new offset is greater than 32-bit..
uint offset = ReadUInt32();
offset += (uint)byteOffset;
WriteUInt32(offset);
}
}
else if (chunk.id == Atom_co64)
{
WriteChunkHeader(chunk);
CopyBytes(4);
// Apply offsets
uint chunkCount = ReadUInt32();
WriteUInt32(chunkCount);
for (int i = 0; i < chunkCount; i++)
{
// TODO: potential bug here if new offset is greater than 64-bit..
ulong offset = ReadUInt64();
offset += byteOffset;
WriteUInt64(offset);
}
}
else if (chunk.id == Atom_trak ||
chunk.id == Atom_mdia ||
chunk.id == Atom_minf ||
chunk.id == Atom_stbl)
{
// Go into these chunks searching for the offset chunks
WriteChunk_moov(chunk, byteOffset);
}
else
{
// We don't care about this chunk so just copy it
WriteChunk(chunk);
}
}
}
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 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 ChunkType(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;
}
}
}
#if false
public class Mp4FastStartTest : MonoBehaviour
{
public string _path = "R:/Products/Unity/AVProVideo/Media/BigBuckBunny_2160p24_h264_2Mbps.mp4";
void Start ()
{
DateTime time = DateTime.Now;
if (MP4FileProcessing.ApplyFastStart(_path, true))
{
DateTime time2 = DateTime.Now;
Debug.Log("success!");
Debug.Log("Took: " + (time2 - time).TotalMilliseconds + "ms");
}
else
{
Debug.LogWarning("Did not modify file");
}
}
}
#endif