Editor/HumanoidMocap/Video/VideoPreviewCache.cs
using System;
using System.Buffers;
using System.IO;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
namespace HumanoidMocapX.EditorTools.Video;
/// <summary>Stages source video in a disk cache without changing or transcoding it.</summary>
internal static class VideoPreviewCache
{
const int BufferSize = 65536;
internal static async Task<string> CopyAsync(string source, string folder, CancellationToken cancellation)
{
cancellation.ThrowIfCancellationRequested();
Directory.CreateDirectory(folder);
var partial = Path.Combine(folder, Guid.NewGuid().ToString("N") + ".partial");
var buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
try
{
byte[] digest;
using (var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256))
{
await using var input = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, true);
if (input.Length == 0) throw new InvalidDataException("The source video is empty: " + source);
await using var output = new FileStream(partial, FileMode.CreateNew, FileAccess.Write, FileShare.None, BufferSize, true);
int count;
while ((count = await input.ReadAsync(buffer.AsMemory(0, BufferSize), cancellation).ConfigureAwait(false)) > 0)
{
hash.AppendData(buffer, 0, count);
await output.WriteAsync(buffer.AsMemory(0, count), cancellation).ConfigureAwait(false);
}
await output.FlushAsync(cancellation).ConfigureAwait(false);
digest = hash.GetHashAndReset();
}
var filename = Convert.ToHexString(digest).ToLowerInvariant() + Path.GetExtension(source).ToLowerInvariant();
var destination = Path.Combine(folder, filename);
if (File.Exists(destination))
{
await using var cached = new FileStream(destination, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, true);
var actual = await SHA256.HashDataAsync(cached, cancellation).ConfigureAwait(false);
if (CryptographicOperations.FixedTimeEquals(digest, actual)) return filename;
}
cancellation.ThrowIfCancellationRequested();
File.Move(partial, destination, true);
return filename;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
File.Delete(partial);
}
}
}