99 lines
3.9 KiB
C#
99 lines
3.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace Paca.Core;
|
|
|
|
/// <summary>
|
|
/// Detected interval of silence in an audio/video stream.
|
|
/// </summary>
|
|
public sealed record SilenceSpan(double StartSec, double DurationSec, double EndSec);
|
|
|
|
/// <summary>
|
|
/// Effective playback and export window skipping unwanted intro and outro sections.
|
|
/// </summary>
|
|
public sealed record TrimWindow(double StartSec, double EndSec, double DurationSec);
|
|
|
|
/// <summary>
|
|
/// Intelligent video trimmer parsing silencedetect logs and producing lossless cuts for intros/outros.
|
|
/// </summary>
|
|
public static class VideoIntroOutroTrimmer
|
|
{
|
|
private static readonly Regex SilenceStartRegex = new(@"silence_start:\s*([0-9.]+)", RegexOptions.Compiled);
|
|
private static readonly Regex SilenceEndRegex = new(@"silence_end:\s*([0-9.]+)\s*\|\s*silence_duration:\s*([0-9.]+)", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// Parses ffmpeg silencedetect stderr output lines into structured silence spans.
|
|
/// </summary>
|
|
public static List<SilenceSpan> ParseSilenceSpans(string ffmpegOutput)
|
|
{
|
|
var spans = new List<SilenceSpan>();
|
|
if (string.IsNullOrWhiteSpace(ffmpegOutput))
|
|
return spans;
|
|
|
|
var startMatches = SilenceStartRegex.Matches(ffmpegOutput);
|
|
var endMatches = SilenceEndRegex.Matches(ffmpegOutput);
|
|
|
|
var count = Math.Min(startMatches.Count, endMatches.Count);
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
if (double.TryParse(startMatches[i].Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var start) &&
|
|
double.TryParse(endMatches[i].Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var end) &&
|
|
double.TryParse(endMatches[i].Groups[2].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var dur))
|
|
{
|
|
spans.Add(new SilenceSpan(start, dur, end));
|
|
}
|
|
}
|
|
|
|
return spans;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates safe playback trim window, guaranteeing at least 1 second duration.
|
|
/// </summary>
|
|
public static TrimWindow CalculateTrimWindow(double totalDurationSec, double skipIntroSec, double skipOutroSec)
|
|
{
|
|
if (totalDurationSec <= 0)
|
|
return new TrimWindow(0, 1.0, 1.0);
|
|
|
|
var start = Math.Max(0, skipIntroSec);
|
|
var end = Math.Max(0, totalDurationSec - skipOutroSec);
|
|
|
|
if (start >= end || (end - start) < 1.0)
|
|
{
|
|
start = 0;
|
|
end = Math.Max(1.0, totalDurationSec);
|
|
}
|
|
|
|
var duration = end - start;
|
|
return new TrimWindow(start, end, duration);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds ffmpeg CLI argument string for ultra-fast lossless video trimming.
|
|
/// </summary>
|
|
public static string BuildLosslessTrimCommandLine(
|
|
string inputPath,
|
|
string outputPath,
|
|
double startSec,
|
|
double durationSec)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(inputPath)) throw new ArgumentException("Input path cannot be null or whitespace.", nameof(inputPath));
|
|
if (string.IsNullOrWhiteSpace(outputPath)) throw new ArgumentException("Output path cannot be null or whitespace.", nameof(outputPath));
|
|
|
|
var cleanIn = inputPath.Trim().Replace("\"", "");
|
|
var cleanOut = outputPath.Trim().Replace("\"", "");
|
|
|
|
if (string.Equals(System.IO.Path.GetFullPath(cleanIn), System.IO.Path.GetFullPath(cleanOut), StringComparison.OrdinalIgnoreCase))
|
|
throw new InvalidOperationException("입력 파일과 출력 파일 경로가 동일할 수 없습니다.");
|
|
|
|
startSec = Math.Max(0.0, startSec);
|
|
durationSec = Math.Max(0.1, durationSec);
|
|
|
|
var s = startSec.ToString("F2", CultureInfo.InvariantCulture);
|
|
var d = durationSec.ToString("F2", CultureInfo.InvariantCulture);
|
|
|
|
return $"-ss {s} -i \"{cleanIn}\" -t {d} -c copy \"{cleanOut}\" -y";
|
|
}
|
|
}
|