mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-22 00:22:01 +00:00
Source image rotation added. Interfaces simplified.
This commit is contained in:
parent
44d817f17f
commit
3fa068e48b
@ -20,6 +20,7 @@ public class SingleJob
|
|||||||
public bool EstimateOnly { get; set; }
|
public bool EstimateOnly { get; set; }
|
||||||
public bool ForceFixed { get; set; }
|
public bool ForceFixed { get; set; }
|
||||||
public bool SingleThreaded { get; set; }
|
public bool SingleThreaded { get; set; }
|
||||||
|
public int? Rotate { get; set; }
|
||||||
public Dictionary<string, string> Parameters { get; set; } = [];
|
public Dictionary<string, string> Parameters { get; set; } = [];
|
||||||
|
|
||||||
public void Override<T>(ref T member, string name)
|
public void Override<T>(ref T member, string name)
|
||||||
@ -100,6 +101,18 @@ public sealed class CommandLine
|
|||||||
{
|
{
|
||||||
Master.Detect = arg.Substring("--detect=".Length).ToLowerInvariant();
|
Master.Detect = arg.Substring("--detect=".Length).ToLowerInvariant();
|
||||||
}
|
}
|
||||||
|
else if (arg =="--rotate")
|
||||||
|
{
|
||||||
|
Master.Rotate = 90;
|
||||||
|
}
|
||||||
|
else if (arg.StartsWith("--rotate="))
|
||||||
|
{
|
||||||
|
var val = arg.Substring("--rotate=".Length);
|
||||||
|
if (int.TryParse(val, out var degrees) && (degrees == 90 || degrees == 180 || degrees == 270))
|
||||||
|
Master.Rotate = degrees;
|
||||||
|
else
|
||||||
|
throw new FormatException($"Invalid --rotate value: {val}");
|
||||||
|
}
|
||||||
else if (arg.StartsWith("--crop="))
|
else if (arg.StartsWith("--crop="))
|
||||||
{
|
{
|
||||||
Master.Crop = ParseCrop(arg.Substring("--crop=".Length));
|
Master.Crop = ParseCrop(arg.Substring("--crop=".Length));
|
||||||
@ -172,6 +185,7 @@ public sealed class CommandLine
|
|||||||
EstimateOnly = Master.EstimateOnly,
|
EstimateOnly = Master.EstimateOnly,
|
||||||
ForceFixed = Master.ForceFixed,
|
ForceFixed = Master.ForceFixed,
|
||||||
SingleThreaded = Master.SingleThreaded,
|
SingleThreaded = Master.SingleThreaded,
|
||||||
|
Rotate = Master.Rotate,
|
||||||
Parameters = new Dictionary<string, string>(Master.Parameters)
|
Parameters = new Dictionary<string, string>(Master.Parameters)
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
}
|
}
|
||||||
@ -297,6 +311,9 @@ Options:
|
|||||||
Last segment may be shorter.
|
Last segment may be shorter.
|
||||||
Default: OFF
|
Default: OFF
|
||||||
|
|
||||||
|
--rotate=<degrees> Rotate video by specified degrees (90, 180, 270).
|
||||||
|
Useful for videos with incorrect orientation metadata.
|
||||||
|
|
||||||
--estimate Print calculated segment information and exit.
|
--estimate Print calculated segment information and exit.
|
||||||
No splitting is performed.
|
No splitting is performed.
|
||||||
|
|
||||||
|
|||||||
@ -6,5 +6,5 @@ namespace splitter;
|
|||||||
|
|
||||||
public interface ISegmentProcessor
|
public interface ISegmentProcessor
|
||||||
{
|
{
|
||||||
Task ProcessSegment( string inputFile, string outputFile, double start, double length, int videoWidth, int videoHeight, double fps, string[] ffmpegPassthroughParameters);
|
Task ProcessSegment( SingleTask job );
|
||||||
}
|
}
|
||||||
|
|||||||
92
ProbeVideo.cs
Normal file
92
ProbeVideo.cs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace splitter;
|
||||||
|
|
||||||
|
public record VideoInfo(
|
||||||
|
double Duration,
|
||||||
|
int Width,
|
||||||
|
int Height,
|
||||||
|
double Fps,
|
||||||
|
double Bitrate
|
||||||
|
);
|
||||||
|
|
||||||
|
public static class ProbeVideo
|
||||||
|
{
|
||||||
|
public static VideoInfo Probe(string inputFile)
|
||||||
|
{
|
||||||
|
var args =
|
||||||
|
"-v error " +
|
||||||
|
"-select_streams v:0 " +
|
||||||
|
"-show_entries format=duration " +
|
||||||
|
"-show_entries stream=width,height,avg_frame_rate,bit_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;
|
||||||
|
var bitrate = 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("bit_rate="))
|
||||||
|
{
|
||||||
|
var v = line.Substring("bit_rate=".Length);
|
||||||
|
double.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out bitrate);
|
||||||
|
}
|
||||||
|
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 new(duration, width, height, fps, bitrate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,15 +9,41 @@ 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, int videoWidth, int videoHeight, double fps, string[] passthrough)
|
public async Task ProcessSegment(SingleTask job)
|
||||||
{
|
{
|
||||||
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
|
string inputFile = job.Job.InputFile;
|
||||||
|
string outputFile = job.OutputFileName;
|
||||||
|
double start = job.SegmentStart;
|
||||||
|
double length = job.SegmentLength;
|
||||||
|
int videoWidth = job.Info.Width;
|
||||||
|
int videoHeight = job.Info.Height;
|
||||||
|
double fps = job.Info.Fps;
|
||||||
|
string[] ffmpegPassthroughParameters = job.Job.Passthrough;
|
||||||
|
|
||||||
|
var pass = ffmpegPassthroughParameters.Length > 0 ? string.Join(" ", ffmpegPassthroughParameters) : "";
|
||||||
|
|
||||||
var args =
|
string args;
|
||||||
$"-ss {start.ToString(CultureInfo.InvariantCulture)} " +
|
var rotation = GetRotationFilter(job.Job.Rotate);
|
||||||
$"-i \"{inputFile}\" " +
|
if (rotation == null)
|
||||||
$"-t {length.ToString(CultureInfo.InvariantCulture)} " +
|
{
|
||||||
$"-c copy {pass} \"{outputFile}\" -y";
|
args =
|
||||||
|
$"-ss {start.ToString(CultureInfo.InvariantCulture)} " +
|
||||||
|
$"-i \"{inputFile}\" " +
|
||||||
|
$"-t {length.ToString(CultureInfo.InvariantCulture)} " +
|
||||||
|
$"-c copy {pass} \"{outputFile}\" -y";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Rotation → must re-encode
|
||||||
|
args =
|
||||||
|
$"-ss {start.ToString(CultureInfo.InvariantCulture)} " +
|
||||||
|
$"-i \"{inputFile}\" " +
|
||||||
|
$"-t {length.ToString(CultureInfo.InvariantCulture)} " +
|
||||||
|
$"-vf \"{rotation}\" " +
|
||||||
|
"-c:v h264_nvenc -preset p4 -b:v 8M -pix_fmt yuv420p " +
|
||||||
|
"-c:a copy " +
|
||||||
|
$"{pass} \"{outputFile}\" -y";
|
||||||
|
}
|
||||||
|
|
||||||
var psi = new ProcessStartInfo
|
var psi = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
@ -43,6 +69,16 @@ public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger,
|
|||||||
LogInfo($"Segment {name} processing completed");
|
LogInfo($"Segment {name} processing completed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string? GetRotationFilter(int? degrees) =>
|
||||||
|
degrees switch
|
||||||
|
{
|
||||||
|
90 => "transpose=1",
|
||||||
|
180 => "rotate=PI",
|
||||||
|
270 => "transpose=2",
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
private void ShowFFMpegProgress(double length, Process proc, string name)
|
private void ShowFFMpegProgress(double length, Process proc, string name)
|
||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
|
|||||||
12
SingleTask.cs
Normal file
12
SingleTask.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using splitter;
|
||||||
|
|
||||||
|
public record SingleTask(
|
||||||
|
SingleJob Job,
|
||||||
|
VideoInfo Info,
|
||||||
|
string OutputFileName,
|
||||||
|
int SegmentIndex,
|
||||||
|
int TotalSegments,
|
||||||
|
double SegmentStart,
|
||||||
|
double SegmentLength,
|
||||||
|
Func<int, ISegmentProcessor> ProcessorFactory
|
||||||
|
);
|
||||||
@ -11,7 +11,6 @@ namespace splitter;
|
|||||||
public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||||
{
|
{
|
||||||
private readonly IObjectDetector _detector;
|
private readonly IObjectDetector _detector;
|
||||||
private readonly SingleJob _cmd;
|
|
||||||
|
|
||||||
public TrackingSplitter(
|
public TrackingSplitter(
|
||||||
int progressLine,
|
int progressLine,
|
||||||
@ -21,7 +20,6 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
: base(logger, progressLine)
|
: base(logger, progressLine)
|
||||||
{
|
{
|
||||||
_detector = detector;
|
_detector = detector;
|
||||||
_cmd = cmd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@ -30,14 +28,18 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
d.Dispose();
|
d.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ProcessSegment(
|
public async Task ProcessSegment(SingleTask job)
|
||||||
string inputFile,
|
|
||||||
string outputFile,
|
|
||||||
double start,
|
|
||||||
double length,
|
|
||||||
int videoWidth, int videoHeight, double fps,
|
|
||||||
string[] ffmpegPassthroughParameters)
|
|
||||||
{
|
{
|
||||||
|
string inputFile = job.Job.InputFile;
|
||||||
|
string outputFile = job.OutputFileName;
|
||||||
|
double start = job.SegmentStart;
|
||||||
|
double length = job.SegmentLength;
|
||||||
|
int videoWidth = job.Info.Width;
|
||||||
|
int videoHeight = job.Info.Height;
|
||||||
|
double fps = job.Info.Fps;
|
||||||
|
double bitrate = job.Info.Bitrate;
|
||||||
|
string[] ffmpegPassthroughParameters = job.Job.Passthrough;
|
||||||
|
|
||||||
var name = Path.GetFileNameWithoutExtension(outputFile);
|
var name = Path.GetFileNameWithoutExtension(outputFile);
|
||||||
|
|
||||||
// 1) Probe source video
|
// 1) Probe source video
|
||||||
@ -47,19 +49,19 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_cmd.Crop == null)
|
if (job.Job.Crop == null)
|
||||||
{
|
{
|
||||||
LogError($"{name}: Crop parameters are required");
|
LogError($"{name}: Crop parameters are required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var encWidth = _cmd.Debug ? videoWidth : _cmd.Crop.Value.width;
|
var encWidth = job.Job.Debug ? videoWidth : job.Job.Crop.Value.width;
|
||||||
var encHeight = _cmd.Debug ? videoHeight : _cmd.Crop.Value.height;
|
var encHeight = job.Job.Debug ? videoHeight : job.Job.Crop.Value.height;
|
||||||
|
|
||||||
LogInfo($"{name}: src={videoWidth}x{videoHeight} @ {fps:F3}fps, seg=[{start:F3},{length:F3}] enc={encWidth}x{encHeight}");
|
LogInfo($"{name}: src={videoWidth}x{videoHeight} @ {fps:F3}fps, seg=[{start:F3},{length:F3}] enc={encWidth}x{encHeight}");
|
||||||
|
|
||||||
// 2) Start FFmpeg decode (video only → raw BGR24 to stdout)
|
// 2) Start FFmpeg decode (video only → raw BGR24 to stdout)
|
||||||
var decode = StartFfmpegDecode(inputFile, start, length);
|
var decode = StartFfmpegDecode(inputFile, start, length, job.Job.Rotate, job.Job.PlainText);
|
||||||
using var decodeStdout = decode.StandardOutput.BaseStream;
|
using var decodeStdout = decode.StandardOutput.BaseStream;
|
||||||
|
|
||||||
// 3) Start FFmpeg encode (video from stdin + audio from original)
|
// 3) Start FFmpeg encode (video from stdin + audio from original)
|
||||||
@ -71,7 +73,8 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
encWidth,
|
encWidth,
|
||||||
encHeight,
|
encHeight,
|
||||||
fps,
|
fps,
|
||||||
ffmpegPassthroughParameters);
|
ffmpegPassthroughParameters,
|
||||||
|
job.Job.PlainText);
|
||||||
|
|
||||||
using var encodeStdin = encode.StandardInput.BaseStream;
|
using var encodeStdin = encode.StandardInput.BaseStream;
|
||||||
|
|
||||||
@ -89,10 +92,10 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
var camera = new CameraController(
|
var camera = new CameraController(
|
||||||
videoWidth,
|
videoWidth,
|
||||||
videoHeight,
|
videoHeight,
|
||||||
_cmd.Crop.Value.width,
|
job.Job.Crop.Value.width,
|
||||||
_cmd.Crop.Value.height,
|
job.Job.Crop.Value.height,
|
||||||
kalman,
|
kalman,
|
||||||
_cmd);
|
job.Job);
|
||||||
|
|
||||||
var startTime = DateTime.UtcNow;
|
var startTime = DateTime.UtcNow;
|
||||||
var totalFrames = (int)Math.Round(length * fps);
|
var totalFrames = (int)Math.Round(length * fps);
|
||||||
@ -115,7 +118,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
camera.Update(primary);
|
camera.Update(primary);
|
||||||
var roi = camera.Roi;
|
var roi = camera.Roi;
|
||||||
|
|
||||||
if (_cmd.Debug)
|
if (job.Job.Debug)
|
||||||
{
|
{
|
||||||
DrawDebug(frameMat, objects, camera, kalman);
|
DrawDebug(frameMat, objects, camera, kalman);
|
||||||
frameMat.CopyTo(outMat);
|
frameMat.CopyTo(outMat);
|
||||||
@ -165,15 +168,26 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
|
|
||||||
// ---------- FFmpeg decode / encode ----------
|
// ---------- FFmpeg decode / encode ----------
|
||||||
|
|
||||||
private Process StartFfmpegDecode(string inputFile, double start, double length)
|
private Process StartFfmpegDecode(string inputFile, double start, double length, int? rotate, bool plainText)
|
||||||
{
|
{
|
||||||
var ss = start.ToString("0.###", CultureInfo.InvariantCulture);
|
var ss = start .ToString("0.###", CultureInfo.InvariantCulture);
|
||||||
var t = length.ToString("0.###", CultureInfo.InvariantCulture);
|
var t = length.ToString("0.###", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var args =
|
var args =
|
||||||
$"-i \"{inputFile}\" -ss {ss} -t {t} " +
|
$"-i \"{inputFile}\" -ss {ss} -t {t} " +
|
||||||
"-an -sn " +
|
"-an -sn " +
|
||||||
"-vf format=bgr24 " +
|
$"-vf format=bgr24{rotateStr} " +
|
||||||
"-f rawvideo -";
|
"-f rawvideo -";
|
||||||
|
|
||||||
var psi = new ProcessStartInfo
|
var psi = new ProcessStartInfo
|
||||||
@ -191,20 +205,17 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
|
|
||||||
var fileName = Path.GetFileName(inputFile);
|
var fileName = Path.GetFileName(inputFile);
|
||||||
|
|
||||||
if (_cmd.PlainText)
|
_ = Task.Run(() =>
|
||||||
{
|
{
|
||||||
_ = Task.Run(() =>
|
try
|
||||||
{
|
{
|
||||||
try
|
string? line;
|
||||||
{
|
while ((line = p.StandardError.ReadLine()) != null)
|
||||||
string? line;
|
if (plainText)
|
||||||
while ((line = p.StandardError.ReadLine()) != null)
|
LogInfo($"[ffmpeg-decode] {fileName}: {line}");
|
||||||
if (_cmd.PlainText)
|
}
|
||||||
LogInfo($"[ffmpeg-decode] {fileName}: {line}");
|
catch { }
|
||||||
}
|
});
|
||||||
catch { }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
@ -217,7 +228,8 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
double fps,
|
double fps,
|
||||||
string[] passthrough)
|
string[] passthrough,
|
||||||
|
bool plainText)
|
||||||
{
|
{
|
||||||
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
|
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
|
||||||
var fpsStr = fps.ToString("0.###", CultureInfo.InvariantCulture);
|
var fpsStr = fps.ToString("0.###", CultureInfo.InvariantCulture);
|
||||||
@ -230,9 +242,10 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
$"-ss {ss} -i \"{inputFile}\" " +
|
$"-ss {ss} -i \"{inputFile}\" " +
|
||||||
"-map 0:v:0 -map 1:a:0? -shortest " +
|
"-map 0:v:0 -map 1:a:0? -shortest " +
|
||||||
"-c:v h264_nvenc -preset p4 -b:v 8M -pix_fmt yuv420p " +
|
"-c:v h264_nvenc -preset p4 -b:v 8M -pix_fmt yuv420p " +
|
||||||
"-c:a aac -b:a 192k " +
|
"-c:a copy " +
|
||||||
pass + $" \"{outputFile}\"";
|
pass + $" \"{outputFile}\"";
|
||||||
|
|
||||||
|
// "-c:a aac -b:a 192k " +
|
||||||
|
|
||||||
|
|
||||||
var psi = new ProcessStartInfo
|
var psi = new ProcessStartInfo
|
||||||
@ -257,7 +270,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
string? line;
|
string? line;
|
||||||
while ((line = p.StandardError.ReadLine()) != null)
|
while ((line = p.StandardError.ReadLine()) != null)
|
||||||
{
|
{
|
||||||
if (_cmd.PlainText)
|
if (plainText)
|
||||||
LogInfo($"[ffmpeg-encode] {fileName}: {line}");
|
LogInfo($"[ffmpeg-encode] {fileName}: {line}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
124
splitter.cs
124
splitter.cs
@ -5,23 +5,10 @@ using System.Text;
|
|||||||
using Spectre.Console;
|
using Spectre.Console;
|
||||||
using splitter;
|
using splitter;
|
||||||
|
|
||||||
static class Program
|
static partial class Program
|
||||||
{
|
{
|
||||||
private static ILogger _logger = null!;
|
private static ILogger _logger = null!;
|
||||||
|
|
||||||
private record SingleTask(
|
|
||||||
SingleJob Job,
|
|
||||||
string OutputFileName,
|
|
||||||
int SegmentIndex,
|
|
||||||
int TotalSegments,
|
|
||||||
double SegmentStart,
|
|
||||||
double SegmentLength,
|
|
||||||
int VideoWidth,
|
|
||||||
int VideoHeight,
|
|
||||||
double VideoFps,
|
|
||||||
Func<int, ISegmentProcessor> ProcessorFactory
|
|
||||||
);
|
|
||||||
|
|
||||||
static async Task<int> Main(string[] args)
|
static async Task<int> Main(string[] args)
|
||||||
{
|
{
|
||||||
Task? uiTask = null;
|
Task? uiTask = null;
|
||||||
@ -48,6 +35,9 @@ static class Program
|
|||||||
uiTask = logger.RunAsync(cts.Token);
|
uiTask = logger.RunAsync(cts.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cmd.Master.EstimateOnly)
|
||||||
|
LogInfo("=== ESTIMATE MODE ===");
|
||||||
|
|
||||||
var allJobs = new List<SingleTask>();
|
var allJobs = new List<SingleTask>();
|
||||||
foreach ( var job in cmd.Jobs )
|
foreach ( var job in cmd.Jobs )
|
||||||
{
|
{
|
||||||
@ -89,10 +79,9 @@ static class Program
|
|||||||
Directory.CreateDirectory(job.OutputFolder);
|
Directory.CreateDirectory(job.OutputFolder);
|
||||||
|
|
||||||
job.Mask ??= $"{baseName}_seg%03d.mp4";
|
job.Mask ??= $"{baseName}_seg%03d.mp4";
|
||||||
LogInfo($"{baseName}: Reading duration via ffprobe...");
|
|
||||||
|
|
||||||
(double duration, int width, int height, double fps) = ProbeVideo(job.InputFile);
|
var info = ProbeVideo.Probe(job.InputFile);
|
||||||
if (duration <= 0)
|
if (info.Duration <= 0)
|
||||||
{
|
{
|
||||||
LogError($"{baseName}: Could not read duration.");
|
LogError($"{baseName}: Could not read duration.");
|
||||||
return [];
|
return [];
|
||||||
@ -106,25 +95,17 @@ static class Program
|
|||||||
if (job.ForceFixed)
|
if (job.ForceFixed)
|
||||||
{
|
{
|
||||||
// Fixed chunk size, last one may be shorter
|
// Fixed chunk size, last one may be shorter
|
||||||
segments = (int)Math.Ceiling(duration / target);
|
segments = (int)Math.Ceiling(info.Duration / target);
|
||||||
segmentLength = target;
|
segmentLength = target;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Equalized segments
|
// Equalized segments
|
||||||
segments = (int)Math.Ceiling(duration / target);
|
segments = (int)Math.Ceiling(info.Duration / target);
|
||||||
segmentLength = duration / segments;
|
segmentLength = info.Duration / segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd.Master.EstimateOnly)
|
LogInfo($"{baseName}: Duration {info.Duration:F2}s, {info.Width}x{info.Height} @ {info.Fps:F3}fps {info.Bitrate/1024:F0}kbps, Target duration: {target:F2}s Segments: {segments} segment length: {segmentLength:F2}s {(job.ForceFixed ? " fixed" : "")}" );
|
||||||
LogInfo("=== ESTIMATE MODE ===");
|
|
||||||
|
|
||||||
LogInfo($"{baseName}: Duration {duration:F2}s, {width}x{height} @ {fps:F3}fps");
|
|
||||||
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");
|
|
||||||
|
|
||||||
if (cmd.Master.EstimateOnly)
|
if (cmd.Master.EstimateOnly)
|
||||||
return [];
|
return [];
|
||||||
@ -152,17 +133,15 @@ static class Program
|
|||||||
.Select(i => new SingleTask
|
.Select(i => new SingleTask
|
||||||
(
|
(
|
||||||
Job : job,
|
Job : job,
|
||||||
|
Info: info,
|
||||||
OutputFileName : BuildOutputFileName(job.OutputFolder, job.Mask, i),
|
OutputFileName : BuildOutputFileName(job.OutputFolder, job.Mask, i),
|
||||||
SegmentIndex : i,
|
SegmentIndex : i,
|
||||||
TotalSegments : segments,
|
TotalSegments : segments,
|
||||||
SegmentStart : i * segmentLength,
|
SegmentStart : i * segmentLength,
|
||||||
SegmentLength : (i == segments - 1)
|
SegmentLength : (i == segments - 1)
|
||||||
? Math.Max(0.1, duration - i * segmentLength)
|
? Math.Max(0.1, info.Duration - i * segmentLength)
|
||||||
: segmentLength,
|
: segmentLength,
|
||||||
ProcessorFactory : processorFactory,
|
ProcessorFactory : processorFactory
|
||||||
VideoWidth : width,
|
|
||||||
VideoHeight : height,
|
|
||||||
VideoFps : fps
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.ToList();
|
.ToList();
|
||||||
@ -273,15 +252,7 @@ static class Program
|
|||||||
var processor = t.ProcessorFactory(slot);
|
var processor = t.ProcessorFactory(slot);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await processor.ProcessSegment(
|
await processor.ProcessSegment(t);
|
||||||
t.Job.InputFile,
|
|
||||||
t.OutputFileName,
|
|
||||||
t.SegmentStart,
|
|
||||||
t.SegmentLength,
|
|
||||||
t.VideoWidth,
|
|
||||||
t.VideoHeight,
|
|
||||||
t.VideoFps,
|
|
||||||
t.Job.Passthrough);
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -317,73 +288,6 @@ 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