From 47600d2eb50d49d44017807d5bd437febf8e63b5 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Mon, 1 Jun 2026 22:25:12 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20mp4=20=EB=B3=80=ED=99=98=20=EB=A9=88?= =?UTF-8?q?=EC=B6=A4=20=ED=95=B4=EA=B2=B0=20=E2=80=94=208K=20HEVC=20?= =?UTF-8?q?=EC=BD=94=EB=8D=B1=20+=20=EB=8F=99=EC=9D=BC=ED=8F=AC=EB=A7=B7?= =?UTF-8?q?=20=EC=9E=AC=EC=9D=B8=EC=BD=94=EB=94=A9=20+=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=EC=83=81=ED=83=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 증상: 8K(7680x4320) HEVC 11분 6.76GB mp4 변환이 멈춤. 근본 원인 2가지: 1. mp4→mp4(같은 형식)이 그래프 self-edge 부재로 Skip 처리되고, QuickProgressWindow가 '전부 건너뜀' 종료 상태를 처리 안 해 멈춘 듯 보임. 2. 강제하던 h264_nvenc가 4096px 초과(8K)를 인코딩 못 해 즉시 실패 → CPU libx264 8K 폴백이 사실상 무한정 소요(진짜 멈춤). 수정: - FfmpegProvider: 해상도별 코덱 — videoWidth>4096(8K 등)은 hevc_nvenc(GPU) →libx265(CPU)로, 이하는 h264_nvenc→libx264. 코덱 강제는 mp4/mkv/mov에만 적용하고 webm/avi/gif는 컨테이너 기본 코덱(vp9 등) 유지. - FfmpegProvider: 비디오/오디오 동일포맷 self-edge 추가(mp4→mp4 등 재압축). gif는 제외 — 이미지로도 다뤄져 ffmpeg 미설치 시 Skip→하드실패 회귀 방지. - QuickProgressWindow.Finish: 성공·실패 없이 전부 건너뜀인 종료 상태를 명확히 안내. 검증: 실제 8K HEVC 클립으로 mp4→mp4 변환 → hevc_nvenc로 정상 인코딩, 28.3MB→2.1MB(93% 압축). 전체 38개 단위테스트 통과(적대적 리뷰 2건 반영). Package 1.0.3.0. --- packaging/Package.appxmanifest | 2 +- .../Views/QuickProgressWindow.xaml.cs | 8 +++++ .../Converters/FfmpegProvider.cs | 29 +++++++++++++++---- .../FfmpegProviderTests.cs | 10 +++++-- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/packaging/Package.appxmanifest b/packaging/Package.appxmanifest index 2718519..d9bd24e 100644 --- a/packaging/Package.appxmanifest +++ b/packaging/Package.appxmanifest @@ -12,7 +12,7 @@ diff --git a/src/Everything2Everything.App/Views/QuickProgressWindow.xaml.cs b/src/Everything2Everything.App/Views/QuickProgressWindow.xaml.cs index 9a39788..0e6b45a 100644 --- a/src/Everything2Everything.App/Views/QuickProgressWindow.xaml.cs +++ b/src/Everything2Everything.App/Views/QuickProgressWindow.xaml.cs @@ -75,6 +75,14 @@ public partial class QuickProgressWindow : FluentWindow DoneIllustration.Visibility = Visibility.Visible; Height = 360; } + else + { + // 성공·실패 없이 전부 건너뜀(같은 형식·기존 파일 존재 등) — 멈춤처럼 보이지 않게 명확히 안내 + PercentText.Text = "건너뜀"; + StatusText.Text = skipped > 0 + ? $"변환할 내용이 없어 건너뛰었습니다 ({skipped}개). '닫기'를 눌러 종료하세요." + : "처리할 항목이 없습니다."; + } } private void OnOpenFolderClick(object sender, RoutedEventArgs e) diff --git a/src/Everything2Everything.Core/Converters/FfmpegProvider.cs b/src/Everything2Everything.Core/Converters/FfmpegProvider.cs index e31fb9a..a7136bf 100644 --- a/src/Everything2Everything.Core/Converters/FfmpegProvider.cs +++ b/src/Everything2Everything.Core/Converters/FfmpegProvider.cs @@ -36,6 +36,11 @@ public sealed class FfmpegProvider : IConverterProvider pairs.AddRange(ProviderCapability.PairsFromMatrix(Audio, Audio, LossClass.Recode)); pairs.AddRange(ProviderCapability.PairsFromMatrix(Video, Audio, LossClass.Recode)); // 영상 → 오디오 추출 pairs.AddRange(ProviderCapability.PairsFromMatrix(Video, ImageFromVideo, LossClass.Rasterize)); // 영상 → 대표 프레임 이미지 + // 동일 포맷 self-edge: 같은 컨테이너로 재인코딩(=압축). mp4→mp4로 6GB 8K를 줄이는 사용 사례. + // self-edge는 ProviderRegistry에서 '다른 형식으로 변환' 목록에선 제외되지만, 명시 요청 시 엔진이 실행한다. + // gif는 제외 — 이미지로도 다뤄지므로(Magick) ffmpeg 미설치 시 무해한 Skip이 하드 실패로 바뀌는 회귀를 막는다. + foreach (var v in Video) if (!string.Equals(v, ".gif", StringComparison.OrdinalIgnoreCase)) pairs.Add(new ConversionPair(v, v, LossClass.Recode)); + foreach (var a in Audio) pairs.Add(new ConversionPair(a, a, LossClass.Recode)); return pairs; } @@ -83,13 +88,23 @@ public sealed class FfmpegProvider : IConverterProvider : ConvertResult.Fail(sourcePath, "영상에서 프레임 추출에 실패했습니다."); } - // 진행률 best-effort: ffprobe로 길이를 알면 시간 기반 보고 + // 진행률 best-effort + 코덱 선택용 해상도 파악: ffprobe로 길이·가로해상도 조회 TimeSpan total = TimeSpan.Zero; - try { total = (await FFProbe.AnalyseAsync(sourcePath, ffOptions, cancellationToken).ConfigureAwait(false)).Duration; } - catch { /* 길이 미상이면 진행률 생략 */ } + int videoWidth = 0; + try + { + var info = await FFProbe.AnalyseAsync(sourcePath, ffOptions, cancellationToken).ConfigureAwait(false); + total = info.Duration; + videoWidth = info.PrimaryVideoStream?.Width ?? 0; + } + catch { /* 분석 실패 시 진행률·코덱은 기본값 사용 */ } - // GPU(NVENC) 가속은 H.264 컨테이너(mp4/mkv/mov)에만 적용 시도하고, 실패하면 CPU로 폴백 - var tryGpu = options.VideoPreferGpu && (outExt is ".mp4" or ".mkv" or ".mov"); + // H.264는 4096px 초과(>4K, 예: 8K 7680px)를 인코딩하지 못해 "encoder를 열 수 없음"으로 즉시 실패한다. + // → 고해상도 입력은 8K까지 지원하는 HEVC(H.265)로 인코딩한다. + var highRes = videoWidth > 4096; + var h264Container = outExt is ".mp4" or ".mkv" or ".mov"; + // GPU(NVENC) 가속은 mp4/mkv/mov 컨테이너에만 적용 시도하고, 실패하면 CPU로 폴백 + var tryGpu = options.VideoPreferGpu && h264Container; async Task RunAsync(bool gpu) { @@ -97,7 +112,9 @@ public sealed class FfmpegProvider : IConverterProvider .FromFileInput(sourcePath) .OutputToFile(outPath, overwrite: true, o => { - if (gpu) o.WithVideoCodec("h264_nvenc"); + if (gpu) o.WithVideoCodec(highRes ? "hevc_nvenc" : "h264_nvenc"); + else if (h264Container) o.WithVideoCodec(highRes ? "libx265" : "libx264"); + // webm/avi/gif 등 비(非) H.264/HEVC 컨테이너는 컨테이너 기본 코덱(vp9 등)을 사용 }) .CancellableThrough(cancellationToken); if (total > TimeSpan.Zero) diff --git a/src/Everything2Everything.Tests/FfmpegProviderTests.cs b/src/Everything2Everything.Tests/FfmpegProviderTests.cs index e6114a3..067cbd1 100644 --- a/src/Everything2Everything.Tests/FfmpegProviderTests.cs +++ b/src/Everything2Everything.Tests/FfmpegProviderTests.cs @@ -27,14 +27,20 @@ public class FfmpegProviderTests Assert.NotNull(graph.FindBestPath(".mp4", ".webm")); // 영상 트랜스코딩 Assert.NotNull(graph.FindBestPath(".wav", ".mp3")); // 오디오 변환 Assert.NotNull(graph.FindBestPath(".mp4", ".mp3")); // 영상→오디오 추출 + Assert.NotNull(graph.FindBestPath(".mp4", ".mp4")); // 동일 포맷 재인코딩(압축) self-edge + Assert.NotNull(graph.FindBestPath(".mp3", ".mp3")); // 동일 포맷 오디오 재인코딩 self-edge } [Fact] - public void Capability_DeclaresVideoAudioMatrix_NoSelfPairs() + public void Capability_DeclaresVideoAudioMatrix_WithReEncodeSelfPairs() { var caps = new FfmpegProvider().Capability.SupportedConversions; Assert.Contains(caps, c => c.InputExtension == ".mp4" && c.OutputExtension == ".mkv"); Assert.Contains(caps, c => c.InputExtension == ".flac" && c.OutputExtension == ".mp3"); - Assert.DoesNotContain(caps, c => c.InputExtension == c.OutputExtension); // 자기쌍 없음 + // 동일 포맷 재인코딩(압축) self-edge: 비디오 컨테이너·오디오는 self-pair를 허용한다. + Assert.Contains(caps, c => c.InputExtension == ".mp4" && c.OutputExtension == ".mp4"); + Assert.Contains(caps, c => c.InputExtension == ".wav" && c.OutputExtension == ".wav"); + // gif는 self-edge에서 제외 — 이미지로 다뤄지며 ffmpeg 미설치 시 회귀를 막기 위함. + Assert.DoesNotContain(caps, c => c.InputExtension == ".gif" && c.OutputExtension == ".gif"); } }