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}"; } }