InferenceWorker/VideoConvert.cs
using OpenCvSharp;

namespace HumanoidMocap.Worker;

/// <summary>Re-encodes a video Windows cannot decode (iPhone HEVC without the Store extension, 10-bit or 4:4:4
/// H.264) into an H.264 MP4. The frames are read with the FFmpeg decoder bundled in OpenCV's runtime, which
/// applies the phone's rotation, and written with Windows' own H.264 encoder, as the HOT3D import does.
/// Frames are written at the source's average rate; audio is dropped, as capture never uses it.</summary>
public static class VideoConvert
{
    public static void Run(string input,string output,CancellationToken cancellation,Action<string>? progress)
    {
        if(!File.Exists(input))throw new FileNotFoundException("Video not found.",input);
        // OpenCV opens files by narrow name; work in a plain-ASCII folder when either path is not.
        var scratch=new[]{Path.GetDirectoryName(Path.GetFullPath(output))!,Path.GetTempPath(),
            Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),"sbox-humanoid-mocap")}.First(p=>p.All(c=>c<128));
        Directory.CreateDirectory(scratch);var id=Guid.NewGuid().ToString("N");
        var source=input.All(c=>c<128)?input:Path.Combine(scratch,"hm-source-"+id+Path.GetExtension(input));
        var encoded=Path.Combine(scratch,"hm-converted-"+id+".mp4");
        try
        {
            if(source!=input)File.Copy(input,source,true);
            using var capture=new VideoCapture(source,VideoCaptureAPIs.FFMPEG);
            if(!capture.IsOpened())throw new NotSupportedException("The video could not be opened for conversion.");
            var fps=capture.Fps;if(!(fps is > 1 and < 1000))fps=30;
            var total=capture.FrameCount;using var frame=new Mat();
            if(!capture.Read(frame)||frame.Empty())throw new NotSupportedException("The video has no readable frames.");
            // H.264 needs even dimensions.
            var size=new Size(frame.Width&~1,frame.Height&~1);
            using(var writer=new VideoWriter(encoded,VideoCaptureAPIs.MSMF,FourCC.H264,fps,size))
            {
                if(!writer.IsOpened())throw new NotSupportedException("Windows' H.264 encoder is unavailable.");
                var written=0;var reported=-1;
                do
                {
                    cancellation.ThrowIfCancellationRequested();
                    if(frame.Width!=size.Width||frame.Height!=size.Height){using var cropped=new Mat(frame,new Rect(0,0,size.Width,size.Height));writer.Write(cropped);}
                    else writer.Write(frame);
                    written++;var percent=total>0?(int)(written*100L/total):-1;
                    if(percent>=reported+5){progress?.Invoke(total>0?$"Converting video for this PC · {Math.Min(percent,100)}%":$"Converting video for this PC · {written} frames");reported=percent;}
                }
                while(capture.Read(frame)&&!frame.Empty());
                if(written<2)throw new NotSupportedException("The video is too short to convert.");
            }
            Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(output))!);
            File.Move(encoded,output,true);
        }
        finally
        {
            if(source!=input&&File.Exists(source))File.Delete(source);
            if(File.Exists(encoded))File.Delete(encoded);
        }
    }
}