mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-22 00:22:01 +00:00
Fixed audio/video sync. Single ffprobe call.
This commit is contained in:
parent
1dac08d21a
commit
44d817f17f
@ -6,5 +6,5 @@ namespace splitter;
|
|||||||
|
|
||||||
public interface ISegmentProcessor
|
public interface ISegmentProcessor
|
||||||
{
|
{
|
||||||
Task ProcessSegment( string inputFile, string outputFile, double start, double length, string[] ffmpegPassthroughParameters);
|
Task ProcessSegment( string inputFile, string outputFile, double start, double length, int videoWidth, int videoHeight, double fps, string[] ffmpegPassthroughParameters);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,17 +9,7 @@ namespace splitter;
|
|||||||
|
|
||||||
public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger, segmentNo), ISegmentProcessor
|
public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger, segmentNo), ISegmentProcessor
|
||||||
{
|
{
|
||||||
public async Task ProcessSegment(string inputFile, string outputFile, double start, double length, string[] passthrough)
|
public async Task ProcessSegment(string inputFile, string outputFile, double start, double length, int videoWidth, int videoHeight, double fps, string[] passthrough)
|
||||||
{
|
|
||||||
RunFFmpegSegment(inputFile, outputFile, start, length, passthrough);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RunFFmpegSegment(
|
|
||||||
string inputFile,
|
|
||||||
string outputFile,
|
|
||||||
double start,
|
|
||||||
double length,
|
|
||||||
string[] passthrough)
|
|
||||||
{
|
{
|
||||||
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
|
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
|
||||||
|
|
||||||
|
|||||||
@ -25,8 +25,10 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable
|
|||||||
private readonly Dictionary<int, ProgressEntry> _progress = new();
|
private readonly Dictionary<int, ProgressEntry> _progress = new();
|
||||||
|
|
||||||
private readonly CancellationTokenSource _cts = new();
|
private readonly CancellationTokenSource _cts = new();
|
||||||
private Task? _uiTask;
|
private Task? _uiTask;
|
||||||
private Task? _inputTask;
|
private Task? _inputTask;
|
||||||
|
private int _numberOfProcesses = 1;
|
||||||
|
private const int _maxLogEntries = 500;
|
||||||
|
|
||||||
// Public configuration
|
// Public configuration
|
||||||
public string Title { get; set; } = string.Empty;
|
public string Title { get; set; } = string.Empty;
|
||||||
@ -51,14 +53,6 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int _numberOfProcesses = 1;
|
|
||||||
|
|
||||||
private const int MaxLogEntries = 500;
|
|
||||||
|
|
||||||
public SpectreConsoleLogger()
|
|
||||||
{
|
|
||||||
NumberOfProcesses = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- ILogger ----
|
// ---- ILogger ----
|
||||||
|
|
||||||
@ -93,8 +87,8 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable
|
|||||||
{
|
{
|
||||||
lock (_sync)
|
lock (_sync)
|
||||||
{
|
{
|
||||||
if (_logs.Count >= MaxLogEntries)
|
if (_logs.Count >= _maxLogEntries)
|
||||||
_logs.RemoveRange(0, _logs.Count - MaxLogEntries + 1);
|
_logs.RemoveRange(0, _logs.Count - _maxLogEntries + 1);
|
||||||
|
|
||||||
_logs.Add(new LogEntry(
|
_logs.Add(new LogEntry(
|
||||||
DateTime.Now,
|
DateTime.Now,
|
||||||
@ -137,7 +131,7 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable
|
|||||||
ctx.UpdateTarget(BuildRoot());
|
ctx.UpdateTarget(BuildRoot());
|
||||||
await Task.Delay(100, token);
|
await Task.Delay(100, token);
|
||||||
}
|
}
|
||||||
catch ( Exception ex )
|
catch ( Exception )
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using OpenCvSharp;
|
using OpenCvSharp;
|
||||||
@ -9,21 +10,16 @@ namespace splitter;
|
|||||||
|
|
||||||
public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||||
{
|
{
|
||||||
private readonly int _cropWidth;
|
|
||||||
private readonly int _cropHeight;
|
|
||||||
private readonly bool _debugOverlay;
|
|
||||||
private readonly bool _plainText;
|
|
||||||
|
|
||||||
private readonly IObjectDetector _detector;
|
private readonly IObjectDetector _detector;
|
||||||
private readonly SingleJob _cmd;
|
private readonly SingleJob _cmd;
|
||||||
|
|
||||||
public TrackingSplitter(int segmentNo, int cropWidth, int cropHeight, bool debugOverlay, bool plainText, IObjectDetector detector, SingleJob cmd, ILogger logger)
|
public TrackingSplitter(
|
||||||
: base(logger, segmentNo)
|
int progressLine,
|
||||||
|
IObjectDetector detector,
|
||||||
|
SingleJob cmd,
|
||||||
|
ILogger logger)
|
||||||
|
: base(logger, progressLine)
|
||||||
{
|
{
|
||||||
_cropWidth = cropWidth;
|
|
||||||
_cropHeight = cropHeight;
|
|
||||||
_debugOverlay = debugOverlay;
|
|
||||||
_plainText = plainText;
|
|
||||||
_detector = detector;
|
_detector = detector;
|
||||||
_cmd = cmd;
|
_cmd = cmd;
|
||||||
}
|
}
|
||||||
@ -34,146 +30,287 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
d.Dispose();
|
d.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ProcessSegment(string inputFile, string outputFile, double start, double length, string[] ffmpegPassthroughParameters)
|
public async Task ProcessSegment(
|
||||||
|
string inputFile,
|
||||||
|
string outputFile,
|
||||||
|
double start,
|
||||||
|
double length,
|
||||||
|
int videoWidth, int videoHeight, double fps,
|
||||||
|
string[] ffmpegPassthroughParameters)
|
||||||
{
|
{
|
||||||
using var capture = new VideoCapture(inputFile);
|
var name = Path.GetFileNameWithoutExtension(outputFile);
|
||||||
if (!capture.IsOpened())
|
|
||||||
|
// 1) Probe source video
|
||||||
|
if (videoWidth <= 0 || videoHeight <= 0 || fps <= 0)
|
||||||
{
|
{
|
||||||
LogError($"{Path.GetFileName(inputFile)}: Cannot open video");
|
LogError($"{name}: ffprobe failed to get metadata");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var name = Path.GetFileNameWithoutExtension(outputFile);
|
if (_cmd.Crop == null)
|
||||||
var skip = TimeSpan.FromSeconds(start);
|
{
|
||||||
var duration = TimeSpan.FromSeconds(length);
|
LogError($"{name}: Crop parameters are required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
capture.Set(VideoCaptureProperties.PosMsec, start);
|
var encWidth = _cmd.Debug ? videoWidth : _cmd.Crop.Value.width;
|
||||||
|
var encHeight = _cmd.Debug ? videoHeight : _cmd.Crop.Value.height;
|
||||||
|
|
||||||
var videoWidth = (int)capture.Get(VideoCaptureProperties.FrameWidth);
|
LogInfo($"{name}: src={videoWidth}x{videoHeight} @ {fps:F3}fps, seg=[{start:F3},{length:F3}] enc={encWidth}x{encHeight}");
|
||||||
var videoHeight = (int)capture.Get(VideoCaptureProperties.FrameHeight);
|
|
||||||
var fps = capture.Get(VideoCaptureProperties.Fps);
|
|
||||||
var totalFrames = (int)(length * fps);
|
|
||||||
|
|
||||||
var originalCropWidth = _cropWidth;
|
// 2) Start FFmpeg decode (video only → raw BGR24 to stdout)
|
||||||
var originalCropHeight = _cropHeight;
|
var decode = StartFfmpegDecode(inputFile, start, length);
|
||||||
|
using var decodeStdout = decode.StandardOutput.BaseStream;
|
||||||
|
|
||||||
LogInfo($"{Path.GetFileName(outputFile)}:: [TrackingSplitter] skip={skip}, duration={duration}, fps={fps}, totalFrames={totalFrames}");
|
// 3) Start FFmpeg encode (video from stdin + audio from original)
|
||||||
|
var encode = StartFfmpegEncode(
|
||||||
var encWidth = _debugOverlay ? videoWidth : originalCropWidth;
|
|
||||||
var encHeight = _debugOverlay ? videoHeight : originalCropHeight;
|
|
||||||
|
|
||||||
var ffmpeg = StartFfmpegNvenc(
|
|
||||||
inputFile,
|
inputFile,
|
||||||
outputFile,
|
outputFile,
|
||||||
|
start,
|
||||||
|
length,
|
||||||
encWidth,
|
encWidth,
|
||||||
encHeight,
|
encHeight,
|
||||||
fps,
|
fps,
|
||||||
skip,
|
|
||||||
ffmpegPassthroughParameters);
|
ffmpegPassthroughParameters);
|
||||||
|
|
||||||
using var stdin = ffmpeg.StandardInput.BaseStream;
|
using var encodeStdin = encode.StandardInput.BaseStream;
|
||||||
|
|
||||||
using var frame = new Mat();
|
// Separate input/output sizes and buffers
|
||||||
using var outputBgr = new Mat(encHeight, encWidth, MatType.CV_8UC3);
|
var inBytes = videoWidth * videoHeight * 3;
|
||||||
|
var outBytes = encWidth * encHeight * 3;
|
||||||
|
|
||||||
var frameBytes = encWidth * encHeight * 3;
|
var inBuffer = new byte[inBytes];
|
||||||
var videoBuffer = new byte[frameBytes];
|
var outBuffer = new byte[outBytes];
|
||||||
|
|
||||||
|
using var frameMat = new Mat(videoHeight, videoWidth, MatType.CV_8UC3);
|
||||||
|
using var outMat = new Mat(encHeight, encWidth, MatType.CV_8UC3);
|
||||||
|
|
||||||
var kalman = new KalmanTracker();
|
var kalman = new KalmanTracker();
|
||||||
// initial reset is now done inside CameraController
|
|
||||||
|
|
||||||
var camera = new CameraController(
|
var camera = new CameraController(
|
||||||
videoWidth,
|
videoWidth,
|
||||||
videoHeight,
|
videoHeight,
|
||||||
originalCropWidth,
|
_cmd.Crop.Value.width,
|
||||||
originalCropHeight,
|
_cmd.Crop.Value.height,
|
||||||
kalman,
|
kalman,
|
||||||
_cmd
|
_cmd);
|
||||||
);
|
|
||||||
|
|
||||||
var startTime = DateTime.UtcNow;
|
var startTime = DateTime.UtcNow;
|
||||||
|
var totalFrames = (int)Math.Round(length * fps);
|
||||||
|
var frameIndex = 0;
|
||||||
|
|
||||||
for (var i = 0; i < totalFrames; i++)
|
while (frameIndex < totalFrames)
|
||||||
{
|
{
|
||||||
if (!capture.Read(frame) || frame.Empty())
|
frameIndex++;
|
||||||
|
|
||||||
|
var read = ReadExact(decodeStdout, inBuffer, 0, inBytes);
|
||||||
|
if (read != inBytes)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
Rect? objectBox = null;
|
// input frame → Mat
|
||||||
Point2f? objectCenter = null;
|
Marshal.Copy(inBuffer, 0, frameMat.Data, inBytes);
|
||||||
|
|
||||||
var objects = _detector.DetectAll(frame, videoWidth, videoHeight);
|
var objects = _detector.DetectAll(frameMat, videoWidth, videoHeight);
|
||||||
var primary = SelectTrackedObject(objects, kalman.LastMeasurement);
|
var primary = SelectTrackedObject(objects, kalman.LastMeasurement);
|
||||||
|
|
||||||
camera.Update(primary);
|
camera.Update(primary);
|
||||||
|
var roi = camera.Roi;
|
||||||
|
|
||||||
objectBox = camera.ObjectBox;
|
if (_cmd.Debug)
|
||||||
objectCenter = camera.ObjectCenter;
|
|
||||||
|
|
||||||
var smoothedCenter = camera.SmoothedCenter;
|
|
||||||
var cameraCenter = camera.CameraCenter;
|
|
||||||
var state = camera.State;
|
|
||||||
var lostFrames = camera.LostFrames;
|
|
||||||
var roi = camera.Roi;
|
|
||||||
|
|
||||||
if (_debugOverlay)
|
|
||||||
{
|
{
|
||||||
if (objectBox.HasValue)
|
DrawDebug(frameMat, objects, camera, kalman);
|
||||||
{
|
frameMat.CopyTo(outMat);
|
||||||
var fb = objectBox.Value;
|
|
||||||
Cv2.Rectangle(frame,
|
|
||||||
new Rect(fb.X, fb.Y, fb.Width, fb.Height),
|
|
||||||
Scalar.LimeGreen, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
Cv2.Circle(frame,
|
|
||||||
new Point((int)smoothedCenter.X, (int)smoothedCenter.Y),
|
|
||||||
6, Scalar.LimeGreen, -1);
|
|
||||||
|
|
||||||
Cv2.Rectangle(frame, roi,
|
|
||||||
objectCenter.HasValue ? Scalar.Yellow : Scalar.Red, 3);
|
|
||||||
|
|
||||||
DrawText(frame, $"Faces: {objects.Count}", 20, 40, Scalar.White);
|
|
||||||
DrawText(frame, $"LostFrames: {lostFrames}", 20, 70, Scalar.White);
|
|
||||||
DrawText(frame, $"Noise: {kalman.CurrentNoise:F3}", 20, 130, Scalar.White);
|
|
||||||
DrawText(frame, $"Camera: {cameraCenter.X:F1},{cameraCenter.Y:F1}", 20, 160, Scalar.White);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_debugOverlay)
|
|
||||||
{
|
|
||||||
frame.CopyTo(outputBgr);
|
|
||||||
Marshal.Copy(outputBgr.Data, videoBuffer, 0, frameBytes);
|
|
||||||
stdin.Write(videoBuffer, 0, frameBytes);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
using var cropped = new Mat(frame, roi);
|
using var cropped = new Mat(frameMat, roi);
|
||||||
cropped.CopyTo(outputBgr);
|
cropped.CopyTo(outMat);
|
||||||
|
|
||||||
Marshal.Copy(outputBgr.Data, videoBuffer, 0, frameBytes);
|
|
||||||
stdin.Write(videoBuffer, 0, frameBytes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// output Mat → outBuffer
|
||||||
|
Marshal.Copy(outMat.Data, outBuffer, 0, outBytes);
|
||||||
|
encodeStdin.Write(outBuffer, 0, outBytes);
|
||||||
|
|
||||||
var elapsed = DateTime.UtcNow - startTime;
|
var elapsed = DateTime.UtcNow - startTime;
|
||||||
var progress = (double)i / totalFrames;
|
var progress = totalFrames > 0 ? (double)frameIndex / totalFrames : 0.0;
|
||||||
var speed = i > 0 ? (i / elapsed.TotalSeconds)/fps : 0.0;
|
var speed = elapsed.TotalSeconds > 0 ? (frameIndex / elapsed.TotalSeconds) / fps : 0.0;
|
||||||
var remainingFrames = totalFrames - i;
|
var remainingFrames = Math.Max(totalFrames - frameIndex, 0);
|
||||||
var etaSeconds = speed > 0 ? remainingFrames / speed : 0;
|
var etaSeconds = speed > 0 ? remainingFrames / speed : 0.0;
|
||||||
var eta = TimeSpan.FromSeconds(etaSeconds);
|
var eta = TimeSpan.FromSeconds(etaSeconds);
|
||||||
|
|
||||||
DrawProgress(name, progress, eta, speed);
|
DrawProgress(name, progress, eta, speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
stdin.Flush();
|
encodeStdin.Flush();
|
||||||
stdin.Close();
|
|
||||||
|
|
||||||
await ffmpeg.WaitForExitAsync();
|
// loop finished
|
||||||
|
|
||||||
|
encodeStdin.Flush();
|
||||||
|
encodeStdin.Close(); // must happen before waiting encode
|
||||||
|
|
||||||
|
await encode.WaitForExitAsync();
|
||||||
|
|
||||||
|
// belt-and-braces: if decode is still alive, kill it
|
||||||
|
try { if (!decode.HasExited) decode.Kill(entireProcessTree: true); } catch { }
|
||||||
|
try { if (!decode.HasExited) await decode.WaitForExitAsync(); } catch { }
|
||||||
|
|
||||||
ClearProgress();
|
ClearProgress();
|
||||||
|
|
||||||
if (ffmpeg.ExitCode != 0)
|
|
||||||
LogError($"{Path.GetFileName(outputFile)}: Segment {name} FFmpeg encoding failed");
|
if (encode.ExitCode != 0)
|
||||||
|
LogError($"{name}: FFmpeg encoding failed");
|
||||||
else
|
else
|
||||||
LogInfo($"{Path.GetFileName(outputFile)}: Segment {name} processing completed");
|
LogInfo($"{name}: Segment processing completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------- FFmpeg decode / encode ----------
|
||||||
|
|
||||||
|
private Process StartFfmpegDecode(string inputFile, double start, double length)
|
||||||
|
{
|
||||||
|
var ss = start.ToString("0.###", CultureInfo.InvariantCulture);
|
||||||
|
var t = length.ToString("0.###", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
var args =
|
||||||
|
$"-i \"{inputFile}\" -ss {ss} -t {t} " +
|
||||||
|
"-an -sn " +
|
||||||
|
"-vf format=bgr24 " +
|
||||||
|
"-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);
|
||||||
|
|
||||||
|
if (_cmd.PlainText)
|
||||||
|
{
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string? line;
|
||||||
|
while ((line = p.StandardError.ReadLine()) != null)
|
||||||
|
if (_cmd.PlainText)
|
||||||
|
LogInfo($"[ffmpeg-decode] {fileName}: {line}");
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Process StartFfmpegEncode(
|
||||||
|
string inputFile,
|
||||||
|
string outputFile,
|
||||||
|
double start,
|
||||||
|
double length,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
double fps,
|
||||||
|
string[] passthrough)
|
||||||
|
{
|
||||||
|
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
|
||||||
|
var fpsStr = fps.ToString("0.###", CultureInfo.InvariantCulture);
|
||||||
|
var ss = start.ToString("0.###", CultureInfo.InvariantCulture);
|
||||||
|
var t = length.ToString("0.###", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
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 " +
|
||||||
|
"-c:a aac -b:a 192k " +
|
||||||
|
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(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string? line;
|
||||||
|
while ((line = p.StandardError.ReadLine()) != null)
|
||||||
|
{
|
||||||
|
if (_cmd.PlainText)
|
||||||
|
LogInfo($"[ffmpeg-encode] {fileName}: {line}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
});
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- helpers ----------
|
||||||
|
|
||||||
|
private static int ReadExact(Stream s, byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
var total = 0;
|
||||||
|
while (total < count)
|
||||||
|
{
|
||||||
|
var read = s.Read(buffer, offset + total, count - total);
|
||||||
|
if (read <= 0)
|
||||||
|
break;
|
||||||
|
total += read;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawDebug(
|
||||||
|
Mat frame,
|
||||||
|
System.Collections.Generic.List<(Rect box, Point2f center)> objects,
|
||||||
|
CameraController camera,
|
||||||
|
KalmanTracker kalman)
|
||||||
|
{
|
||||||
|
if (camera.ObjectBox.HasValue)
|
||||||
|
{
|
||||||
|
var fb = camera.ObjectBox.Value;
|
||||||
|
Cv2.Rectangle(frame, fb, Scalar.LimeGreen, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cv2.Circle(frame,
|
||||||
|
new Point((int)camera.SmoothedCenter.X, (int)camera.SmoothedCenter.Y),
|
||||||
|
6, Scalar.LimeGreen, -1);
|
||||||
|
|
||||||
|
Cv2.Rectangle(frame, camera.Roi,
|
||||||
|
camera.ObjectCenter.HasValue ? Scalar.Yellow : Scalar.Red, 3);
|
||||||
|
|
||||||
|
DrawText(frame, $"Faces: {objects.Count}", 20, 40, Scalar.White);
|
||||||
|
DrawText(frame, $"LostFrames: {camera.LostFrames}", 20, 70, Scalar.White);
|
||||||
|
DrawText(frame, $"Noise: {kalman.CurrentNoise:F3}", 20, 130, Scalar.White);
|
||||||
|
DrawText(frame, $"Camera: {camera.CameraCenter.X:F1},{camera.CameraCenter.Y:F1}", 20, 160, Scalar.White);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawText(Mat img, string text, int x, int y, Scalar color)
|
||||||
|
{
|
||||||
|
Cv2.PutText(img, text, new Point(x, y),
|
||||||
|
HersheyFonts.HersheySimplex, 0.6, color, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private (Rect box, Point2f center)? SelectTrackedObject(
|
private (Rect box, Point2f center)? SelectTrackedObject(
|
||||||
@ -185,7 +322,6 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
|
|
||||||
if (!previousCenter.HasValue)
|
if (!previousCenter.HasValue)
|
||||||
{
|
{
|
||||||
// Largest area
|
|
||||||
var bestIndex = 0;
|
var bestIndex = 0;
|
||||||
var bestArea = float.MinValue;
|
var bestArea = float.MinValue;
|
||||||
|
|
||||||
@ -204,8 +340,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Closest to previous center
|
var prev = previousCenter.Value;
|
||||||
var prev = previousCenter.Value;
|
|
||||||
var bestIndex = 0;
|
var bestIndex = 0;
|
||||||
var bestDist2 = float.MaxValue;
|
var bestDist2 = float.MaxValue;
|
||||||
|
|
||||||
@ -226,64 +361,4 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
return foundObjects[bestIndex];
|
return foundObjects[bestIndex];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Process StartFfmpegNvenc(
|
|
||||||
string srcFileName,
|
|
||||||
string destFileName,
|
|
||||||
int width,
|
|
||||||
int height,
|
|
||||||
double fps,
|
|
||||||
TimeSpan skip,
|
|
||||||
string[] passthrough)
|
|
||||||
{
|
|
||||||
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
|
|
||||||
var skipSeconds = skip.TotalSeconds.ToString("0.###", CultureInfo.InvariantCulture);
|
|
||||||
var fpsStr = fps.ToString("0.###", CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
var args =
|
|
||||||
"-y " +
|
|
||||||
$"-f rawvideo -pix_fmt bgr24 -s {width}x{height} -r {fpsStr} -i - " +
|
|
||||||
$"-ss {skipSeconds} -i \"{srcFileName}\" " +
|
|
||||||
"-map 0:v:0 -map 1:a:0? -shortest " +
|
|
||||||
"-c:v h264_nvenc -preset p4 -b:v 8M -pix_fmt yuv420p " +
|
|
||||||
"-c:a aac -b:a 192k " +
|
|
||||||
pass + $" \"{destFileName}\"";
|
|
||||||
|
|
||||||
var psi = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = "ffmpeg",
|
|
||||||
Arguments = args,
|
|
||||||
RedirectStandardInput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
UseShellExecute = false,
|
|
||||||
CreateNoWindow = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var process = new Process { StartInfo = psi };
|
|
||||||
process.Start();
|
|
||||||
|
|
||||||
_ = Task.Run(() =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string? line;
|
|
||||||
while ((line = process.StandardError.ReadLine()) != null)
|
|
||||||
{
|
|
||||||
if (_plainText)
|
|
||||||
Console.WriteLine($"[ffmpeg] {line}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
});
|
|
||||||
|
|
||||||
return process;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DrawText(Mat img, string text, int x, int y, Scalar color)
|
|
||||||
{
|
|
||||||
Cv2.PutText(img, text, new Point(x, y),
|
|
||||||
HersheyFonts.HersheySimplex, 0.6, color, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
157
splitter.cs
157
splitter.cs
@ -16,6 +16,9 @@ static class Program
|
|||||||
int TotalSegments,
|
int TotalSegments,
|
||||||
double SegmentStart,
|
double SegmentStart,
|
||||||
double SegmentLength,
|
double SegmentLength,
|
||||||
|
int VideoWidth,
|
||||||
|
int VideoHeight,
|
||||||
|
double VideoFps,
|
||||||
Func<int, ISegmentProcessor> ProcessorFactory
|
Func<int, ISegmentProcessor> ProcessorFactory
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -38,7 +41,6 @@ static class Program
|
|||||||
var logger = new SpectreConsoleLogger
|
var logger = new SpectreConsoleLogger
|
||||||
{
|
{
|
||||||
Title = "Splitter",
|
Title = "Splitter",
|
||||||
NumberOfProcesses = cmd.Master.SingleThreaded ? 1 : Math.Max(1, Environment.ProcessorCount / 2) + 1
|
|
||||||
};
|
};
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
@ -89,7 +91,7 @@ static class Program
|
|||||||
job.Mask ??= $"{baseName}_seg%03d.mp4";
|
job.Mask ??= $"{baseName}_seg%03d.mp4";
|
||||||
LogInfo($"{baseName}: Reading duration via ffprobe...");
|
LogInfo($"{baseName}: Reading duration via ffprobe...");
|
||||||
|
|
||||||
var duration = GetDuration(job.InputFile);
|
(double duration, int width, int height, double fps) = ProbeVideo(job.InputFile);
|
||||||
if (duration <= 0)
|
if (duration <= 0)
|
||||||
{
|
{
|
||||||
LogError($"{baseName}: Could not read duration.");
|
LogError($"{baseName}: Could not read duration.");
|
||||||
@ -115,20 +117,17 @@ static class Program
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cmd.Master.EstimateOnly)
|
if (cmd.Master.EstimateOnly)
|
||||||
{
|
|
||||||
LogInfo("=== ESTIMATE MODE ===");
|
LogInfo("=== ESTIMATE MODE ===");
|
||||||
LogInfo($"{baseName}: Total duration: {duration:F2}s");
|
|
||||||
LogInfo($"{baseName}: Target duration: {target:F2}s");
|
|
||||||
LogInfo($"{baseName}: Segments: {segments}");
|
|
||||||
LogInfo(job.ForceFixed
|
|
||||||
? $"{baseName}: Fixed segment length: {segmentLength:F2}s (last may be shorter)"
|
|
||||||
: $"{baseName}: Equalized segment length: {segmentLength:F2}s");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
LogInfo($"{baseName}: Duration: {duration:F2}s");
|
LogInfo($"{baseName}: Duration {duration:F2}s, {width}x{height} @ {fps:F3}fps");
|
||||||
|
LogInfo($"{baseName}: Target duration: {target:F2}s");
|
||||||
LogInfo($"{baseName}: Segments: {segments}");
|
LogInfo($"{baseName}: Segments: {segments}");
|
||||||
LogInfo($"{baseName}: Equal segment length: {segmentLength:F3}s");
|
LogInfo(job.ForceFixed
|
||||||
|
? $"{baseName}: Fixed segment length: {segmentLength:F2}s (last may be shorter)"
|
||||||
|
: $"{baseName}: Equalized segment length: {segmentLength:F2}s");
|
||||||
|
|
||||||
|
if (cmd.Master.EstimateOnly)
|
||||||
|
return [];
|
||||||
|
|
||||||
Func<int, ISegmentProcessor> processorFactory;
|
Func<int, ISegmentProcessor> processorFactory;
|
||||||
if (job.Crop != null)
|
if (job.Crop != null)
|
||||||
@ -141,7 +140,7 @@ static class Program
|
|||||||
"body" => new YoloOnnxObjectDetector(_logger),
|
"body" => new YoloOnnxObjectDetector(_logger),
|
||||||
_ => throw new InvalidOperationException($"Unknown detector: {job.Detect}")
|
_ => throw new InvalidOperationException($"Unknown detector: {job.Detect}")
|
||||||
};
|
};
|
||||||
return new TrackingSplitter(i, job.Crop.Value.width, job.Crop.Value.height, cmd.Master.Debug, cmd.Master.PlainText, detector, job, _logger);
|
return new TrackingSplitter(i, detector, job, _logger);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -160,7 +159,10 @@ static class Program
|
|||||||
SegmentLength : (i == segments - 1)
|
SegmentLength : (i == segments - 1)
|
||||||
? Math.Max(0.1, duration - i * segmentLength)
|
? Math.Max(0.1, duration - i * segmentLength)
|
||||||
: segmentLength,
|
: segmentLength,
|
||||||
ProcessorFactory : processorFactory
|
ProcessorFactory : processorFactory,
|
||||||
|
VideoWidth : width,
|
||||||
|
VideoHeight : height,
|
||||||
|
VideoFps : fps
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.ToList();
|
.ToList();
|
||||||
@ -195,28 +197,6 @@ static class Program
|
|||||||
// ffprobe
|
// ffprobe
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
|
|
||||||
static double GetDuration(string inputFile)
|
|
||||||
{
|
|
||||||
var psi = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = "ffprobe",
|
|
||||||
Arguments = $"-v error -show_entries format=duration -of csv=p=0 \"{inputFile}\"",
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
UseShellExecute = false,
|
|
||||||
CreateNoWindow = true
|
|
||||||
};
|
|
||||||
|
|
||||||
using var proc = Process.Start(psi) ?? throw new Exception("Failed to start ffprobe.");
|
|
||||||
var output = proc.StandardOutput.ReadToEnd();
|
|
||||||
proc.WaitForExit();
|
|
||||||
|
|
||||||
if (output != null &&
|
|
||||||
double.TryParse(output, NumberStyles.Any, CultureInfo.InvariantCulture, out var duration))
|
|
||||||
return duration;
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
// Multi-threaded splitting
|
// Multi-threaded splitting
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
@ -252,15 +232,7 @@ static class Program
|
|||||||
while (!freeSlots.TryDequeue(out slot))
|
while (!freeSlots.TryDequeue(out slot))
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
|
|
||||||
await ProcessSegment(
|
await ProcessSegment(job,slot + 1);
|
||||||
job.ProcessorFactory,
|
|
||||||
job.Job.InputFile,
|
|
||||||
job.OutputFileName,
|
|
||||||
job.Job.Passthrough,
|
|
||||||
slot + 1, // <-- slot instead of SegmentIndex (+1 for totals)
|
|
||||||
job.SegmentStart,
|
|
||||||
job.SegmentLength
|
|
||||||
);
|
|
||||||
|
|
||||||
var processed = Interlocked.Increment(ref processedSegments);
|
var processed = Interlocked.Increment(ref processedSegments);
|
||||||
var elapsed = sw.Elapsed;
|
var elapsed = sw.Elapsed;
|
||||||
@ -291,25 +263,25 @@ static class Program
|
|||||||
{
|
{
|
||||||
foreach (var job in jobs)
|
foreach (var job in jobs)
|
||||||
{
|
{
|
||||||
await ProcessSegment(
|
await ProcessSegment(job, 0);
|
||||||
job.ProcessorFactory,
|
|
||||||
job.Job.InputFile,
|
|
||||||
job.OutputFileName,
|
|
||||||
job.Job.Passthrough,
|
|
||||||
job.SegmentIndex,
|
|
||||||
job.SegmentStart,
|
|
||||||
job.SegmentLength
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task ProcessSegment(Func<int, ISegmentProcessor> processorFactory, string inputFile, string outputFileName, string[] passthrough, int index, double start, double length)
|
private static async Task ProcessSegment(SingleTask t, int slot)
|
||||||
{
|
{
|
||||||
var processor = processorFactory(index);
|
var processor = t.ProcessorFactory(slot);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await processor.ProcessSegment(inputFile, outputFileName, start, length, passthrough);
|
await processor.ProcessSegment(
|
||||||
|
t.Job.InputFile,
|
||||||
|
t.OutputFileName,
|
||||||
|
t.SegmentStart,
|
||||||
|
t.SegmentLength,
|
||||||
|
t.VideoWidth,
|
||||||
|
t.VideoHeight,
|
||||||
|
t.VideoFps,
|
||||||
|
t.Job.Passthrough);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -345,4 +317,73 @@ static class Program
|
|||||||
return Path.Combine(folder, fileName);
|
return Path.Combine(folder, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static (double duration, int width, int height, double fps) ProbeVideo(string inputFile)
|
||||||
|
{
|
||||||
|
var args =
|
||||||
|
"-v error " +
|
||||||
|
"-select_streams v:0 " +
|
||||||
|
"-show_entries format=duration " +
|
||||||
|
"-show_entries stream=width,height,avg_frame_rate " +
|
||||||
|
"-of default=noprint_wrappers=1:nokey=0 " + // <-- IMPORTANT: include keys
|
||||||
|
$"\"{inputFile}\"";
|
||||||
|
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "ffprobe",
|
||||||
|
Arguments = args,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using var p = new Process { StartInfo = psi };
|
||||||
|
p.Start();
|
||||||
|
|
||||||
|
var duration = -1.0;
|
||||||
|
var width = 0;
|
||||||
|
var height = 0;
|
||||||
|
var fps = 0.0;
|
||||||
|
|
||||||
|
while (!p.StandardOutput.EndOfStream)
|
||||||
|
{
|
||||||
|
var line = p.StandardOutput.ReadLine()?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (line.StartsWith("duration="))
|
||||||
|
{
|
||||||
|
var v = line.Substring("duration=".Length);
|
||||||
|
double.TryParse(v, NumberStyles.Any, CultureInfo.InvariantCulture, out duration);
|
||||||
|
}
|
||||||
|
else if (line.StartsWith("width="))
|
||||||
|
{
|
||||||
|
var v = line.Substring("width=".Length);
|
||||||
|
int.TryParse(v, NumberStyles.Integer, CultureInfo.InvariantCulture, out width);
|
||||||
|
}
|
||||||
|
else if (line.StartsWith("height="))
|
||||||
|
{
|
||||||
|
var v = line.Substring("height=".Length);
|
||||||
|
int.TryParse(v, NumberStyles.Integer, CultureInfo.InvariantCulture, out height);
|
||||||
|
}
|
||||||
|
else if (line.StartsWith("avg_frame_rate="))
|
||||||
|
{
|
||||||
|
var v = line.Substring("avg_frame_rate=".Length);
|
||||||
|
var parts = v.Split('/');
|
||||||
|
if (parts.Length == 2 &&
|
||||||
|
double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var num) &&
|
||||||
|
double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var den) &&
|
||||||
|
den != 0)
|
||||||
|
{
|
||||||
|
fps = num / den;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.WaitForExit();
|
||||||
|
|
||||||
|
return (duration, width, height, fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user