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");
}
}