tests/test_preview.cs
using System.Security.Cryptography;
using HumanoidMocapX.Core.Preview;
using HumanoidMocapX.EditorTools.Video;
static class PreviewTests
{
internal static async Task Run()
{
Pixels(); Playback(); await Cache();
Console.WriteLine("PASS: bounded preview pixels, allocation-free playback decisions, streamed video cache and cancellation");
}
static void Pixels()
{
var frame = new PreviewFrameBuffer();
byte[] source = { 255, 10, 20, 255, 30, 240, 50, 128, 60, 70, 230, 0, 80, 90, 100, 255 };
frame.Copy(source, 2, 2);
MotionTests.Equal(2, frame.Width); MotionTests.Equal(2, frame.Height); MotionTests.Equal(1L, frame.Version);
MotionTests.Check(frame.Pixels.SequenceEqual(source), "Preview changed RGBA channel order or alpha");
source[0] = 0; MotionTests.Equal((byte)255, frame.Pixels[0]);
var large = new byte[1920 * 1080 * 4];
for (var y = 0; y < 1080; y++)
for (var x = 0; x < 1920; x++)
{
var offset = (y * 1920 + x) * 4;
large[offset] = (byte)x; large[offset + 1] = (byte)y; large[offset + 2] = (byte)(x + y); large[offset + 3] = 255;
}
frame.Copy(large, 1920, 1080); MotionTests.Equal(960, frame.Width); MotionTests.Equal(540, frame.Height);
for (var y = 0; y < 540; y++)
for (var x = 0; x < 960; x++)
MotionTests.Check(frame.Pixels.Slice((y * 960 + x) * 4, 4).SequenceEqual(large.AsSpan((y * 2 * 1920 + x * 2) * 4, 4)), "Preview sampled the wrong pixel");
var portrait = new byte[1080 * 1920 * 4]; frame.Copy(portrait, 1080, 1920);
MotionTests.Equal(303, frame.Width); MotionTests.Equal(540, frame.Height);
for (var i = 0; i < 3; i++) frame.Copy(portrait, 1080, 1920);
var allocated = GC.GetAllocatedBytesForCurrentThread();
for (var i = 0; i < 8; i++) frame.Copy(portrait, 1080, 1920);
MotionTests.Equal(0L, GC.GetAllocatedBytesForCurrentThread() - allocated);
var version = frame.Version;
MotionTests.Throws<ArgumentException>(() => frame.Copy(new byte[4], 0, 1));
MotionTests.Throws<ArgumentException>(() => frame.Copy(new byte[4], 2, 1));
MotionTests.Throws<ArgumentException>(() => frame.Copy(new byte[4], int.MaxValue, int.MaxValue));
MotionTests.Equal(version, frame.Version);
}
static void Playback()
{
var playback = new PreviewPlayback();
MotionTests.Near(0, playback.SeekTime(0, 8, 0, true)!.Value);
MotionTests.Check(playback.SeekTime(.033, 8, .025, true) is null, "Normal playback unnecessarily seeks");
MotionTests.Near(4, playback.SeekTime(4, 8, .05, true)!.Value);
MotionTests.Near(0, playback.SeekTime(0, 8, 7.99, true)!.Value);
MotionTests.Near(1, playback.SeekTime(1, 8, 1.05, false)!.Value);
MotionTests.Check(playback.SeekTime(1.01, 8, 1, false) is null, "Paused sub-frame movement unnecessarily seeks");
MotionTests.Near(31d / 30, playback.SeekTime(1.04, 8, 1, false)!.Value);
MotionTests.Near(7.999, playback.SeekTime(20, 8, 1, true)!.Value, 1e-6);
MotionTests.Near(0, playback.SeekTime(0, .0001, 0, false)!.Value);
MotionTests.Throws<ArgumentException>(() => playback.SeekTime(double.NaN, 8, 0, true));
MotionTests.Throws<ArgumentException>(() => playback.SeekTime(0, 0, 0, true));
MotionTests.Throws<ArgumentException>(() => playback.SeekTime(0, 8, -1, true));
playback.SeekTime(1, 8, 1, true);
var allocated = GC.GetAllocatedBytesForCurrentThread();
for (var i = 0; i < 1000; i++) playback.SeekTime(1, 8, 1, true);
MotionTests.Equal(0L, GC.GetAllocatedBytesForCurrentThread() - allocated);
}
static async Task Cache()
{
var folder = Path.Combine(Path.GetTempPath(), "mocap-preview-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(folder);
try
{
var source = Path.Combine(folder, "source.MP4"); var cache = Path.Combine(folder, "cache");
var data = Enumerable.Range(0, 200_003).Select(i => (byte)(i * 37)).ToArray(); await File.WriteAllBytesAsync(source, data);
var expected = Convert.ToHexString(SHA256.HashData(data)).ToLowerInvariant() + ".mp4";
var filename = await VideoPreviewCache.CopyAsync(source, cache, default);
MotionTests.Equal(expected, filename);
MotionTests.Check((await File.ReadAllBytesAsync(Path.Combine(cache, filename))).SequenceEqual(data), "Preview cache changed the source bytes");
MotionTests.Equal(filename, await VideoPreviewCache.CopyAsync(source, cache, default));
await File.WriteAllBytesAsync(Path.Combine(cache, filename), new byte[] { 1, 2, 3 });
await VideoPreviewCache.CopyAsync(source, cache, default);
MotionTests.Check((await File.ReadAllBytesAsync(Path.Combine(cache, filename))).SequenceEqual(data), "Preview cache reused corrupted footage");
MotionTests.Check((await File.ReadAllBytesAsync(source)).SequenceEqual(data), "Preview staging modified the source");
using var cancelled = new CancellationTokenSource(); cancelled.Cancel();
await Throws<OperationCanceledException>(() => VideoPreviewCache.CopyAsync(source, cache, cancelled.Token));
var empty = Path.Combine(folder, "empty.mp4"); await File.WriteAllBytesAsync(empty, Array.Empty<byte>());
await Throws<InvalidDataException>(() => VideoPreviewCache.CopyAsync(empty, cache, default));
await Throws<FileNotFoundException>(() => VideoPreviewCache.CopyAsync(Path.Combine(folder, "missing.mp4"), cache, default));
MotionTests.Equal(1, Directory.GetFiles(cache).Length);
MotionTests.Equal(0, Directory.GetFiles(cache, "*.partial").Length);
}
finally { Directory.Delete(folder, true); }
}
static async Task Throws<T>(Func<Task> action) where T : Exception
{
try { await action(); } catch (T) { return; }
throw new Exception("Expected " + typeof(T).Name);
}
}