91 lines
3.6 KiB
C#
91 lines
3.6 KiB
C#
using System;
|
|
using System.Text;
|
|
|
|
namespace Paca.Core;
|
|
|
|
/// <summary>
|
|
/// Configuration for tiled thumbnail sprite sheet generation.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates ffmpeg tile filter pipelines and WebVTT sprite coordinate mappings for scrubbing preview.
|
|
/// </summary>
|
|
public static class VideoSpriteSheetGenerator
|
|
{
|
|
/// <summary>
|
|
/// Builds ffmpeg CLI command line for rendering tiled thumbnail sprite sheet JPEG.
|
|
/// </summary>
|
|
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";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates standard WebVTT subtitle/scrubber specification with #xywh coordinates.
|
|
/// </summary>
|
|
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}";
|
|
}
|
|
}
|