using System; using System.Globalization; namespace Paca.Core; /// /// Image format for extracted video poster thumbnails. /// public enum PosterFormat { Jpeg, Png, WebP } /// /// Specification for capturing high-quality single-frame video posters. /// public sealed record PosterExtractionSpec( double SeekTimeSec, int Width, int Height, PosterFormat Format, int Quality = 85 ); /// /// Video poster frame extractor generating intelligent seek points and scaling pipelines. /// public static class VideoPosterExtractor { /// /// 배터리 절전 모드 활성화 시 CPU 집약적인 썸네일/포스터 프레임 추출을 일시 유예할지 판정합니다. /// public static bool ShouldDeferExtraction(Platform.BatterySaverPowerManager? powerManager) => powerManager?.ShouldDeferBackgroundProcessing ?? false; /// /// Computes optimal frame timestamp skipping black intro/logos while avoiding spoilers. /// public static double CalculateOptimalSeekTime(double totalDurationSec) { if (totalDurationSec <= 0) return 0.0; if (totalDurationSec < 10.0) return Math.Min(1.0, totalDurationSec * 0.5); return Math.Min(10.0, totalDurationSec * 0.1); } /// /// Builds aspect-ratio preserving downscale filter. /// public static string BuildScalingFilter(PosterExtractionSpec spec) { return $"scale={spec.Width}:{spec.Height}:force_original_aspect_ratio=decrease"; } /// /// Builds full ffmpeg command line for single-frame thumbnail extraction. /// public static string BuildPosterCommandLine( string inputVideoPath, string outputImagePath, PosterExtractionSpec spec) { if (string.IsNullOrWhiteSpace(inputVideoPath)) throw new ArgumentException("Input video path cannot be null or whitespace.", nameof(inputVideoPath)); if (string.IsNullOrWhiteSpace(outputImagePath)) throw new ArgumentException("Output image path cannot be null or whitespace.", nameof(outputImagePath)); var cleanIn = inputVideoPath.Trim().Replace("\"", ""); var cleanOut = outputImagePath.Trim().Replace("\"", ""); if (string.Equals(System.IO.Path.GetFullPath(cleanIn), System.IO.Path.GetFullPath(cleanOut), StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("입력 파일과 출력 파일 경로가 동일할 수 없습니다."); var seekStr = spec.SeekTimeSec.ToString("F2", CultureInfo.InvariantCulture); var scaleFilter = BuildScalingFilter(spec); var encoderFlags = spec.Format switch { PosterFormat.WebP => $"-c:v libwebp -quality {spec.Quality}", PosterFormat.Png => "-c:v png", _ => "-q:v 2" }; return $"-ss {seekStr} -i \"{cleanIn}\" -vframes 1 -vf \"{scaleFilter}\" {encoderFlags} \"{cleanOut}\" -y"; } }