using System.Diagnostics; using Age.Engine.Sys4; internal sealed record MovieCorpusInput(long PackedId, string Name, Func ReadPayload); internal sealed record MovieCorpusItemResult( long PackedId, string PackedIdHex, string Name, long PayloadBytes, int ExpectedWidth, int ExpectedHeight, int Width, int Height, long StopTimeMs, int FrameRateNumerator, int FrameRateDenominator, bool HasAudio, long FrameCount, long FirstPresentationTimeMs, long LastPresentationTimeMs, bool FramesChanged, long ReadMilliseconds, long OpenMilliseconds, long DecodeMilliseconds, long DisposeMilliseconds, bool Passed, string? Error); internal sealed record MovieCorpusReport( string StartedUtc, string CompletedUtc, int ExpectedCount, int CandidateCount, int PassedCount, int FailedCount, long ElapsedMilliseconds, bool Passed, IReadOnlyList SelectionErrors, IReadOnlyList Movies); internal static class MovieCorpusDiscovery { private static ReadOnlySpan MpegPackStart => [0, 0, 1, 0xba]; public static IReadOnlyList DiscoverMpegMovies( Sys4AssetCatalog catalog, IAssetStore store) { var movies = new List(); Span signature = stackalloc byte[4]; foreach (var packed in catalog.EnumerateAssets()) { if (!packed.Asset.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase)) continue; using Stream stream = store.Open(packed.Asset); int length = 0; while (length < signature.Length) { int read = stream.Read(signature[length..]); if (read == 0) break; length += read; } if (length == signature.Length && signature.SequenceEqual(MpegPackStart)) movies.Add(packed); } return movies; } public static bool TryReadSequenceDimensions(ReadOnlySpan payload, out int width, out int height) { for (int offset = 0; offset <= payload.Length - 7; offset++) { if (payload[offset] != 0 || payload[offset + 1] != 0 || payload[offset + 2] != 1 || payload[offset + 3] != 0xb3) continue; width = (payload[offset + 4] << 4) | (payload[offset + 5] >> 4); height = ((payload[offset + 5] & 0x0f) << 8) | payload[offset + 6]; return width > 0 && height > 0; } width = 0; height = 0; return false; } } internal sealed class MovieCorpusGate { private readonly Func _open; public MovieCorpusGate(Func open) => _open = open ?? throw new ArgumentNullException(nameof(open)); public MovieCorpusReport Run(IReadOnlyList inputs, int expectedCount, long maximumItemMilliseconds, Action? progress = null) { if (expectedCount < 0) throw new ArgumentOutOfRangeException(nameof(expectedCount)); if (maximumItemMilliseconds <= 0) throw new ArgumentOutOfRangeException(nameof(maximumItemMilliseconds)); DateTimeOffset started = DateTimeOffset.UtcNow; var total = Stopwatch.StartNew(); var selectionErrors = new List(); if (inputs.Count != expectedCount) selectionErrors.Add($"expected {expectedCount} MPEG movies, discovered {inputs.Count}"); var results = new List(inputs.Count); for (int index = 0; index < inputs.Count; index++) { MovieCorpusItemResult result = RunOne(inputs[index], maximumItemMilliseconds); results.Add(result); progress?.Invoke(index + 1, inputs.Count, result); } total.Stop(); int passed = results.Count(result => result.Passed); int failed = results.Count - passed; return new MovieCorpusReport( started.ToString("O"), DateTimeOffset.UtcNow.ToString("O"), expectedCount, inputs.Count, passed, failed, total.ElapsedMilliseconds, selectionErrors.Count == 0 && failed == 0, selectionErrors, results); } private MovieCorpusItemResult RunOne(MovieCorpusInput input, long maximumItemMilliseconds) { long payloadBytes = 0; int expectedWidth = 0, expectedHeight = 0, width = 0, height = 0; long stopTimeMs = 0, frameCount = 0, firstPts = -1, lastPts = -1; int frameRateNumerator = 0, frameRateDenominator = 0; bool hasAudio = false, framesChanged = false; long readMs = 0, openMs = 0, decodeMs = 0, disposeMs = 0; string? error = null; IFfmpegFrameSource? source = null; var itemTime = Stopwatch.StartNew(); try { var phase = Stopwatch.StartNew(); MoviePayload payload = input.ReadPayload(); phase.Stop(); readMs = phase.ElapsedMilliseconds; payloadBytes = payload.Bytes.LongLength; if (!MovieCorpusDiscovery.TryReadSequenceDimensions(payload.Bytes, out expectedWidth, out expectedHeight)) throw new InvalidDataException("MPEG sequence header was not found"); phase.Restart(); source = _open(payload); phase.Stop(); openMs = phase.ElapsedMilliseconds; FfmpegMovieInfo info = source.Info; width = info.Width; height = info.Height; stopTimeMs = info.StopTimeMs; frameRateNumerator = info.FrameRateNumerator; frameRateDenominator = info.FrameRateDenominator; hasAudio = info.HasAudio; if (width != expectedWidth || height != expectedHeight) throw new InvalidDataException( $"decoder dimensions {width}x{height} differ from MPEG sequence {expectedWidth}x{expectedHeight}"); if (width <= 0 || height <= 0 || stopTimeMs <= 0 || frameRateNumerator <= 0 || frameRateDenominator <= 0) throw new InvalidDataException( $"invalid metadata {width}x{height}, {stopTimeMs} ms, " + $"rate {frameRateNumerator}/{frameRateDenominator}"); var decodeTime = Stopwatch.StartNew(); byte[]? firstPixels = null; try { while (source.TryDecodeNextVideoFrame(out FfmpegVideoFrame frame)) { if (frame.Image.Width != width || frame.Image.Height != height) throw new InvalidDataException( $"frame {frameCount} dimensions are {frame.Image.Width}x{frame.Image.Height}, expected {width}x{height}"); int expectedBytes = checked(width * height * 4); if (frame.Image.Pixels.Length != expectedBytes) throw new InvalidDataException( $"frame {frameCount} has {frame.Image.Pixels.Length} RGBA bytes, expected {expectedBytes}"); if (frame.PresentationTimeMs < 0 || frame.PresentationTimeMs < lastPts) throw new InvalidDataException( $"frame {frameCount} timestamp {frame.PresentationTimeMs} follows {lastPts}"); if (frameCount == 0) { firstPts = frame.PresentationTimeMs; firstPixels = frame.Image.Pixels; } else if (!framesChanged && !frame.Image.Pixels.AsSpan().SequenceEqual(firstPixels)) framesChanged = true; lastPts = frame.PresentationTimeMs; frameCount++; if (itemTime.ElapsedMilliseconds > maximumItemMilliseconds) throw new TimeoutException($"item exceeded {maximumItemMilliseconds} ms before EOF"); } } finally { decodeTime.Stop(); decodeMs = decodeTime.ElapsedMilliseconds; } if (frameCount == 0) throw new InvalidDataException("decoder reached EOF without a video frame"); } catch (Exception exception) { error = exception.Message; } finally { if (source != null) { var disposeTime = Stopwatch.StartNew(); try { source.Dispose(); } catch (Exception exception) { error = error == null ? $"dispose failed: {exception.Message}" : $"{error}; dispose failed: {exception.Message}"; } disposeTime.Stop(); disposeMs = disposeTime.ElapsedMilliseconds; } } itemTime.Stop(); if (error == null && itemTime.ElapsedMilliseconds > maximumItemMilliseconds) error = $"item took {itemTime.ElapsedMilliseconds} ms, limit is {maximumItemMilliseconds} ms"; return new MovieCorpusItemResult( input.PackedId, $"0x{input.PackedId:x}", input.Name, payloadBytes, expectedWidth, expectedHeight, width, height, stopTimeMs, frameRateNumerator, frameRateDenominator, hasAudio, frameCount, firstPts, lastPts, framesChanged, readMs, openMs, decodeMs, disposeMs, error == null, error); } }