mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 04:53:11 +00:00
130 lines
4.6 KiB
C#
130 lines
4.6 KiB
C#
|
|
using System.Buffers.Binary;
|
||
|
|
using System.Text;
|
||
|
|
|
||
|
|
namespace SeaHaven.Services.Helpers
|
||
|
|
{
|
||
|
|
/// <summary>
|
||
|
|
/// Reads a video's duration from the ISO base media (MP4 / QuickTime) movie header:
|
||
|
|
/// top-level <c>moov</c> box → <c>mvhd</c> timescale and duration. It seeks over box
|
||
|
|
/// headers only (the <c>moov</c> box may sit after a large <c>mdat</c>), never decodes
|
||
|
|
/// media and never allocates more than a few bytes. Returns null whenever the structure
|
||
|
|
/// cannot be read, so callers can let unreadable files through (SH-116: unreadable
|
||
|
|
/// metadata never blocks an upload).
|
||
|
|
/// </summary>
|
||
|
|
public static class VideoDurationProbe
|
||
|
|
{
|
||
|
|
private const int MaxBoxesPerLevel = 1024;
|
||
|
|
|
||
|
|
public static double? TryReadDurationSeconds(Stream? stream)
|
||
|
|
{
|
||
|
|
if (stream == null || !stream.CanSeek || !stream.CanRead)
|
||
|
|
return null;
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var moov = FindBox(stream, 0, stream.Length, "moov");
|
||
|
|
if (moov == null)
|
||
|
|
return null;
|
||
|
|
|
||
|
|
var mvhd = FindBox(stream, moov.Value.BodyStart, moov.Value.End, "mvhd");
|
||
|
|
return mvhd == null ? null : ReadMovieHeaderSeconds(stream, mvhd.Value);
|
||
|
|
}
|
||
|
|
catch (IOException)
|
||
|
|
{
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private readonly record struct Box(long BodyStart, long End);
|
||
|
|
|
||
|
|
private static Box? FindBox(Stream stream, long start, long end, string type)
|
||
|
|
{
|
||
|
|
var header = new byte[8];
|
||
|
|
var position = start;
|
||
|
|
for (var i = 0; i < MaxBoxesPerLevel && position + 8 <= end; i++)
|
||
|
|
{
|
||
|
|
stream.Position = position;
|
||
|
|
if (!ReadExactly(stream, header, 8))
|
||
|
|
return null;
|
||
|
|
|
||
|
|
long size = BinaryPrimitives.ReadUInt32BigEndian(header);
|
||
|
|
var boxType = Encoding.ASCII.GetString(header, 4, 4);
|
||
|
|
var headerLength = 8;
|
||
|
|
if (size == 1)
|
||
|
|
{
|
||
|
|
// 64-bit "largesize" follows the type.
|
||
|
|
var large = new byte[8];
|
||
|
|
if (!ReadExactly(stream, large, 8))
|
||
|
|
return null;
|
||
|
|
var largeSize = BinaryPrimitives.ReadUInt64BigEndian(large);
|
||
|
|
if (largeSize > long.MaxValue)
|
||
|
|
return null;
|
||
|
|
size = (long)largeSize;
|
||
|
|
headerLength = 16;
|
||
|
|
}
|
||
|
|
else if (size == 0)
|
||
|
|
{
|
||
|
|
size = end - position;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (size < headerLength || position + size > end)
|
||
|
|
return null;
|
||
|
|
|
||
|
|
if (boxType == type)
|
||
|
|
return new Box(position + headerLength, position + size);
|
||
|
|
|
||
|
|
position += size;
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static double? ReadMovieHeaderSeconds(Stream stream, Box mvhd)
|
||
|
|
{
|
||
|
|
// version(1) flags(3), then v0: creation(4) modification(4) timescale(4) duration(4)
|
||
|
|
// v1: creation(8) modification(8) timescale(4) duration(8)
|
||
|
|
var body = new byte[32];
|
||
|
|
stream.Position = mvhd.BodyStart;
|
||
|
|
var available = (int)Math.Min(body.Length, mvhd.End - mvhd.BodyStart);
|
||
|
|
if (available < 20 || !ReadExactly(stream, body, available))
|
||
|
|
return null;
|
||
|
|
|
||
|
|
uint timescale;
|
||
|
|
ulong duration;
|
||
|
|
if (body[0] == 0)
|
||
|
|
{
|
||
|
|
timescale = BinaryPrimitives.ReadUInt32BigEndian(body.AsSpan(12, 4));
|
||
|
|
duration = BinaryPrimitives.ReadUInt32BigEndian(body.AsSpan(16, 4));
|
||
|
|
}
|
||
|
|
else if (body[0] == 1 && available >= 32)
|
||
|
|
{
|
||
|
|
timescale = BinaryPrimitives.ReadUInt32BigEndian(body.AsSpan(20, 4));
|
||
|
|
duration = BinaryPrimitives.ReadUInt64BigEndian(body.AsSpan(24, 8));
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// All-ones duration means "unknown" in the spec.
|
||
|
|
if (timescale == 0 || duration == uint.MaxValue || duration == ulong.MaxValue)
|
||
|
|
return null;
|
||
|
|
|
||
|
|
return (double)duration / timescale;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static bool ReadExactly(Stream stream, byte[] buffer, int count)
|
||
|
|
{
|
||
|
|
var read = 0;
|
||
|
|
while (read < count)
|
||
|
|
{
|
||
|
|
var n = stream.Read(buffer, read, count - read);
|
||
|
|
if (n <= 0)
|
||
|
|
return false;
|
||
|
|
read += n;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|