diff --git a/src/Everything2Everything.App/ViewModels/OptionsViewModel.cs b/src/Everything2Everything.App/ViewModels/OptionsViewModel.cs
index 9946c33..23c6d92 100644
--- a/src/Everything2Everything.App/ViewModels/OptionsViewModel.cs
+++ b/src/Everything2Everything.App/ViewModels/OptionsViewModel.cs
@@ -25,7 +25,28 @@ public partial class OptionsViewModel : ObservableObject
[ObservableProperty] private bool _videoPreferGpu = true;
- /// 현재 상태로 불변 ConvertOptions를 구성한다(기존 MainWindow.BuildOptions와 동일 동작).
+ // ── 영상 인코딩 (출력이 영상 컨테이너일 때) ──────────────────────────────────────────────
+ [ObservableProperty] private int _videoCodecIndex; // 0=Auto,1=H264,2=H265,3=VP9,4=AV1,5=Copy
+ [ObservableProperty] private int _rateControlIndex; // 0=CRF,1=ABR,2=ConstrainedCRF,3=CBR,4=2pass
+ [ObservableProperty] private int _crf = 23; // 0~51
+ [ObservableProperty] private int _videoBitrateKbps = 8000;
+ [ObservableProperty] private int _presetIndex = 5; // 0=UltraFast..8=VerySlow(기본 Medium)
+ [ObservableProperty] private int _resolutionIndex; // 0=원본,1=2160p,2=1440p,3=1080p,4=720p,5=480p
+ [ObservableProperty] private int _fpsIndex; // 0=원본,1=24,2=30,3=60
+ [ObservableProperty] private int _rotateIndex; // 0=None,1=Cw90,2=Ccw90,3=180,4=FlipH,5=FlipV
+ [ObservableProperty] private int _deinterlaceIndex; // 0=Off,1=Yadif,2=Bwdif
+ [ObservableProperty] private bool _fastStart = true;
+ [ObservableProperty] private bool _spatialAq = true;
+
+ // ── 오디오 인코딩 (영상의 오디오 트랙 + 오디오 전용 출력) ─────────────────────────────────
+ [ObservableProperty] private int _audioCodecIndex; // 0=Auto,1=AAC,2=MP3,3=Opus,4=Vorbis,5=FLAC,6=PCM,7=Copy
+ [ObservableProperty] private int _audioBitrateIndex = 2; // 0=96,1=128,2=192,3=256,4=320
+ [ObservableProperty] private bool _audioVbr;
+ [ObservableProperty] private int _sampleRateIndex; // 0=원본,1=44100,2=48000
+ [ObservableProperty] private int _channelsIndex; // 0=원본,1=모노,2=스테레오
+ [ObservableProperty] private bool _loudnorm;
+
+ /// 현재 상태로 불변 ConvertOptions를 구성한다(기존 MainWindow.BuildOptions와 동일 동작 + 영상/오디오).
public ConvertOptions ToConvertOptions()
{
var hasCustom = !string.IsNullOrWhiteSpace(CustomOutputDirectory);
@@ -50,6 +71,34 @@ public partial class OptionsViewModel : ObservableObject
TargetLanguage = string.IsNullOrWhiteSpace(TargetLanguage) ? null : TargetLanguage.Trim(),
},
VideoPreferGpu = VideoPreferGpu,
+ Video = BuildVideo(),
+ Audio = BuildAudio(),
};
}
+
+ private VideoEncodeOptions BuildVideo() => new()
+ {
+ Codec = VideoCodecIndex switch { 1 => VideoCodec.H264, 2 => VideoCodec.H265, 3 => VideoCodec.Vp9, 4 => VideoCodec.Av1, 5 => VideoCodec.Copy, _ => VideoCodec.Auto },
+ RateControl = RateControlIndex switch { 1 => RateControlMode.AverageBitrate, 2 => RateControlMode.ConstrainedCrf, 3 => RateControlMode.Cbr, 4 => RateControlMode.TwoPass, _ => RateControlMode.Crf },
+ Crf = Math.Clamp(Crf, 0, 63),
+ VideoBitrateKbps = VideoBitrateKbps,
+ VideoMaxrateKbps = RateControlIndex == 2 ? VideoBitrateKbps : null, // 제약CRF는 비트레이트 입력을 상한으로
+ Preset = (VideoSpeedPreset)Math.Clamp(PresetIndex, 0, 8),
+ ScaleWidth = ResolutionIndex switch { 1 => 3840, 2 => 2560, 3 => 1920, 4 => 1280, 5 => 854, _ => (int?)null },
+ Fps = FpsIndex switch { 1 => 24.0, 2 => 30.0, 3 => 60.0, _ => (double?)null },
+ Rotate = (RotateMode)Math.Clamp(RotateIndex, 0, 5),
+ Deinterlace = (DeinterlaceMode)Math.Clamp(DeinterlaceIndex, 0, 2),
+ FastStart = FastStart,
+ SpatialAq = SpatialAq,
+ };
+
+ private AudioEncodeOptions BuildAudio() => new()
+ {
+ Codec = AudioCodecIndex switch { 1 => AudioCodec.Aac, 2 => AudioCodec.Mp3, 3 => AudioCodec.Opus, 4 => AudioCodec.Vorbis, 5 => AudioCodec.Flac, 6 => AudioCodec.Pcm, 7 => AudioCodec.Copy, _ => AudioCodec.Auto },
+ RateMode = AudioVbr ? AudioRateMode.Vbr : AudioRateMode.Bitrate,
+ AudioBitrateKbps = AudioBitrateIndex switch { 0 => 96, 1 => 128, 3 => 256, 4 => 320, _ => 192 },
+ SampleRate = SampleRateIndex switch { 1 => 44100, 2 => 48000, _ => (int?)null },
+ Channels = ChannelsIndex switch { 1 => 1, 2 => 2, _ => (int?)null },
+ Loudnorm = Loudnorm,
+ };
}
diff --git a/src/Everything2Everything.App/Views/MainWindow.xaml b/src/Everything2Everything.App/Views/MainWindow.xaml
index 259136b..eb47f72 100644
--- a/src/Everything2Everything.App/Views/MainWindow.xaml
+++ b/src/Everything2Everything.App/Views/MainWindow.xaml
@@ -244,6 +244,153 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
"AVIF QUALITY",
_ => "ENCODING QUALITY",
};
+
+ UpdateMediaPanelForFormat(extension);
+ }
+
+ /// 출력이 영상/오디오일 때만 영상·오디오 인코딩 패널을 노출(영상이면 영상+오디오, 오디오면 오디오만).
+ private void UpdateMediaPanelForFormat(string? extension)
+ {
+ if (MediaPanel is null) return;
+ var ext = (extension ?? string.Empty).ToLowerInvariant();
+ var isVideo = ext is ".mp4" or ".mkv" or ".webm" or ".mov" or ".avi";
+ var isAudio = ext is ".mp3" or ".aac" or ".m4a" or ".opus" or ".ogg" or ".flac" or ".wav";
+ MediaPanel.Visibility = (isVideo || isAudio) ? Visibility.Visible : Visibility.Collapsed;
+ VideoSubPanel.Visibility = isVideo ? Visibility.Visible : Visibility.Collapsed;
+ AudioSubPanel.Visibility = (isVideo || isAudio) ? Visibility.Visible : Visibility.Collapsed;
}
private void UpdateOutputDestHint(string? extension)