mirror of
https://github.com/unclshura/splitter.git
synced 2026-08-06 03:33:39 +00:00
Separated pass-through and rotating splitters. Rotation fixed.
This commit is contained in:
parent
3457aa6816
commit
e986964f00
@ -2,7 +2,6 @@
|
||||
|
||||
public sealed class BufferPool : IBufferPool
|
||||
{
|
||||
private readonly int _capacity;
|
||||
|
||||
public sealed class Entry
|
||||
{
|
||||
@ -18,10 +17,14 @@ public sealed class BufferPool : IBufferPool
|
||||
Bgr = new byte[w * h * 3];
|
||||
Bgra = new byte[w * h * 4];
|
||||
}
|
||||
|
||||
override public string ToString() => $"Entry({Width}x{Height})";
|
||||
}
|
||||
|
||||
private readonly Dictionary<(int w, int h), LinkedListNode<Entry>> _map;
|
||||
private readonly LinkedList<Entry> _lru;
|
||||
private readonly int _capacity;
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
public BufferPool()
|
||||
{
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
x:DataType="vm:PreviewPaneViewModel">
|
||||
|
||||
<Border Background="#202020" Padding="10">
|
||||
<Grid RowDefinitions="*,Auto,Auto">
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
|
||||
<local:PreviewCanvas
|
||||
Grid.Row="0"
|
||||
@ -19,7 +19,7 @@
|
||||
DetectAbove="{Binding DetectAbove, Mode=TwoWay}"
|
||||
DetectId="{Binding Selected.DetectId, Mode=TwoWay}"
|
||||
/>
|
||||
|
||||
<!--
|
||||
<Grid Grid.Row="1"
|
||||
ColumnDefinitions="Auto"
|
||||
Margin="0,10,0,0">
|
||||
@ -37,8 +37,8 @@
|
||||
</Button>
|
||||
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="2"
|
||||
-->
|
||||
<Grid Grid.Row="1"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
Margin="0,10,0,0">
|
||||
|
||||
|
||||
@ -57,7 +57,11 @@ public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcesso
|
||||
return [];
|
||||
|
||||
Func<int, ISegmentProcessor> processorFactory;
|
||||
if (job.Crop != null)
|
||||
if (job.Rotate != null && job.Rotate != 0)
|
||||
{
|
||||
processorFactory = i => new RotatingSplitter(i, _logger);
|
||||
}
|
||||
else if (job.Crop != null && job.Detect != null && job.Detect != "none")
|
||||
{
|
||||
processorFactory = i =>
|
||||
{
|
||||
@ -75,7 +79,7 @@ public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcesso
|
||||
}
|
||||
else
|
||||
{
|
||||
processorFactory = i => new SimpleSplitter(i, _logger);
|
||||
processorFactory = i => new PassthroughSplitter(i, _logger);
|
||||
}
|
||||
|
||||
var segmentsToUse = predefinedSegments;
|
||||
|
||||
103
splitter-cli/PassthroughSplitter.cs
Normal file
103
splitter-cli/PassthroughSplitter.cs
Normal file
@ -0,0 +1,103 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
namespace splitter;
|
||||
|
||||
public sealed class PassthroughSplitter : LoggingBase, ISegmentProcessor
|
||||
{
|
||||
private sealed class State : IFrameProcessingState
|
||||
{
|
||||
public Process? EncodeProcess { get; set; }
|
||||
|
||||
public string InputFile { get; }
|
||||
public string OutputFile { get; }
|
||||
public double Start { get; }
|
||||
public double Length { get; }
|
||||
public string[] Passthrough { get; }
|
||||
|
||||
public State(SingleTask job)
|
||||
{
|
||||
InputFile = job.Job.InputFile;
|
||||
OutputFile = job.OutputFileName;
|
||||
Start = job.SegmentStart;
|
||||
Length = job.SegmentLength;
|
||||
Passthrough = job.Job.Passthrough;
|
||||
}
|
||||
}
|
||||
|
||||
public PassthroughSplitter(int segmentNo, ILogger logger)
|
||||
: base(logger, segmentNo)
|
||||
{
|
||||
}
|
||||
|
||||
public IFrameProcessingState InitSegment(SingleTask job, CancellationToken token)
|
||||
{
|
||||
var state = new State(job);
|
||||
state.EncodeProcess = StartEncode(job);
|
||||
return state;
|
||||
}
|
||||
|
||||
public FrameProcessingResult GetNextProcessedFrame(IFrameProcessingState processorState, CancellationToken token)
|
||||
{
|
||||
return new FrameProcessingResult(null, [], null);
|
||||
}
|
||||
|
||||
public void FinishSegment(IFrameProcessingState processorState)
|
||||
{
|
||||
var state = (State)processorState;
|
||||
|
||||
try
|
||||
{
|
||||
if (state.EncodeProcess != null && !state.EncodeProcess.HasExited)
|
||||
state.EncodeProcess.WaitForExit();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public async Task ProcessSegment(
|
||||
SingleTask job,
|
||||
Action<FrameProcessingResult>? onFrameProcessed,
|
||||
CancellationToken token)
|
||||
{
|
||||
var state = (State)InitSegment(job, token);
|
||||
|
||||
var p = state.EncodeProcess;
|
||||
if (p != null)
|
||||
await p.WaitForExitAsync(token);
|
||||
|
||||
FinishSegment(state);
|
||||
|
||||
ClearProgress(job.OutputFileName);
|
||||
|
||||
if (p != null && p.ExitCode != 0)
|
||||
LogError($"Segment {job.OutputFileName} FFmpeg passthrough failed");
|
||||
else
|
||||
LogInfo($"Segment {job.OutputFileName} passthrough completed");
|
||||
}
|
||||
|
||||
private Process StartEncode(SingleTask job)
|
||||
{
|
||||
var inputFile = job.Job.InputFile;
|
||||
var outputFile = job.OutputFileName;
|
||||
var start = job.SegmentStart;
|
||||
var length = job.SegmentLength;
|
||||
|
||||
var args =
|
||||
$"-ss {start.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"-i \"{inputFile}\" " +
|
||||
$"-t {length.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"-c copy {string.Join(" ", job.Job.Passthrough)} " +
|
||||
$"\"{outputFile}\" -y";
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "ffmpeg",
|
||||
Arguments = args,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
return Process.Start(psi) ?? throw new Exception("Failed to start ffmpeg passthrough.");
|
||||
}
|
||||
}
|
||||
142
splitter-cli/RotatingSplitter.cs
Normal file
142
splitter-cli/RotatingSplitter.cs
Normal file
@ -0,0 +1,142 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Xml.Linq;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace splitter;
|
||||
|
||||
public sealed class RotatingSplitter : LoggingBase, ISegmentProcessor
|
||||
{
|
||||
private sealed class State : IFrameProcessingState
|
||||
{
|
||||
public Process? EncodeProcess { get; set; }
|
||||
|
||||
public string InputFile { get; }
|
||||
public string OutputFile { get; }
|
||||
public double Start { get; }
|
||||
public double Length { get; }
|
||||
public int Rotate { get; }
|
||||
public VideoInfo Info { get; }
|
||||
public string[] Passthrough { get; }
|
||||
|
||||
public State(SingleTask job)
|
||||
{
|
||||
InputFile = job.Job.InputFile;
|
||||
OutputFile = job.OutputFileName;
|
||||
Start = job.SegmentStart;
|
||||
Length = job.SegmentLength;
|
||||
Rotate = job.Job.Rotate ?? 0;
|
||||
Info = job.Info;
|
||||
Passthrough = job.Job.Passthrough;
|
||||
}
|
||||
}
|
||||
|
||||
public RotatingSplitter(int segmentNo, ILogger logger)
|
||||
: base(logger, segmentNo)
|
||||
{
|
||||
}
|
||||
|
||||
public IFrameProcessingState InitSegment(SingleTask job, CancellationToken token)
|
||||
{
|
||||
var state = new State(job);
|
||||
state.EncodeProcess = StartEncode(job, state.Info, state.Rotate);
|
||||
return state;
|
||||
}
|
||||
|
||||
public FrameProcessingResult GetNextProcessedFrame(IFrameProcessingState processorState, CancellationToken token)
|
||||
{
|
||||
return new FrameProcessingResult(null, [], null);
|
||||
}
|
||||
|
||||
public void FinishSegment(IFrameProcessingState processorState)
|
||||
{
|
||||
var state = (State)processorState;
|
||||
|
||||
try
|
||||
{
|
||||
if (state.EncodeProcess != null && !state.EncodeProcess.HasExited)
|
||||
state.EncodeProcess.WaitForExit();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public async Task ProcessSegment(
|
||||
SingleTask job,
|
||||
Action<FrameProcessingResult>? onFrameProcessed,
|
||||
CancellationToken token)
|
||||
{
|
||||
DrawProgress(Path.GetFileName(job.OutputFileName), 0, TimeSpan.FromSeconds(10), 60);
|
||||
|
||||
var state = (State)InitSegment(job, token);
|
||||
|
||||
var p = state.EncodeProcess;
|
||||
if (p != null)
|
||||
await p.WaitForExitAsync(token);
|
||||
|
||||
FinishSegment(state);
|
||||
|
||||
ClearProgress(job.OutputFileName);
|
||||
|
||||
if (p != null && p.ExitCode != 0)
|
||||
LogError($"Segment {job.OutputFileName} FFmpeg rotation failed");
|
||||
else
|
||||
LogInfo($"Segment {job.OutputFileName} rotation completed");
|
||||
}
|
||||
|
||||
private Process StartEncode(SingleTask job, VideoInfo info, int rotate)
|
||||
{
|
||||
var inputFile = job.Job.InputFile;
|
||||
var outputFile = job.OutputFileName;
|
||||
var start = job.SegmentStart;
|
||||
var length = job.SegmentLength;
|
||||
|
||||
var rotation = GetRotationFilter(rotate);
|
||||
|
||||
// Rotation only. No SAR/DAR manipulation.
|
||||
var vfArg = $"-vf \"{rotation}\" ";
|
||||
|
||||
var args =
|
||||
$"-ss {start.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"-i \"{inputFile}\" " +
|
||||
$"-t {length.ToString(CultureInfo.InvariantCulture)} " +
|
||||
vfArg +
|
||||
"-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,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
var p = Process.Start(psi) ?? throw new Exception("Failed to start ffmpeg encode.");
|
||||
|
||||
// Drain stderr and log
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
string? line;
|
||||
while ((line = p.StandardError.ReadLine()) != null)
|
||||
LogInfo(line);
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
private static string GetRotationFilter(int degrees) =>
|
||||
degrees switch
|
||||
{
|
||||
90 => "transpose=1",
|
||||
180 => "rotate=PI",
|
||||
270 => "transpose=2",
|
||||
_ => "transpose=1"
|
||||
};
|
||||
}
|
||||
@ -1,279 +0,0 @@
|
||||
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 FrameProcessingResult GetNextProcessedFrame(IFrameProcessingState processorState, CancellationToken token)
|
||||
{
|
||||
var state = (State)processorState;
|
||||
|
||||
if (state.DecodeStdout == null)
|
||||
return new FrameProcessingResult(null, [], 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 new FrameProcessingResult(null, [], null);
|
||||
|
||||
var mat = new Mat(h, w, MatType.CV_8UC3);
|
||||
System.Runtime.InteropServices.Marshal.Copy(buffer, 0, mat.Data, bytes);
|
||||
|
||||
return new FrameProcessingResult(mat, [], null);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 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, Action<FrameProcessingResult>? onFrameProcessed, 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 res = GetNextProcessedFrame(state, token);
|
||||
var frame = res.Image;
|
||||
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);
|
||||
|
||||
onFrameProcessed?.Invoke(res);
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
@ -219,7 +219,7 @@ public sealed class TrackingSplitter : LoggingBase, ISegmentProcessor
|
||||
// INTERNAL HELPERS
|
||||
// ============================================================
|
||||
|
||||
private object CreateFrameState(SingleTask job)
|
||||
private FrameProcessingState CreateFrameState(SingleTask job)
|
||||
{
|
||||
var w = job.Info.Width;
|
||||
var h = job.Info.Height;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user