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); } /// A phone-style file: ftyp, a large mdat, then moov at the end (not fast-start). 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; } }