using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Paca.Core;
///
/// Detected interval of silence in an audio/video stream.
///
public sealed record SilenceSpan(double StartSec, double DurationSec, double EndSec);
///
/// Effective playback and export window skipping unwanted intro and outro sections.
///
public sealed record TrimWindow(double StartSec, double EndSec, double DurationSec);
///
/// Intelligent video trimmer parsing silencedetect logs and producing lossless cuts for intros/outros.
///
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);
///
/// Parses ffmpeg silencedetect stderr output lines into structured silence spans.
///
public static List ParseSilenceSpans(string ffmpegOutput)
{
var spans = new List();
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;
}
///
/// Calculates safe playback trim window, guaranteeing at least 1 second duration.
///
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);
}
///
/// Builds ffmpeg CLI argument string for ultra-fast lossless video trimming.
///
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";
}
}