mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-21 16:12:01 +00:00
277 lines
8.6 KiB
C#
277 lines
8.6 KiB
C#
using System.Diagnostics;
|
|
using System.Globalization;
|
|
|
|
namespace splitter;
|
|
|
|
public sealed class SimpleSplitter : LoggingBase, ISegmentProcessor
|
|
{
|
|
// ------------------------------------------------------------
|
|
// Internal state (opaque to caller)
|
|
// ------------------------------------------------------------
|
|
|
|
private sealed class State : IFrameProcessingState
|
|
{
|
|
public Process? DecodeProcess { get; set; }
|
|
public Stream? DecodeStdout { get; set; }
|
|
|
|
public string InputFile { get; }
|
|
public double Start { get; }
|
|
public double Length { get; }
|
|
public int? Rotate { get; }
|
|
public string[] Passthrough { get; }
|
|
public VideoInfo Info { get; }
|
|
public bool PlainText { get; }
|
|
|
|
public State(SingleTask job)
|
|
{
|
|
InputFile = job.Job.InputFile;
|
|
Start = job.SegmentStart;
|
|
Length = job.SegmentLength;
|
|
Rotate = job.Job.Rotate;
|
|
Passthrough = job.Job.Passthrough;
|
|
Info = job.Info;
|
|
PlainText = job.Job.PlainText;
|
|
}
|
|
}
|
|
|
|
public SimpleSplitter(int segmentNo, ILogger logger)
|
|
: base(logger, segmentNo)
|
|
{
|
|
}
|
|
|
|
// ============================================================
|
|
// InitSegment
|
|
// ============================================================
|
|
|
|
public IFrameProcessingState InitSegment(SingleTask job, CancellationToken token)
|
|
{
|
|
var state = new State(job);
|
|
|
|
var decode = StartDecode(job, token);
|
|
state.DecodeProcess = decode;
|
|
state.DecodeStdout = decode.StandardOutput.BaseStream;
|
|
|
|
return state;
|
|
}
|
|
|
|
// ============================================================
|
|
// GetNextProcessedFrame
|
|
// ============================================================
|
|
|
|
public Mat? GetNextProcessedFrame(IFrameProcessingState processorState, CancellationToken token)
|
|
{
|
|
var state = (State)processorState;
|
|
|
|
if (state.DecodeStdout == null)
|
|
return null;
|
|
|
|
// SimpleSplitter does not modify frames; it only copies or rotates.
|
|
// For preview, we decode raw frames and return them as-is.
|
|
|
|
// Determine expected frame size
|
|
var w = state.Info.Width;
|
|
var h = state.Info.Height;
|
|
var bytes = w * h * 3;
|
|
|
|
var buffer = new byte[bytes];
|
|
var read = state.DecodeStdout.Read(buffer, 0, bytes);
|
|
if (read != bytes)
|
|
return null;
|
|
|
|
var mat = new Mat(h, w, MatType.CV_8UC3);
|
|
System.Runtime.InteropServices.Marshal.Copy(buffer, 0, mat.Data, bytes);
|
|
|
|
return mat;
|
|
}
|
|
|
|
// ============================================================
|
|
// FinishSegment
|
|
// ============================================================
|
|
|
|
public void FinishSegment(IFrameProcessingState processorState)
|
|
{
|
|
var state = (State)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 { }
|
|
}
|
|
|
|
// ============================================================
|
|
// ProcessSegment (now uses preview API)
|
|
// ============================================================
|
|
|
|
public async Task ProcessSegment(SingleTask job, CancellationToken token)
|
|
{
|
|
var state = (State)InitSegment(job, token);
|
|
|
|
var encode = StartEncode(job);
|
|
using var encodeStdin = encode.StandardInput.BaseStream;
|
|
|
|
var name = Path.GetFileNameWithoutExtension(job.OutputFileName);
|
|
var sw = Stopwatch.StartNew();
|
|
|
|
while (true)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
|
|
var frame = GetNextProcessedFrame(state, token);
|
|
if (frame == null)
|
|
break;
|
|
|
|
// Write raw frame to encoder
|
|
var bytes = frame.Width * frame.Height * 3;
|
|
var buffer = new byte[bytes];
|
|
System.Runtime.InteropServices.Marshal.Copy(frame.Data, buffer, 0, bytes);
|
|
encodeStdin.Write(buffer, 0, bytes);
|
|
|
|
frame.Dispose();
|
|
}
|
|
|
|
encodeStdin.Flush();
|
|
encodeStdin.Close();
|
|
|
|
await encode.WaitForExitAsync(token);
|
|
|
|
FinishSegment(state);
|
|
|
|
ClearProgress(name);
|
|
|
|
if (encode.ExitCode != 0)
|
|
LogError($"Segment {name} FFmpeg encoding failed");
|
|
else
|
|
LogInfo($"Segment {name} processing completed");
|
|
}
|
|
|
|
// ============================================================
|
|
// FFmpeg helpers
|
|
// ============================================================
|
|
|
|
private Process StartDecode(SingleTask job, CancellationToken token)
|
|
{
|
|
var ss = job.SegmentStart.ToString("0.###", CultureInfo.InvariantCulture);
|
|
var t = job.SegmentLength.ToString("0.###", CultureInfo.InvariantCulture);
|
|
|
|
var rotate = GetRotationFilter(job.Job.Rotate);
|
|
var vf = rotate != null ? $"-vf format=bgr24,{rotate}" : "-vf format=bgr24";
|
|
|
|
var args =
|
|
$"-i \"{job.Job.InputFile}\" -ss {ss} -t {t} " +
|
|
"-an -sn " +
|
|
$"{vf} " +
|
|
"-f rawvideo -";
|
|
|
|
var psi = new ProcessStartInfo
|
|
{
|
|
FileName = "ffmpeg",
|
|
Arguments = args,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
var p = Process.Start(psi) ?? throw new Exception("Failed to start ffmpeg decode.");
|
|
return p;
|
|
}
|
|
|
|
private Process StartEncode(SingleTask job)
|
|
{
|
|
var inputFile = job.Job.InputFile;
|
|
var outputFile = job.OutputFileName;
|
|
var start = job.SegmentStart;
|
|
var length = job.SegmentLength;
|
|
|
|
var rotation = GetRotationFilter(job.Job.Rotate);
|
|
|
|
string args;
|
|
|
|
if (rotation == null)
|
|
{
|
|
args =
|
|
$"-ss {start.ToString(CultureInfo.InvariantCulture)} " +
|
|
$"-i \"{inputFile}\" " +
|
|
$"-t {length.ToString(CultureInfo.InvariantCulture)} " +
|
|
$"-c copy {string.Join(" ", job.Job.Passthrough)} " +
|
|
$"\"{outputFile}\" -y";
|
|
}
|
|
else
|
|
{
|
|
var sarArg = "";
|
|
var darArg = "";
|
|
|
|
var sar = job.Info.SampleAspectRatio;
|
|
if (sar != null)
|
|
{
|
|
var sarNum = Convert.ToInt64(job.Info.Sar.X);
|
|
var sarDen = Convert.ToInt64(job.Info.Sar.Y);
|
|
|
|
var w = job.Info.Width;
|
|
var h = job.Info.Height;
|
|
|
|
if (job.Job.Rotate == 90 || job.Job.Rotate == 270)
|
|
(w, h) = (h, w);
|
|
|
|
var darNum = w * sarNum;
|
|
var darDen = h * sarDen;
|
|
|
|
long Gcd(long a, long b)
|
|
{
|
|
while (b != 0) (a, b) = (b, a % b);
|
|
return a;
|
|
}
|
|
|
|
var g = Gcd(darNum, darDen);
|
|
darNum /= g;
|
|
darDen /= g;
|
|
|
|
sarArg = $"-vf \"{rotation},setsar={sarNum}:{sarDen}\" ";
|
|
darArg = $"-aspect {darNum}:{darDen} ";
|
|
}
|
|
else
|
|
sarArg = $"-vf \"{rotation}\" ";
|
|
|
|
args =
|
|
$"-ss {start.ToString(CultureInfo.InvariantCulture)} " +
|
|
$"-i \"{inputFile}\" " +
|
|
$"-t {length.ToString(CultureInfo.InvariantCulture)} " +
|
|
sarArg + darArg +
|
|
"-c:v h264_nvenc -preset p4 -b:v 8M -pix_fmt yuv420p " +
|
|
"-c:a copy " +
|
|
$"{string.Join(" ", job.Job.Passthrough)} " +
|
|
$"\"{outputFile}\" -y";
|
|
}
|
|
|
|
var psi = new ProcessStartInfo
|
|
{
|
|
FileName = "ffmpeg",
|
|
Arguments = args,
|
|
RedirectStandardInput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
return Process.Start(psi) ?? throw new Exception("Failed to start ffmpeg encode.");
|
|
}
|
|
|
|
private string? GetRotationFilter(int? degrees) =>
|
|
degrees switch
|
|
{
|
|
90 => "transpose=1",
|
|
180 => "rotate=PI",
|
|
270 => "transpose=2",
|
|
_ => null
|
|
};
|
|
}
|