mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 11:53:12 +00:00
Read the duration from the MP4/MOV movie header (moov/mvhd) on both the dispatcher media endpoint and the vendor portal completion upload, so a direct request cannot bypass the browser check. Unreadable metadata still never blocks an upload.
112 lines
4 KiB
C#
112 lines
4 KiB
C#
using System.Buffers.Binary;
|
|
using System.Text;
|
|
using SeaHaven.Services.Helpers;
|
|
|
|
namespace SeaHavenIndustries.Tests;
|
|
|
|
public class VideoDurationProbeTests
|
|
{
|
|
internal static byte[] Box(string type, params byte[][] children)
|
|
{
|
|
var body = children.SelectMany(child => child).ToArray();
|
|
var box = new byte[8 + body.Length];
|
|
BinaryPrimitives.WriteUInt32BigEndian(box, (uint)box.Length);
|
|
Encoding.ASCII.GetBytes(type).CopyTo(box, 4);
|
|
body.CopyTo(box, 8);
|
|
return box;
|
|
}
|
|
|
|
internal static byte[] MovieHeader(uint timescale, ulong duration, bool version1 = false)
|
|
{
|
|
var body = new byte[version1 ? 32 : 20];
|
|
body[0] = version1 ? (byte)1 : (byte)0;
|
|
if (version1)
|
|
{
|
|
BinaryPrimitives.WriteUInt32BigEndian(body.AsSpan(20), timescale);
|
|
BinaryPrimitives.WriteUInt64BigEndian(body.AsSpan(24), duration);
|
|
}
|
|
else
|
|
{
|
|
BinaryPrimitives.WriteUInt32BigEndian(body.AsSpan(12), timescale);
|
|
BinaryPrimitives.WriteUInt32BigEndian(body.AsSpan(16), (uint)duration);
|
|
}
|
|
return Box("mvhd", body);
|
|
}
|
|
|
|
/// <summary>A phone-style file: ftyp, a large mdat, then moov at the end (not fast-start).</summary>
|
|
internal static byte[] Mp4(uint timescale, ulong duration, bool version1 = false, int mediaBytes = 4096)
|
|
{
|
|
var ftyp = Box("ftyp", Encoding.ASCII.GetBytes("isom"), new byte[4]);
|
|
var mdat = Box("mdat", new byte[mediaBytes]);
|
|
var moov = Box("moov", MovieHeader(timescale, duration, version1));
|
|
return ftyp.Concat(mdat).Concat(moov).ToArray();
|
|
}
|
|
|
|
[Fact]
|
|
public void ReadsDurationFromMoovAfterMediaData()
|
|
{
|
|
Assert.Equal(95.0, VideoDurationProbe.TryReadDurationSeconds(new MemoryStream(Mp4(600, 57_000))));
|
|
}
|
|
|
|
[Fact]
|
|
public void ReadsVersionOneMovieHeader()
|
|
{
|
|
Assert.Equal(30.5, VideoDurationProbe.TryReadDurationSeconds(
|
|
new MemoryStream(Mp4(1000, 30_500, version1: true))));
|
|
}
|
|
|
|
[Fact]
|
|
public void FollowsSixtyFourBitBoxSizes()
|
|
{
|
|
var ftyp = Box("ftyp", Encoding.ASCII.GetBytes("qt "), new byte[4]);
|
|
var mdatBody = new byte[512];
|
|
var mdat = new byte[16 + mdatBody.Length];
|
|
BinaryPrimitives.WriteUInt32BigEndian(mdat, 1);
|
|
Encoding.ASCII.GetBytes("mdat").CopyTo(mdat, 4);
|
|
BinaryPrimitives.WriteUInt64BigEndian(mdat.AsSpan(8), (ulong)mdat.Length);
|
|
var moov = Box("moov", MovieHeader(90_000, 90_000UL * 120));
|
|
var file = ftyp.Concat(mdat).Concat(moov).ToArray();
|
|
|
|
Assert.Equal(120.0, VideoDurationProbe.TryReadDurationSeconds(new MemoryStream(file)));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("no-moov")]
|
|
[InlineData("truncated")]
|
|
[InlineData("zero-timescale")]
|
|
[InlineData("oversized-box")]
|
|
public void UnreadableStructureReturnsNull(string shape)
|
|
{
|
|
var bytes = shape switch
|
|
{
|
|
"no-moov" => Box("ftyp", Encoding.ASCII.GetBytes("isom"), new byte[4]).Concat(Box("mdat", new byte[64])).ToArray(),
|
|
"truncated" => Mp4(600, 57_000)[..^10],
|
|
"zero-timescale" => Mp4(0, 57_000),
|
|
_ => Box("ftyp", new byte[8]).Concat(new byte[] { 0x7F, 0xFF, 0xFF, 0xFF, (byte)'m', (byte)'o', (byte)'o', (byte)'v' }).ToArray(),
|
|
};
|
|
|
|
Assert.Null(VideoDurationProbe.TryReadDurationSeconds(new MemoryStream(bytes)));
|
|
}
|
|
|
|
[Fact]
|
|
public void NonSeekableStreamReturnsNull()
|
|
{
|
|
Assert.Null(VideoDurationProbe.TryReadDurationSeconds(new NonSeekableStream(Mp4(600, 57_000))));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(90.0, true)]
|
|
[InlineData(90.5, false)]
|
|
public void ContractAllowsUpToNinetySeconds(double seconds, bool allowed)
|
|
{
|
|
var message = WorkOrderMediaContract.ValidateVideoDuration(
|
|
new MemoryStream(Mp4(1000, (ulong)(seconds * 1000))));
|
|
|
|
Assert.Equal(allowed ? null : "Videos must be 90 seconds or shorter.", message);
|
|
}
|
|
|
|
private sealed class NonSeekableStream(byte[] bytes) : MemoryStream(bytes)
|
|
{
|
|
public override bool CanSeek => false;
|
|
}
|
|
}
|