fix: 빠른 변환 진행 표시(제목 동적+%) + 영상→이미지 프레임 추출 + GPU 가속
사용자 리포트: mp4 변환 시 %안나오고 멈춤, JPEG 고정 문구, GPU 가속 부재.
- QuickProgressWindow: 제목/타이틀바를 출력 형식으로 동적화('JPEG 고정' 해결), % 텍스트 + indeterminate 표시
- FfmpegProvider: 영상→이미지(jpg/png/webp/bmp) 대표 프레임 추출 직접 엣지 (mp4→jpg가 무거운 mp4→gif→jpg 멀티홉 거치던 문제 해결)
- GPU 가속: ConvertOptions.VideoPreferGpu(기본 on) — NVENC 시도 후 실패 시 CPU 자동 폴백, 설정창 토글로 제어
- 테스트 38개 통과
This commit is contained in:
parent
573d04521d
commit
ef10c139cd
8 changed files with 87 additions and 17 deletions
|
|
@ -64,6 +64,9 @@ public sealed class ConvertOptions
|
|||
/// <summary>래스터화 같은 큰 손실 엣지를 회피한다.</summary>
|
||||
public bool AvoidLossy { get; set; } = false;
|
||||
|
||||
/// <summary>영상 인코딩 시 GPU 하드웨어 가속(NVENC)을 우선 시도하고, 실패하면 CPU로 자동 폴백한다.</summary>
|
||||
public bool VideoPreferGpu { get; set; } = true;
|
||||
|
||||
public PdfCompressOptions PdfCompress { get; set; } = new();
|
||||
|
||||
public AiOptions Ai { get; set; } = new();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ public sealed class FfmpegProvider : IConverterProvider
|
|||
{
|
||||
private static readonly string[] Video = { ".mp4", ".mkv", ".webm", ".mov", ".avi", ".gif" };
|
||||
private static readonly string[] Audio = { ".mp3", ".aac", ".m4a", ".opus", ".ogg", ".flac", ".wav" };
|
||||
private static readonly string[] ImageFromVideo = { ".png", ".jpg", ".jpeg", ".webp", ".bmp" };
|
||||
|
||||
public ProviderCapability Capability { get; } = new(
|
||||
Id: "ffmpeg",
|
||||
|
|
@ -34,6 +35,7 @@ public sealed class FfmpegProvider : IConverterProvider
|
|||
pairs.AddRange(ProviderCapability.PairsFromMatrix(Video, Video, LossClass.Recode));
|
||||
pairs.AddRange(ProviderCapability.PairsFromMatrix(Audio, Audio, LossClass.Recode));
|
||||
pairs.AddRange(ProviderCapability.PairsFromMatrix(Video, Audio, LossClass.Recode)); // 영상 → 오디오 추출
|
||||
pairs.AddRange(ProviderCapability.PairsFromMatrix(Video, ImageFromVideo, LossClass.Rasterize)); // 영상 → 대표 프레임 이미지
|
||||
return pairs;
|
||||
}
|
||||
|
||||
|
|
@ -60,27 +62,63 @@ public sealed class FfmpegProvider : IConverterProvider
|
|||
return ConvertResult.Skip(sourcePath, "기존 파일이 있어 건너뜁니다.");
|
||||
|
||||
var ffOptions = new FFOptions { BinaryFolder = dir };
|
||||
var inExt = ConversionPair.Normalize(Path.GetExtension(sourcePath));
|
||||
var isImageOut = outExt is ".png" or ".jpg" or ".jpeg" or ".webp" or ".bmp";
|
||||
try
|
||||
{
|
||||
progress?.Report(0.05);
|
||||
progress?.Report(0.1);
|
||||
|
||||
// 영상 → 이미지: 대표 프레임 1장 추출 (gif 경유 멀티홉을 피해 즉시 처리)
|
||||
if (isImageOut && Array.IndexOf(Video, inExt) >= 0)
|
||||
{
|
||||
var okImg = await FFMpegArguments
|
||||
.FromFileInput(sourcePath)
|
||||
.OutputToFile(outPath, overwrite: true, o => o.WithCustomArgument("-frames:v 1 -update 1"))
|
||||
.CancellableThrough(cancellationToken)
|
||||
.ProcessAsynchronously(throwOnError: true, ffOptions)
|
||||
.ConfigureAwait(false);
|
||||
progress?.Report(1.0);
|
||||
return okImg && File.Exists(outPath)
|
||||
? ConvertResult.Ok(sourcePath, new[] { outPath })
|
||||
: ConvertResult.Fail(sourcePath, "영상에서 프레임 추출에 실패했습니다.");
|
||||
}
|
||||
|
||||
// 진행률 best-effort: ffprobe로 길이를 알면 시간 기반 보고
|
||||
TimeSpan total = TimeSpan.Zero;
|
||||
try { total = (await FFProbe.AnalyseAsync(sourcePath, ffOptions, cancellationToken).ConfigureAwait(false)).Duration; }
|
||||
catch { /* 길이 미상이면 진행률 생략 */ }
|
||||
|
||||
var processor = FFMpegArguments
|
||||
.FromFileInput(sourcePath)
|
||||
.OutputToFile(outPath, overwrite: true)
|
||||
.CancellableThrough(cancellationToken);
|
||||
// GPU(NVENC) 가속은 H.264 컨테이너(mp4/mkv/mov)에만 적용 시도하고, 실패하면 CPU로 폴백
|
||||
var tryGpu = options.VideoPreferGpu && (outExt is ".mp4" or ".mkv" or ".mov");
|
||||
|
||||
if (total > TimeSpan.Zero)
|
||||
processor = processor.NotifyOnProgress(
|
||||
percent => progress?.Report(Math.Clamp(percent / 100.0, 0, 1)), total);
|
||||
async Task<bool> RunAsync(bool gpu)
|
||||
{
|
||||
var processor = FFMpegArguments
|
||||
.FromFileInput(sourcePath)
|
||||
.OutputToFile(outPath, overwrite: true, o =>
|
||||
{
|
||||
if (gpu) o.WithVideoCodec("h264_nvenc");
|
||||
})
|
||||
.CancellableThrough(cancellationToken);
|
||||
if (total > TimeSpan.Zero)
|
||||
processor = processor.NotifyOnProgress(
|
||||
percent => progress?.Report(Math.Clamp(percent / 100.0, 0, 1)), total);
|
||||
return await processor.ProcessAsynchronously(throwOnError: true, ffOptions).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
bool ok;
|
||||
try
|
||||
{
|
||||
ok = await RunAsync(tryGpu).ConfigureAwait(false);
|
||||
}
|
||||
catch when (tryGpu && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// NVENC 미지원(GPU 없음 등) → CPU 인코더로 폴백
|
||||
progress?.Report(0.05);
|
||||
ok = await RunAsync(false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var ok = await processor.ProcessAsynchronously(throwOnError: true, ffOptions).ConfigureAwait(false);
|
||||
progress?.Report(1.0);
|
||||
|
||||
return ok && File.Exists(outPath)
|
||||
? ConvertResult.Ok(sourcePath, new[] { outPath })
|
||||
: ConvertResult.Fail(sourcePath, "FFmpeg 변환에 실패했습니다.");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue