mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-21 16:12:01 +00:00
502 lines
14 KiB
C#
502 lines
14 KiB
C#
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace splitter;
|
|
|
|
public sealed class TrackingSplitter : LoggingBase, ISegmentProcessor
|
|
{
|
|
private readonly IObjectTracker _tracker;
|
|
|
|
// ------------------------------------------------------------
|
|
// Internal state (never exposed)
|
|
// ------------------------------------------------------------
|
|
|
|
private sealed class FrameProcessingState : IFrameProcessingState
|
|
{
|
|
public SingleTask Job { get; }
|
|
public KalmanTracker Kalman { get; }
|
|
public CameraController Camera { get; }
|
|
|
|
public Mat FrameMat { get; }
|
|
public Mat OutMat { get; }
|
|
public byte[] InBuffer { get; }
|
|
public byte[] OutBuffer { get; }
|
|
|
|
public IVideoEnhancer? Enhancer { get; }
|
|
|
|
public int InBytes { get; }
|
|
public int OutBytes { get; }
|
|
|
|
public Process? DecodeProcess { get; set; }
|
|
public Stream? DecodeStdout { get; set; }
|
|
|
|
public FrameProcessingState(
|
|
SingleTask job,
|
|
KalmanTracker kalman,
|
|
CameraController camera,
|
|
Mat frameMat,
|
|
Mat outMat,
|
|
byte[] inBuffer,
|
|
byte[] outBuffer,
|
|
IVideoEnhancer? enhancer,
|
|
int inBytes,
|
|
int outBytes)
|
|
{
|
|
Job = job;
|
|
Kalman = kalman;
|
|
Camera = camera;
|
|
FrameMat = frameMat;
|
|
OutMat = outMat;
|
|
InBuffer = inBuffer;
|
|
OutBuffer = outBuffer;
|
|
Enhancer = enhancer;
|
|
InBytes = inBytes;
|
|
OutBytes = outBytes;
|
|
}
|
|
}
|
|
|
|
public TrackingSplitter(
|
|
int progressLine,
|
|
IObjectTracker tracker,
|
|
SingleJob cmd,
|
|
ILogger logger)
|
|
: base(logger, progressLine)
|
|
{
|
|
_tracker = tracker;
|
|
}
|
|
|
|
// ============================================================
|
|
// PUBLIC PREVIEW API
|
|
// ============================================================
|
|
|
|
// ------------------------------------------------------------
|
|
// InitSegment
|
|
// ------------------------------------------------------------
|
|
|
|
public IFrameProcessingState InitSegment(SingleTask job, CancellationToken token)
|
|
{
|
|
var state = (FrameProcessingState)CreateFrameState(job);
|
|
|
|
if (state.Enhancer != null)
|
|
state.Enhancer.InitializeAsync(
|
|
state.OutMat.Width,
|
|
state.OutMat.Height,
|
|
5,
|
|
token).Wait(token);
|
|
|
|
var decode = StartFfmpegDecode(
|
|
job.Job.InputFile,
|
|
job.SegmentStart,
|
|
job.SegmentLength,
|
|
job.Job.Rotate,
|
|
job.Job.PlainText,
|
|
token).Result;
|
|
|
|
state.DecodeProcess = decode;
|
|
state.DecodeStdout = decode.StandardOutput.BaseStream;
|
|
|
|
return state;
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// GetNextProcessedFrame
|
|
// ------------------------------------------------------------
|
|
|
|
public Mat? GetNextProcessedFrame(
|
|
IFrameProcessingState processorState,
|
|
CancellationToken token)
|
|
{
|
|
var state = (FrameProcessingState)processorState;
|
|
|
|
if (state.DecodeStdout == null)
|
|
return null;
|
|
|
|
if (!TryReadNextFrame(state.DecodeStdout, state, token))
|
|
return null;
|
|
|
|
return ProcessFrame(state.FrameMat, state, state.Job, token);
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// FinishSegment
|
|
// ------------------------------------------------------------
|
|
|
|
public void FinishSegment(IFrameProcessingState processorState)
|
|
{
|
|
var state = (FrameProcessingState)processorState;
|
|
|
|
try
|
|
{
|
|
if (state.DecodeProcess != null && !state.DecodeProcess.HasExited)
|
|
state.DecodeProcess.Kill(entireProcessTree: true);
|
|
}
|
|
catch { }
|
|
|
|
try
|
|
{
|
|
if (state.DecodeProcess != null && !state.DecodeProcess.HasExited)
|
|
state.DecodeProcess.WaitForExit();
|
|
}
|
|
catch { }
|
|
|
|
if (state.Enhancer is IAsyncDisposable ad)
|
|
ad.DisposeAsync().AsTask().Wait();
|
|
else if (state.Enhancer is IDisposable d)
|
|
d.Dispose();
|
|
}
|
|
|
|
// ============================================================
|
|
// PROCESSSEGMENT (full pipeline)
|
|
// ============================================================
|
|
|
|
public async Task ProcessSegment(SingleTask job, CancellationToken token)
|
|
{
|
|
var name = Path.GetFileNameWithoutExtension(job.OutputFileName);
|
|
var fps = job.Info.Fps;
|
|
|
|
var state = (FrameProcessingState)InitSegment(job, token);
|
|
|
|
var encode = await StartFfmpegEncode(
|
|
job.Job.InputFile,
|
|
job.OutputFileName,
|
|
job.SegmentStart,
|
|
job.SegmentLength,
|
|
state.OutMat.Width,
|
|
state.OutMat.Height,
|
|
job.Info,
|
|
job.Job.Passthrough,
|
|
job.Job.PlainText,
|
|
token);
|
|
|
|
using var encodeStdin = encode.StandardInput.BaseStream;
|
|
|
|
var totalFrames = (int)Math.Round(job.SegmentLength * fps);
|
|
var frameIndex = 0;
|
|
var startTime = DateTime.UtcNow;
|
|
|
|
while (frameIndex < totalFrames)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
|
|
var frame = GetNextProcessedFrame(state, token);
|
|
if (frame == null)
|
|
break;
|
|
|
|
frameIndex++;
|
|
|
|
EncodeFrame(frame, state, encodeStdin);
|
|
|
|
var elapsed = DateTime.UtcNow - startTime;
|
|
var progress = totalFrames > 0 ? (double)frameIndex / totalFrames : 0.0;
|
|
var speed = elapsed.TotalSeconds > 0 ? (frameIndex / elapsed.TotalSeconds) / fps : 0.0;
|
|
|
|
var remainingFrames = Math.Max(totalFrames - frameIndex, 0);
|
|
var etaSeconds = speed > 0 ? remainingFrames / speed : 0.0;
|
|
var eta = TimeSpan.FromSeconds(etaSeconds);
|
|
|
|
DrawProgress(name, progress, eta, speed);
|
|
}
|
|
|
|
encodeStdin.Flush();
|
|
encodeStdin.Close();
|
|
|
|
await encode.WaitForExitAsync();
|
|
|
|
ClearProgress(name);
|
|
|
|
if (encode.ExitCode != 0)
|
|
LogError($"{name}: FFmpeg encoding failed");
|
|
else
|
|
LogInfo($"{name}: Segment processing completed");
|
|
|
|
FinishSegment(state);
|
|
}
|
|
|
|
// ============================================================
|
|
// INTERNAL HELPERS
|
|
// ============================================================
|
|
|
|
private object CreateFrameState(SingleTask job)
|
|
{
|
|
var w = job.Info.Width;
|
|
var h = job.Info.Height;
|
|
var cw = job.Job.Debug ? w : job.Job.Crop!.Value.width;
|
|
var ch = job.Job.Debug ? h : job.Job.Crop!.Value.height;
|
|
|
|
var kalman = new KalmanTracker();
|
|
var camera = new CameraController(w, h, cw, ch, kalman, job.Job);
|
|
|
|
var frameMat = new Mat(h, w, MatType.CV_8UC3);
|
|
var outMat = new Mat(ch, cw, MatType.CV_8UC3);
|
|
|
|
var inBytes = w * h * 3;
|
|
var outBytes = cw * ch * 3;
|
|
|
|
var inBuffer = new byte[inBytes];
|
|
var outBuffer = new byte[outBytes];
|
|
|
|
IVideoEnhancer? enhancer = job.Job.Enhance
|
|
? new RealBasicVsr2xDmlEnhancer()
|
|
: null;
|
|
|
|
return new FrameProcessingState(
|
|
job,
|
|
kalman,
|
|
camera,
|
|
frameMat,
|
|
outMat,
|
|
inBuffer,
|
|
outBuffer,
|
|
enhancer,
|
|
inBytes,
|
|
outBytes);
|
|
}
|
|
|
|
private bool TryReadNextFrame(
|
|
Stream decodeStdout,
|
|
FrameProcessingState state,
|
|
CancellationToken token)
|
|
{
|
|
var read = ReadExact(
|
|
decodeStdout,
|
|
state.InBuffer,
|
|
0,
|
|
state.InBytes,
|
|
token).Result;
|
|
|
|
if (read != state.InBytes)
|
|
return false;
|
|
|
|
Marshal.Copy(state.InBuffer, 0, state.FrameMat.Data, state.InBytes);
|
|
return true;
|
|
}
|
|
|
|
private Mat ProcessFrame(
|
|
Mat inputFrame,
|
|
FrameProcessingState state,
|
|
SingleTask job,
|
|
CancellationToken token)
|
|
{
|
|
var (objects, primary) =
|
|
_tracker.SelectTrackedObject(job, inputFrame, state.Kalman.LastMeasurement);
|
|
|
|
state.Camera.Update(primary);
|
|
var roi = state.Camera.Roi;
|
|
|
|
if (job.Job.Debug)
|
|
{
|
|
DebugOverlay.DrawDebug(inputFrame, objects, state.Camera, state.Kalman);
|
|
inputFrame.CopyTo(state.OutMat);
|
|
}
|
|
else
|
|
{
|
|
using var cropped = new Mat(inputFrame, roi);
|
|
cropped.CopyTo(state.OutMat);
|
|
}
|
|
|
|
if (state.Enhancer != null)
|
|
{
|
|
if (state.Enhancer.TryProcessFrame(state.OutMat, out var enhanced, token))
|
|
return enhanced;
|
|
|
|
return state.OutMat;
|
|
}
|
|
|
|
return state.OutMat;
|
|
}
|
|
|
|
private void EncodeFrame(
|
|
Mat frame,
|
|
FrameProcessingState state,
|
|
Stream encodeStdin)
|
|
{
|
|
Marshal.Copy(frame.Data, state.OutBuffer, 0, state.OutBytes);
|
|
encodeStdin.Write(state.OutBuffer, 0, state.OutBytes);
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// FFmpeg helpers
|
|
// ------------------------------------------------------------
|
|
|
|
private async Task<Process> StartFfmpegDecode(
|
|
string inputFile,
|
|
double start,
|
|
double length,
|
|
int? rotate,
|
|
bool plainText,
|
|
CancellationToken token)
|
|
{
|
|
var ss = start .ToString("0.###", CultureInfo.InvariantCulture);
|
|
var t = length.ToString("0.###", CultureInfo.InvariantCulture);
|
|
|
|
var rotateStr = GetRorationArg(rotate);
|
|
|
|
var args =
|
|
$"-i \"{inputFile}\" -ss {ss} -t {t} " +
|
|
"-an -sn " +
|
|
$"-vf format=bgr24{rotateStr} " +
|
|
"-f rawvideo -";
|
|
|
|
var psi = new ProcessStartInfo
|
|
{
|
|
FileName = "ffmpeg",
|
|
Arguments = args,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
var p = new Process { StartInfo = psi };
|
|
p.Start();
|
|
|
|
var fileName = Path.GetFileName(inputFile);
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
string? line;
|
|
while ((line = await p.StandardError.ReadLineAsync(token)) != null)
|
|
if (plainText)
|
|
LogInfo($"[ffmpeg-decode] {fileName}: {line}");
|
|
}
|
|
catch { }
|
|
});
|
|
|
|
return p;
|
|
}
|
|
|
|
public static string GetRorationArg(int? rotate)
|
|
{
|
|
var rotateStr = "";
|
|
if (rotate != null)
|
|
{
|
|
switch (rotate.Value)
|
|
{
|
|
case 90: rotateStr = ",transpose=1"; break;
|
|
case 180: rotateStr = ",transpose=PI"; break;
|
|
case 270: rotateStr = ",transpose=2"; break;
|
|
}
|
|
}
|
|
return rotateStr;
|
|
}
|
|
|
|
private async Task<Process> StartFfmpegEncode(
|
|
string inputFile,
|
|
string outputFile,
|
|
double start,
|
|
double length,
|
|
int width,
|
|
int height,
|
|
VideoInfo info,
|
|
string[] passthrough,
|
|
bool plainText,
|
|
CancellationToken token)
|
|
{
|
|
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
|
|
var fpsStr = info.Fps.ToString("0.###", CultureInfo.InvariantCulture);
|
|
var ss = start.ToString("0.###", CultureInfo.InvariantCulture);
|
|
var t = length.ToString("0.###", CultureInfo.InvariantCulture);
|
|
|
|
var sarArg = !string.IsNullOrWhiteSpace(info.SampleAspectRatio)
|
|
? $"-vf setsar={info.SampleAspectRatio} "
|
|
: "";
|
|
|
|
var darArg = "";
|
|
if (info.Sar is { } s)
|
|
{
|
|
var darNum = width * s.X;
|
|
var darDen = height * s.Y;
|
|
|
|
var dn = (int)Math.Min(int.MaxValue, Math.Max(int.MinValue, darNum));
|
|
var dd = (int)Math.Min(int.MaxValue, Math.Max(int.MinValue, darDen));
|
|
ReduceFraction(ref dn, ref dd);
|
|
|
|
if (dn > 0 && dd > 0)
|
|
darArg = $"-aspect {dn}:{dd} ";
|
|
}
|
|
|
|
var args =
|
|
"-y " +
|
|
$"-f rawvideo -pix_fmt bgr24 -s {width}x{height} -r {fpsStr} -i - " +
|
|
$"-ss {ss} -i \"{inputFile}\" " +
|
|
"-map 0:v:0 -map 1:a:0? -shortest " +
|
|
"-c:v h264_nvenc -preset p4 -b:v 8M -pix_fmt yuv420p " +
|
|
sarArg + darArg +
|
|
"-c:a copy " +
|
|
pass + $" \"{outputFile}\"";
|
|
|
|
var psi = new ProcessStartInfo
|
|
{
|
|
FileName = "ffmpeg",
|
|
Arguments = args,
|
|
RedirectStandardInput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
var p = new Process { StartInfo = psi };
|
|
p.Start();
|
|
|
|
var fileName = Path.GetFileName(outputFile);
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
string? line;
|
|
while ((line = await p.StandardError.ReadLineAsync(token)) != null)
|
|
if (plainText)
|
|
LogInfo($"[ffmpeg-encode] {fileName}: {line}");
|
|
}
|
|
catch { }
|
|
});
|
|
|
|
return p;
|
|
}
|
|
|
|
private static void ReduceFraction(ref int num, ref int den)
|
|
{
|
|
int Gcd(int a, int b)
|
|
{
|
|
while (b != 0)
|
|
{
|
|
var t = b;
|
|
b = a % b;
|
|
a = t;
|
|
}
|
|
return a;
|
|
}
|
|
|
|
var g = Gcd(Math.Abs(num), Math.Abs(den));
|
|
if (g > 1)
|
|
{
|
|
num /= g;
|
|
den /= g;
|
|
}
|
|
}
|
|
|
|
private static async Task<int> ReadExact(
|
|
Stream s,
|
|
byte[] buffer,
|
|
int offset,
|
|
int count,
|
|
CancellationToken token)
|
|
{
|
|
var total = 0;
|
|
while (total < count)
|
|
{
|
|
var read = await s.ReadAsync(buffer, offset + total, count - total, token);
|
|
if (read <= 0)
|
|
break;
|
|
total += read;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
|
|
}
|