Editor/HumanoidMocap/HandCaptureJob.cs
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HumanoidMocap.Target;

namespace HumanoidMocap.Editor;

/// <summary>Editor-side preparation and progress dispatch. Neural reconstruction
/// runs in the same separate C# worker used by the other capture backends.</summary>
public static class HandCaptureJob
{
    static readonly SemaphoreSlim SingleJob=new(1,1);
    public static async Task<string> RunAsync(string video,string modelPath,TargetRig template,double start,double? end,
        bool swapHands,Action<int,int,string> progress,CancellationToken token)
    {
        if(!await SingleJob.WaitAsync(0,token))throw new InvalidOperationException("Another reconstruction job is active.");
        try
        {
            // Canonical source proportions are independent of the selected target.
            var fs=global::Editor.FileSystem.ProjectTemporary;fs.CreateDirectory("humanoid_mocap/jobs");
            var directory=fs.GetFullPath("humanoid_mocap/jobs");
            var metadata=await Task.Run(()=>Mp4Metadata.Read(video),token);
            var total=metadata.CaptureTimes.Count(t=>t>=start&&(!end.HasValue||t<end));
            if(total is <1 or >1800)throw new ArgumentException("Choose a nonempty range of at most 1800 frames.");
            await EditorPipeline.SwitchToMainThread();
            var messages=new ConcurrentQueue<string>();var done=0;
            var job=NativeCapture.LandmarksAsync(video,modelPath,directory,start,end,swapHands,messages.Enqueue,token);
            void Dispatch()
            {
                while(messages.TryDequeue(out var message))
                {
                    if(message.StartsWith("Reconstructed ",StringComparison.Ordinal))
                    {
                        var counts=message.Split(' ')[1].Split('/');
                        if(counts.Length==2&&int.TryParse(counts[0],out var count))done=count;
                    }
                    else if(message=="Reusing cached hand observations")done=total;
                    progress?.Invoke(done,total,message);
                }
            }
            while(!job.IsCompleted)
            {
                await Task.WhenAny(job,Task.Delay(100));
                await EditorPipeline.SwitchToMainThread();Dispatch();
            }
            await EditorPipeline.SwitchToMainThread();Dispatch();
            return await job;
        }
        finally{SingleJob.Release();}
    }
}