Separated pass-through and rotating splitters. Rotation fixed.

This commit is contained in:
Alexander Shabarshov 2026-07-05 12:17:27 +01:00
parent 3457aa6816
commit e986964f00
7 changed files with 260 additions and 287 deletions

View File

@ -2,7 +2,6 @@
public sealed class BufferPool : IBufferPool public sealed class BufferPool : IBufferPool
{ {
private readonly int _capacity;
public sealed class Entry public sealed class Entry
{ {
@ -18,10 +17,14 @@ public sealed class BufferPool : IBufferPool
Bgr = new byte[w * h * 3]; Bgr = new byte[w * h * 3];
Bgra = new byte[w * h * 4]; 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 Dictionary<(int w, int h), LinkedListNode<Entry>> _map;
private readonly LinkedList<Entry> _lru; private readonly LinkedList<Entry> _lru;
private readonly int _capacity;
private readonly Lock _lock = new();
public BufferPool() public BufferPool()
{ {

View File

@ -8,7 +8,7 @@
x:DataType="vm:PreviewPaneViewModel"> x:DataType="vm:PreviewPaneViewModel">
<Border Background="#202020" Padding="10"> <Border Background="#202020" Padding="10">
<Grid RowDefinitions="*,Auto,Auto"> <Grid RowDefinitions="*,Auto">
<local:PreviewCanvas <local:PreviewCanvas
Grid.Row="0" Grid.Row="0"
@ -19,7 +19,7 @@
DetectAbove="{Binding DetectAbove, Mode=TwoWay}" DetectAbove="{Binding DetectAbove, Mode=TwoWay}"
DetectId="{Binding Selected.DetectId, Mode=TwoWay}" DetectId="{Binding Selected.DetectId, Mode=TwoWay}"
/> />
<!--
<Grid Grid.Row="1" <Grid Grid.Row="1"
ColumnDefinitions="Auto" ColumnDefinitions="Auto"
Margin="0,10,0,0"> Margin="0,10,0,0">
@ -37,8 +37,8 @@
</Button> </Button>
</Grid> </Grid>
-->
<Grid Grid.Row="2" <Grid Grid.Row="1"
ColumnDefinitions="Auto,*,Auto" ColumnDefinitions="Auto,*,Auto"
Margin="0,10,0,0"> Margin="0,10,0,0">

View File

@ -57,7 +57,11 @@ public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcesso
return []; return [];
Func<int, ISegmentProcessor> processorFactory; 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 => processorFactory = i =>
{ {
@ -75,7 +79,7 @@ public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcesso
} }
else else
{ {
processorFactory = i => new SimpleSplitter(i, _logger); processorFactory = i => new PassthroughSplitter(i, _logger);
} }
var segmentsToUse = predefinedSegments; var segmentsToUse = predefinedSegments;

View 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.");
}
}

View 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"
};
}

View File

@ -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
};
}

View File

@ -219,7 +219,7 @@ public sealed class TrackingSplitter : LoggingBase, ISegmentProcessor
// INTERNAL HELPERS // INTERNAL HELPERS
// ============================================================ // ============================================================
private object CreateFrameState(SingleTask job) private FrameProcessingState CreateFrameState(SingleTask job)
{ {
var w = job.Info.Width; var w = job.Info.Width;
var h = job.Info.Height; var h = job.Info.Height;