using System;
using System.Text;
namespace Paca.Core;
///
/// Configuration for tiled thumbnail sprite sheet generation.
///
public sealed record SpriteSheetConfig(
int Columns,
int Rows,
int ThumbWidth,
int ThumbHeight,
int IntervalSec)
{
public int TotalTilesPerSheet => Columns * Rows;
public int SheetWidth => Columns * ThumbWidth;
public int SheetHeight => Rows * ThumbHeight;
}
///
/// Generates ffmpeg tile filter pipelines and WebVTT sprite coordinate mappings for scrubbing preview.
///
public static class VideoSpriteSheetGenerator
{
///
/// Builds ffmpeg CLI command line for rendering tiled thumbnail sprite sheet JPEG.
///
public static string BuildFfmpegSpriteCommandLine(
string inputVideoPath,
string outputJpgPath,
SpriteSheetConfig config)
{
if (string.IsNullOrWhiteSpace(inputVideoPath)) throw new ArgumentException("Input video path cannot be null or whitespace.", nameof(inputVideoPath));
if (string.IsNullOrWhiteSpace(outputJpgPath)) throw new ArgumentException("Output JPG path cannot be null or whitespace.", nameof(outputJpgPath));
var cleanIn = inputVideoPath.Trim().Replace("\"", "");
var cleanOut = outputJpgPath.Trim().Replace("\"", "");
if (string.Equals(System.IO.Path.GetFullPath(cleanIn), System.IO.Path.GetFullPath(cleanOut), StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("입력 파일과 출력 파일 경로가 동일할 수 없습니다.");
if (config.IntervalSec <= 0) throw new ArgumentOutOfRangeException(nameof(config.IntervalSec), "Interval must be positive.");
if (config.Columns <= 0 || config.Rows <= 0) throw new ArgumentOutOfRangeException(nameof(config), "Columns and Rows must be positive.");
return $"-i \"{cleanIn}\" -vf \"fps=1/{config.IntervalSec},scale={config.ThumbWidth}:{config.ThumbHeight},tile={config.Columns}x{config.Rows}\" -frames:v 1 \"{cleanOut}\" -y";
}
///
/// Generates standard WebVTT subtitle/scrubber specification with #xywh coordinates.
///
public static string GenerateVttMapping(
string spriteFileName,
double totalDurationSec,
SpriteSheetConfig config)
{
if (config.IntervalSec <= 0) throw new ArgumentOutOfRangeException(nameof(config.IntervalSec), "Interval must be positive.");
if (config.Columns <= 0 || config.Rows <= 0) throw new ArgumentOutOfRangeException(nameof(config), "Columns and Rows must be positive.");
var sb = new StringBuilder();
sb.AppendLine("WEBVTT");
sb.AppendLine();
var tileIndex = 0;
for (double start = 0; start < totalDurationSec; start += config.IntervalSec)
{
var end = Math.Min(start + config.IntervalSec, totalDurationSec);
var startTimeStr = FormatVttTime(start);
var endTimeStr = FormatVttTime(end);
var col = tileIndex % config.Columns;
var row = (tileIndex / config.Columns) % config.Rows;
var x = col * config.ThumbWidth;
var y = row * config.ThumbHeight;
sb.AppendLine($"{startTimeStr} --> {endTimeStr}");
sb.AppendLine($"{spriteFileName}#xywh={x},{y},{config.ThumbWidth},{config.ThumbHeight}");
sb.AppendLine();
tileIndex++;
}
return sb.ToString();
}
private static string FormatVttTime(double seconds)
{
var ts = TimeSpan.FromSeconds(seconds);
return $"{(int)ts.TotalHours:D2}:{ts.Minutes:D2}:{ts.Seconds:D2}.{ts.Milliseconds:D3}";
}
}