89 lines
3 KiB
C#
89 lines
3 KiB
C#
using System;
|
|
using System.Globalization;
|
|
|
|
namespace Paca.Core;
|
|
|
|
/// <summary>
|
|
/// Image format for extracted video poster thumbnails.
|
|
/// </summary>
|
|
public enum PosterFormat
|
|
{
|
|
Jpeg,
|
|
Png,
|
|
WebP
|
|
}
|
|
|
|
/// <summary>
|
|
/// Specification for capturing high-quality single-frame video posters.
|
|
/// </summary>
|
|
public sealed record PosterExtractionSpec(
|
|
double SeekTimeSec,
|
|
int Width,
|
|
int Height,
|
|
PosterFormat Format,
|
|
int Quality = 85
|
|
);
|
|
|
|
/// <summary>
|
|
/// Video poster frame extractor generating intelligent seek points and scaling pipelines.
|
|
/// </summary>
|
|
public static class VideoPosterExtractor
|
|
{
|
|
/// <summary>
|
|
/// 배터리 절전 모드 활성화 시 CPU 집약적인 썸네일/포스터 프레임 추출을 일시 유예할지 판정합니다.
|
|
/// </summary>
|
|
public static bool ShouldDeferExtraction(Platform.BatterySaverPowerManager? powerManager) =>
|
|
powerManager?.ShouldDeferBackgroundProcessing ?? false;
|
|
|
|
/// <summary>
|
|
/// Computes optimal frame timestamp skipping black intro/logos while avoiding spoilers.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds aspect-ratio preserving downscale filter.
|
|
/// </summary>
|
|
public static string BuildScalingFilter(PosterExtractionSpec spec)
|
|
{
|
|
return $"scale={spec.Width}:{spec.Height}:force_original_aspect_ratio=decrease";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds full ffmpeg command line for single-frame thumbnail extraction.
|
|
/// </summary>
|
|
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";
|
|
}
|
|
}
|