Refactor Encoder Detection

This commit is contained in:
copecone committed 2026-08-08 17:46:36 +09:00
1 parent 2fe09c71ae
commit d4b5352fb9
3 files changed
+159 -57

No files matched your search

+38 -38
View File
@@ -282,7 +282,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
} }
/// <summary> /// <summary>
/// 선택된 코덱에 대해 시도할 인코더 이름 목록을 우선순위 순으로 반환합니다. /// 선택된 코덱에 대해 시도할 인코더 목록을 우선순위 순으로 반환합니다.
/// ///
/// 우선순위: 하드웨어 인코더(NVENC → AMF → QSV) → 소프트웨어 인코더(libx264 등) /// 우선순위: 하드웨어 인코더(NVENC → AMF → QSV) → 소프트웨어 인코더(libx264 등)
/// ///
@@ -293,9 +293,9 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// ///
/// Start()에서 이 목록을 순회하며 첫 번째로 구동 가능한 인코더를 사용합니다. /// Start()에서 이 목록을 순회하며 첫 번째로 구동 가능한 인코더를 사용합니다.
/// </summary> /// </summary>
private string[] GetEncoderCandidates() private VideoEncoder[] GetEncoderCandidates()
{ {
return GPUUtil.GetPrioritizedEncoderCandidates(_settings.Codec, _settings.ForceSoftware).ToArray(); return GPUUtil.GetPrioritizedEncoders(_settings.Codec, _settings.ForceSoftware).ToArray();
} }
/// <summary> /// <summary>
@@ -366,12 +366,12 @@ namespace ADOFAIRenderer.Util.FFmpeg
var candidates = GetEncoderCandidates(); var candidates = GetEncoderCandidates();
var openedSuccessfully = false; var openedSuccessfully = false;
foreach (var encoderName in candidates) foreach (var encoder in candidates)
{ {
// avcodec_find_encoder_by_name: 이름(예: "h264_nvenc")으로 인코더를 검색합니다. // avcodec_find_encoder_by_name: 이름(예: "h264_nvenc")으로 인코더를 검색합니다.
// 해당 인코더가 빌드에 포함되어 있지 않으면 null을 반환합니다. // 해당 인코더가 빌드에 포함되어 있지 않으면 null을 반환합니다.
// 참고: https://ffmpeg.org/doxygen/trunk/group__lavc__encoding.html // 참고: https://ffmpeg.org/doxygen/trunk/group__lavc__encoding.html
var pCodec = ffmpeg.avcodec_find_encoder_by_name(encoderName); var pCodec = ffmpeg.avcodec_find_encoder_by_name(encoder.Name);
if (pCodec == null) continue; if (pCodec == null) continue;
// avcodec_alloc_context3: 선택된 인코더용 코덱 컨텍스트를 할당합니다. // avcodec_alloc_context3: 선택된 인코더용 코덱 컨텍스트를 할당합니다.
@@ -402,17 +402,17 @@ namespace ADOFAIRenderer.Util.FFmpeg
// //
// 참고: https://ffmpeg.org/doxygen/trunk/pixfmt_8h.html // 참고: https://ffmpeg.org/doxygen/trunk/pixfmt_8h.html
var selectedPixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P; var selectedPixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P;
if (encoderName.Contains("qsv")) if (encoder.Kind == EncoderKind.QSV)
{ {
selectedPixFmt = AVPixelFormat.AV_PIX_FMT_NV12; selectedPixFmt = AVPixelFormat.AV_PIX_FMT_NV12;
_pCodecContext->pix_fmt = AVPixelFormat.AV_PIX_FMT_QSV; _pCodecContext->pix_fmt = AVPixelFormat.AV_PIX_FMT_QSV;
} }
else if (encoderName.Contains("nvenc") || encoderName.Contains("amf")) else if (encoder.Kind is EncoderKind.NVENC or EncoderKind.AMF)
{ {
selectedPixFmt = AVPixelFormat.AV_PIX_FMT_NV12; selectedPixFmt = AVPixelFormat.AV_PIX_FMT_NV12;
_pCodecContext->pix_fmt = selectedPixFmt; _pCodecContext->pix_fmt = selectedPixFmt;
} }
else if (encoderName == "libvvenc") else if (encoder == VideoEncoder.LibVvenc)
{ {
// libvvenc: 10비트 YUV420P10LE만 지원 (이 빌드에서 yuv420p 미지원 확인됨) // libvvenc: 10비트 YUV420P10LE만 지원 (이 빌드에서 yuv420p 미지원 확인됨)
selectedPixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P10LE; selectedPixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P10LE;
@@ -452,7 +452,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
// 키프레임은 완전한 이미지를 담고 있어 탐색(seek) 지점이 됩니다. // 키프레임은 완전한 이미지를 담고 있어 탐색(seek) 지점이 됩니다.
// 너무 크면 탐색 정밀도가 떨어지고, 너무 작으면 파일 크기가 증가합니다. // 너무 크면 탐색 정밀도가 떨어지고, 너무 작으면 파일 크기가 증가합니다.
var calculatedGop = Math.Max(1, (int)Math.Round(_settings.FPS * _settings.GopSize)); var calculatedGop = Math.Max(1, (int)Math.Round(_settings.FPS * _settings.GopSize));
if (encoderName == "libvvenc") if (encoder == VideoEncoder.LibVvenc)
{ {
// libvvenc (VVenC) requires IntraPeriod (gop_size) to be a multiple of its GOPSize (typically 32). // libvvenc (VVenC) requires IntraPeriod (gop_size) to be a multiple of its GOPSize (typically 32).
_pCodecContext->gop_size = Math.Max(32, ((calculatedGop + 15) / 32) * 32); _pCodecContext->gop_size = Math.Max(32, ((calculatedGop + 15) / 32) * 32);
@@ -473,11 +473,11 @@ namespace ADOFAIRenderer.Util.FFmpeg
// SVT-AV1 특수 사항: max_b_frames를 AVCodecContext에서 직접 설정하면 // SVT-AV1 특수 사항: max_b_frames를 AVCodecContext에서 직접 설정하면
// "bad parameter" 에러가 발생하므로, svtav1-params를 통해 제어합니다. // "bad parameter" 에러가 발생하므로, svtav1-params를 통해 제어합니다.
// pred-struct=1: Low Delay (IPPP) 구조로 B-프레임 비활성화 // pred-struct=1: Low Delay (IPPP) 구조로 B-프레임 비활성화
if (encoderName == "libsvtav1") if (encoder == VideoEncoder.LibSvtAv1)
{ {
ffmpeg.av_opt_set(_pCodecContext->priv_data, "svtav1-params", "pred-struct=1", 0); ffmpeg.av_opt_set(_pCodecContext->priv_data, "svtav1-params", "pred-struct=1", 0);
} }
else if (encoderName == "libvvenc") else if (encoder == VideoEncoder.LibVvenc)
{ {
// libvvenc relies on hierarchical GOP structures; forcing max_b_frames = 0 causes initialization failure. // libvvenc relies on hierarchical GOP structures; forcing max_b_frames = 0 causes initialization failure.
// We explicitly set max_b_frames to -1 to allow libvvenc to use its default GOP reference distance. // We explicitly set max_b_frames to -1 to allow libvvenc to use its default GOP reference distance.
@@ -500,7 +500,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
} }
// ── 품질/비트레이트 설정 적용 ── // ── 품질/비트레이트 설정 적용 ──
ApplyQualitySettings(encoderName); ApplyQualitySettings(encoder);
// ── QSV 인코더 하드웨어 디바이스 초기화 ── // ── QSV 인코더 하드웨어 디바이스 초기화 ──
// QSV(Quick Sync Video)는 Intel GPU의 고정 기능 인코딩 하드웨어를 사용합니다. // QSV(Quick Sync Video)는 Intel GPU의 고정 기능 인코딩 하드웨어를 사용합니다.
@@ -519,7 +519,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
// hw_device_ctx: 시스템 메모리에서 인코더가 직접 프레임을 읽음 (호환성 우수) // hw_device_ctx: 시스템 메모리에서 인코더가 직접 프레임을 읽음 (호환성 우수)
// //
// 참고: https://ffmpeg.org/doxygen/trunk/group__lavu__hwcontext.html // 참고: https://ffmpeg.org/doxygen/trunk/group__lavu__hwcontext.html
if (encoderName.Contains("qsv")) if (encoder.Kind == EncoderKind.QSV)
{ {
AVBufferRef* hwDeviceCtx = null; AVBufferRef* hwDeviceCtx = null;
var qsvReady = false; var qsvReady = false;
@@ -540,11 +540,11 @@ namespace ADOFAIRenderer.Util.FFmpeg
if (err >= 0) if (err >= 0)
{ {
qsvReady = true; qsvReady = true;
Melon<RenderMod>.Logger.Msg($"{encoderName}: child_device_type={childDeviceName} 방식으로 QSV 디바이스 생성 성공"); Melon<RenderMod>.Logger.Msg($"{encoder.Name}: child_device_type={childDeviceName} 방식으로 QSV 디바이스 생성 성공");
} }
else else
{ {
Melon<RenderMod>.Logger.Warning($"{encoderName}: child_device_type 방식 실패 — {GetFFmpegErrorString(err)} (Code: {err})"); Melon<RenderMod>.Logger.Warning($"{encoder.Name}: child_device_type 방식 실패 — {GetFFmpegErrorString(err)} (Code: {err})");
} }
} }
@@ -561,7 +561,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
if (err >= 0) if (err >= 0)
{ {
qsvReady = true; qsvReady = true;
Melon<RenderMod>.Logger.Msg($"{encoderName}: {childDeviceType} → QSV 파생 방식으로 디바이스 생성 성공"); Melon<RenderMod>.Logger.Msg($"{encoder.Name}: {childDeviceType} → QSV 파생 방식으로 디바이스 생성 성공");
} }
ffmpeg.av_buffer_unref(&childDeviceCtx); ffmpeg.av_buffer_unref(&childDeviceCtx);
} }
@@ -576,7 +576,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
if (!qsvReady) if (!qsvReady)
{ {
Melon<RenderMod>.Logger.Warning($"{encoderName}: 모든 QSV 디바이스 생성 전략 실패. 다음 후보 코덱을 테스트합니다."); Melon<RenderMod>.Logger.Warning($"{encoder.Name}: 모든 QSV 디바이스 생성 전략 실패. 다음 후보 코덱을 테스트합니다.");
{ {
var pCtx = _pCodecContext; var pCtx = _pCodecContext;
ffmpeg.avcodec_free_context(&pCtx); ffmpeg.avcodec_free_context(&pCtx);
@@ -618,7 +618,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
_pCodecContext->hw_frames_ctx = ffmpeg.av_buffer_ref(hwFramesRef); _pCodecContext->hw_frames_ctx = ffmpeg.av_buffer_ref(hwFramesRef);
_isQsvEncoder = true; _isQsvEncoder = true;
framesCtxReady = true; framesCtxReady = true;
Melon<RenderMod>.Logger.Msg($"{encoderName}: hw_frames_ctx 초기화 성공 (GPU 프레임 풀 모드)"); Melon<RenderMod>.Logger.Msg($"{encoder.Name}: hw_frames_ctx 초기화 성공 (GPU 프레임 풀 모드)");
_pHwFrame = ffmpeg.av_frame_alloc(); _pHwFrame = ffmpeg.av_frame_alloc();
if (_pHwFrame == null) if (_pHwFrame == null)
@@ -626,7 +626,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
} }
else else
{ {
Melon<RenderMod>.Logger.Warning($"{encoderName}: hw_frames_ctx 실패 — {GetFFmpegErrorString(err)} (Code: {err}). hw_device_ctx 모드로 폴백합니다."); Melon<RenderMod>.Logger.Warning($"{encoder.Name}: hw_frames_ctx 실패 — {GetFFmpegErrorString(err)} (Code: {err}). hw_device_ctx 모드로 폴백합니다.");
} }
ffmpeg.av_buffer_unref(&hwFramesRef); ffmpeg.av_buffer_unref(&hwFramesRef);
} }
@@ -657,30 +657,30 @@ namespace ADOFAIRenderer.Util.FFmpeg
// 느린 프리셋: 인코딩 느리지만 같은 품질에서 파일 크기 감소 // 느린 프리셋: 인코딩 느리지만 같은 품질에서 파일 크기 감소
// //
// 참고: https://ffmpeg.org/doxygen/trunk/group__opt__set__funcs.html // 참고: https://ffmpeg.org/doxygen/trunk/group__opt__set__funcs.html
if (encoderName.Contains("nvenc")) if (encoder.Kind == EncoderKind.NVENC)
{ {
// NVENC 프리셋 범위: p1(최고속) ~ p7(최고품질). p4 = 중간(default) // NVENC 프리셋 범위: p1(최고속) ~ p7(최고품질). p4 = 중간(default)
ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "p4", 0); ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "p4", 0);
} }
else if (encoderName.Contains("libx264") || encoderName.Contains("libx265")) else if (encoder == VideoEncoder.Libx264 || encoder == VideoEncoder.Libx265)
{ {
// x264/x265 프리셋: ultrafast, superfast, veryfast, faster, fast, medium, slow, ... // x264/x265 프리셋: ultrafast, superfast, veryfast, faster, fast, medium, slow, ...
// CPU 인코딩은 병목이 되기 쉬우므로 빠른 프리셋 사용 // CPU 인코딩은 병목이 되기 쉬우므로 빠른 프리셋 사용
ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "veryfast", 0); ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "veryfast", 0);
} }
else if (encoderName == "libsvtav1") else if (encoder == VideoEncoder.LibSvtAv1)
{ {
// SVT-AV1 프리셋: 0(최고 품질/최저 속도) ~ 13(최고 속도/최저 품질) // SVT-AV1 프리셋: 0(최고 품질/최저 속도) ~ 13(최고 속도/최저 품질)
ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "10", 0); ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "10", 0);
} }
else if (encoderName == "libaom-av1") else if (encoder == VideoEncoder.LibAomAv1)
{ {
// libaom cpu-used: 0(최고 품질) ~ 8(최고 속도) // libaom cpu-used: 0(최고 품질) ~ 8(최고 속도)
ffmpeg.av_opt_set(_pCodecContext->priv_data, "cpu-used", "6", 0); ffmpeg.av_opt_set(_pCodecContext->priv_data, "cpu-used", "6", 0);
// "realtime" usage: 단일 패스, 저지연 인코딩 모드 // "realtime" usage: 단일 패스, 저지연 인코딩 모드
ffmpeg.av_opt_set(_pCodecContext->priv_data, "usage", "realtime", 0); ffmpeg.av_opt_set(_pCodecContext->priv_data, "usage", "realtime", 0);
} }
else if (encoderName == "libvvenc") else if (encoder == VideoEncoder.LibVvenc)
{ {
// libvvenc 프리셋: slower, slow, medium, fast, faster. 디폴트가 medium이므로 faster로 변경하여 성능 확보 // libvvenc 프리셋: slower, slow, medium, fast, faster. 디폴트가 medium이므로 faster로 변경하여 성능 확보
ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "faster", 0); ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "faster", 0);
@@ -693,20 +693,20 @@ namespace ADOFAIRenderer.Util.FFmpeg
// 성공하면 0 이상을 반환하고, 이후 avcodec_send_frame/avcodec_receive_packet으로 // 성공하면 0 이상을 반환하고, 이후 avcodec_send_frame/avcodec_receive_packet으로
// 프레임 인코딩이 가능해집니다. // 프레임 인코딩이 가능해집니다.
// 참고: https://ffmpeg.org/doxygen/trunk/group__lavc__core.html // 참고: https://ffmpeg.org/doxygen/trunk/group__lavc__core.html
Melon<RenderMod>.Logger.Msg($"[인코더 진단] {encoderName}: {_pCodecContext->width}x{_pCodecContext->height}, pix_fmt={_pCodecContext->pix_fmt}, gop={_pCodecContext->gop_size}, max_b_frames={_pCodecContext->max_b_frames}, framerate={_pCodecContext->framerate.num}/{_pCodecContext->framerate.den}, thread_count={_pCodecContext->thread_count}, bit_rate={_pCodecContext->bit_rate}, rc_max_rate={_pCodecContext->rc_max_rate}, rc_buffer_size={_pCodecContext->rc_buffer_size}, mode={_settings.Mode}"); Melon<RenderMod>.Logger.Msg($"[인코더 진단] {encoder.Name}: {_pCodecContext->width}x{_pCodecContext->height}, pix_fmt={_pCodecContext->pix_fmt}, gop={_pCodecContext->gop_size}, max_b_frames={_pCodecContext->max_b_frames}, framerate={_pCodecContext->framerate.num}/{_pCodecContext->framerate.den}, thread_count={_pCodecContext->thread_count}, bit_rate={_pCodecContext->bit_rate}, rc_max_rate={_pCodecContext->rc_max_rate}, rc_buffer_size={_pCodecContext->rc_buffer_size}, mode={_settings.Mode}");
var openResult = ffmpeg.avcodec_open2(_pCodecContext, pCodec, null); var openResult = ffmpeg.avcodec_open2(_pCodecContext, pCodec, null);
if (openResult >= 0) if (openResult >= 0)
{ {
bool isHardware = encoderName.Contains("nvenc") || encoderName.Contains("amf") || encoderName.Contains("qsv"); var isHardware = encoder.IsHardware;
string accelStatus = isHardware ? "하드웨어 가속 활성화" : "소프트웨어 CPU 사용"; var accelStatus = isHardware ? "하드웨어 가속 활성화" : "소프트웨어 CPU 사용";
Melon<RenderMod>.Logger.Msg($"비디오 인코더 구동 성공: {encoderName} ({accelStatus})"); Melon<RenderMod>.Logger.Msg($"비디오 인코더 구동 성공: {encoder.Name} ({accelStatus})");
_dstPixFmt = selectedPixFmt; _dstPixFmt = selectedPixFmt;
openedSuccessfully = true; openedSuccessfully = true;
break; break;
} }
// 구동 실패 시 코덱 컨텍스트를 해제하고 다음 인코더를 시도합니다. // 구동 실패 시 코덱 컨텍스트를 해제하고 다음 인코더를 시도합니다.
Melon<RenderMod>.Logger.Warning($"{encoderName} 하드웨어 인코더 구동 실패: {GetFFmpegErrorString(openResult)} (Code: {openResult}). 다음 후보 코덱을 테스트합니다."); Melon<RenderMod>.Logger.Warning($"{encoder.Name} 하드웨어 인코더 구동 실패: {GetFFmpegErrorString(openResult)} (Code: {openResult}). 다음 후보 코덱을 테스트합니다.");
var pCodecCtx = _pCodecContext; var pCodecCtx = _pCodecContext;
ffmpeg.avcodec_free_context(&pCodecCtx); ffmpeg.avcodec_free_context(&pCodecCtx);
_pCodecContext = null; _pCodecContext = null;
@@ -832,7 +832,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// ///
/// 참고: https://ffmpeg.org/doxygen/trunk/structAVCodecContext.html /// 참고: https://ffmpeg.org/doxygen/trunk/structAVCodecContext.html
/// </summary> /// </summary>
private void ApplyQualitySettings(string encoderName) private void ApplyQualitySettings(VideoEncoder encoder)
{ {
switch (_settings.Mode) switch (_settings.Mode)
{ {
@@ -842,7 +842,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
// libvvenc는 rc_min_rate/rc_max_rate/rc_buffer_size를 지원하지 않으므로 // libvvenc는 rc_min_rate/rc_max_rate/rc_buffer_size를 지원하지 않으므로
// bit_rate만 설정하고 나머지는 내부 rate control에 위임합니다. // bit_rate만 설정하고 나머지는 내부 rate control에 위임합니다.
if (encoderName != "libvvenc") if (encoder != VideoEncoder.LibVvenc)
{ {
// CBR에서는 목표/최대/최소 비트레이트를 모두 동일하게 설정합니다. // CBR에서는 목표/최대/최소 비트레이트를 모두 동일하게 설정합니다.
// rc_buffer_size는 비트레이트 버퍼링의 크기를 결정합니다. // rc_buffer_size는 비트레이트 버퍼링의 크기를 결정합니다.
@@ -851,7 +851,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
// nal-hrd=cbr: x264/x265 전용 옵션. NAL HRD(Hypothetical Reference Decoder) 규격에 // nal-hrd=cbr: x264/x265 전용 옵션. NAL HRD(Hypothetical Reference Decoder) 규격에
// 맞는 CBR 비트스트림을 생성합니다. 스트리밍 호환성을 위해 필요할 수 있습니다. // 맞는 CBR 비트스트림을 생성합니다. 스트리밍 호환성을 위해 필요할 수 있습니다.
if (encoderName.Contains("libx264") || encoderName.Contains("libx265")) if (encoder == VideoEncoder.Libx264 || encoder == VideoEncoder.Libx265)
{ {
ffmpeg.av_opt_set(_pCodecContext->priv_data, "nal-hrd", "cbr", 0); ffmpeg.av_opt_set(_pCodecContext->priv_data, "nal-hrd", "cbr", 0);
} }
@@ -862,8 +862,8 @@ namespace ADOFAIRenderer.Util.FFmpeg
// kbps → bps 변환 // kbps → bps 변환
_pCodecContext->bit_rate = _settings.TargetBitrate * 1000L; _pCodecContext->bit_rate = _settings.TargetBitrate * 1000L;
// libvvenc는 rc_max_rate/rc_buffer_size를 지원하지 않으므로 건너릴니다. // libvvenc는 rc_max_rate/rc_buffer_size를 지원하지 않으므로 건너뜁니다.
if (encoderName != "libvvenc") if (encoder != VideoEncoder.LibVvenc)
{ {
// MaxBitrate가 TargetBitrate보다 큰 경우 그대로 사용, // MaxBitrate가 TargetBitrate보다 큰 경우 그대로 사용,
// 그렇지 않으면 TargetBitrate의 1.5배를 최대값으로 설정합니다. // 그렇지 않으면 TargetBitrate의 1.5배를 최대값으로 설정합니다.
@@ -882,24 +882,24 @@ namespace ADOFAIRenderer.Util.FFmpeg
// 다른 코덱에서도 비슷한 수준의 품질을 제공합니다. // 다른 코덱에서도 비슷한 수준의 품질을 제공합니다.
var qpValue = _settings.QualityValue is >= 0 and <= 51 ? _settings.QualityValue : 18; var qpValue = _settings.QualityValue is >= 0 and <= 51 ? _settings.QualityValue : 18;
if (encoderName.Contains("nvenc")) if (encoder.Kind == EncoderKind.NVENC)
{ {
// NVENC: "qp" 옵션 하나로 I/P/B 프레임 모두에 동일한 QP 적용 // NVENC: "qp" 옵션 하나로 I/P/B 프레임 모두에 동일한 QP 적용
ffmpeg.av_opt_set(_pCodecContext->priv_data, "qp", qpValue.ToString(), 0); ffmpeg.av_opt_set(_pCodecContext->priv_data, "qp", qpValue.ToString(), 0);
} }
else if (encoderName.Contains("amf")) else if (encoder.Kind == EncoderKind.AMF)
{ {
// AMF: I프레임(qp_i)과 P프레임(qp_p)의 QP를 별도로 설정해야 합니다. // AMF: I프레임(qp_i)과 P프레임(qp_p)의 QP를 별도로 설정해야 합니다.
// B-프레임은 비활성화했으므로 qp_b는 설정하지 않습니다. // B-프레임은 비활성화했으므로 qp_b는 설정하지 않습니다.
ffmpeg.av_opt_set(_pCodecContext->priv_data, "qp_i", qpValue.ToString(), 0); ffmpeg.av_opt_set(_pCodecContext->priv_data, "qp_i", qpValue.ToString(), 0);
ffmpeg.av_opt_set(_pCodecContext->priv_data, "qp_p", qpValue.ToString(), 0); ffmpeg.av_opt_set(_pCodecContext->priv_data, "qp_p", qpValue.ToString(), 0);
} }
else if (encoderName.Contains("qsv")) else if (encoder.Kind == EncoderKind.QSV)
{ {
// QSV: "global_quality" 옵션으로 ICQ(Intelligent Constant Quality) 모드 사용 // QSV: "global_quality" 옵션으로 ICQ(Intelligent Constant Quality) 모드 사용
ffmpeg.av_opt_set(_pCodecContext->priv_data, "global_quality", qpValue.ToString(), 0); ffmpeg.av_opt_set(_pCodecContext->priv_data, "global_quality", qpValue.ToString(), 0);
} }
else if (encoderName == "libvvenc") else if (encoder == VideoEncoder.LibVvenc)
{ {
// libvvenc: "qp" 옵션으로 Constant QP 모드 지정 // libvvenc: "qp" 옵션으로 Constant QP 모드 지정
ffmpeg.av_opt_set(_pCodecContext->priv_data, "qp", qpValue.ToString(), 0); ffmpeg.av_opt_set(_pCodecContext->priv_data, "qp", qpValue.ToString(), 0);
+113
View File
@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace ADOFAIRenderer.Util.FFmpeg
{
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum EncoderKind
{
Software,
NVENC,
AMF,
QSV,
VideoToolbox
}
[SuppressMessage("ReSharper", "InconsistentNaming")]
public sealed class VideoEncoder : IEquatable<VideoEncoder>
{
// ReSharper disable MemberCanBePrivate.Global
// ── H.264 ──
public static readonly VideoEncoder H264_NVENC = new("h264_nvenc", Codec.H264, GPUVendor.NVIDIA, EncoderKind.NVENC);
public static readonly VideoEncoder H264_QSV = new("h264_qsv", Codec.H264, GPUVendor.Intel, EncoderKind.QSV);
public static readonly VideoEncoder H264_AMF = new("h264_amf", Codec.H264, GPUVendor.AMD, EncoderKind.AMF);
public static readonly VideoEncoder H264_VideoToolbox = new("h264_videotoolbox", Codec.H264, GPUVendor.Apple, EncoderKind.VideoToolbox);
public static readonly VideoEncoder Libx264 = new("libx264", Codec.H264, GPUVendor.Unknown, EncoderKind.Software);
// ── H.265 (HEVC) ──
public static readonly VideoEncoder HEVC_NVENC = new("hevc_nvenc", Codec.H265, GPUVendor.NVIDIA, EncoderKind.NVENC);
public static readonly VideoEncoder HEVC_QSV = new("hevc_qsv", Codec.H265, GPUVendor.Intel, EncoderKind.QSV);
public static readonly VideoEncoder HEVC_AMF = new("hevc_amf", Codec.H265, GPUVendor.AMD, EncoderKind.AMF);
public static readonly VideoEncoder HEVC_VideoToolbox = new("hevc_videotoolbox", Codec.H265, GPUVendor.Apple, EncoderKind.VideoToolbox);
public static readonly VideoEncoder Libx265 = new("libx265", Codec.H265, GPUVendor.Unknown, EncoderKind.Software);
// ── VP9 ──
public static readonly VideoEncoder VP9_NVENC = new("vp9_nvenc", Codec.VP9, GPUVendor.NVIDIA, EncoderKind.NVENC);
public static readonly VideoEncoder VP9_QSV = new("vp9_qsv", Codec.VP9, GPUVendor.Intel, EncoderKind.QSV);
public static readonly VideoEncoder LibvpxVP9 = new("libvpx-vp9", Codec.VP9, GPUVendor.Unknown, EncoderKind.Software);
// ── AV1 ──
public static readonly VideoEncoder AV1_NVENC = new("av1_nvenc", Codec.AV1, GPUVendor.NVIDIA, EncoderKind.NVENC);
public static readonly VideoEncoder AV1_QSV = new("av1_qsv", Codec.AV1, GPUVendor.Intel, EncoderKind.QSV);
public static readonly VideoEncoder AV1_AMF = new("av1_amf", Codec.AV1, GPUVendor.AMD, EncoderKind.AMF);
public static readonly VideoEncoder LibSvtAv1 = new("libsvtav1", Codec.AV1, GPUVendor.Unknown, EncoderKind.Software);
public static readonly VideoEncoder LibAomAv1 = new("libaom-av1", Codec.AV1, GPUVendor.Unknown, EncoderKind.Software);
// ── VVC ──
public static readonly VideoEncoder LibVvenc = new("libvvenc", Codec.VVC, GPUVendor.Unknown, EncoderKind.Software);
// ReSharper restore MemberCanBePrivate.Global
public static readonly IReadOnlyList<VideoEncoder> All = new[]
{
H264_NVENC, H264_QSV, H264_AMF, H264_VideoToolbox, Libx264,
HEVC_NVENC, HEVC_QSV, HEVC_AMF, HEVC_VideoToolbox, Libx265,
VP9_NVENC, VP9_QSV, LibvpxVP9,
AV1_NVENC, AV1_QSV, AV1_AMF, LibSvtAv1, LibAomAv1,
LibVvenc
};
private static readonly Dictionary<string, VideoEncoder> ByName =
All.ToDictionary(e => e.Name, StringComparer.OrdinalIgnoreCase);
public string Name { get; }
public Codec Codec { get; }
public GPUVendor Vendor { get; }
public EncoderKind Kind { get; }
public bool IsHardware => Kind != EncoderKind.Software;
private VideoEncoder(string name, Codec codec, GPUVendor vendor, EncoderKind kind)
{
Name = name;
Codec = codec;
Vendor = vendor;
Kind = kind;
}
public static VideoEncoder? FromName(string name)
{
return string.IsNullOrEmpty(name) ? null : ByName.GetValueOrDefault(name);
}
public bool Equals(VideoEncoder other)
{
if (ReferenceEquals(null, other)) return false;
return ReferenceEquals(this, other) || string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase);
}
public override bool Equals(object? obj)
{
return ReferenceEquals(this, obj) || obj is VideoEncoder other && Equals(other);
}
public override int GetHashCode()
{
return StringComparer.OrdinalIgnoreCase.GetHashCode(Name);
}
public static bool operator ==(VideoEncoder left, VideoEncoder right)
{
return Equals(left, right);
}
public static bool operator !=(VideoEncoder left, VideoEncoder right)
{
return !Equals(left, right);
}
public override string ToString() => Name;
}
}
+8 -19
View File
@@ -25,15 +25,6 @@ namespace ADOFAIRenderer.Util
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
private const string AMDPattern = private const string AMDPattern =
@"^AMD Radeon RX [0-9]+(?> XT)?$"; @"^AMD Radeon RX [0-9]+(?> XT)?$";
private static readonly Dictionary<Codec, List<(GPUVendor, string)>> CodecMap = new()
{
[Codec.H264] = new List<(GPUVendor, string)> { (GPUVendor.NVIDIA, "h264_nvenc"), (GPUVendor.Intel, "h264_qsv"), (GPUVendor.AMD, "h264_amf"), (GPUVendor.Apple, "h264_videotoolbox"), (GPUVendor.Unknown, "libx264") },
[Codec.H265] = new List<(GPUVendor, string)> { (GPUVendor.NVIDIA, "hevc_nvenc"), (GPUVendor.Intel, "hevc_qsv"), (GPUVendor.AMD, "hevc_amf"), (GPUVendor.Apple, "hevc_videotoolbox"), (GPUVendor.Unknown, "libx265") },
[Codec.VP9] = new List<(GPUVendor, string)> { (GPUVendor.NVIDIA, "vp9_nvenc"), (GPUVendor.Intel, "vp9_qsv"), (GPUVendor.Unknown, "libvpx-vp9") },
[Codec.AV1] = new List<(GPUVendor, string)> { (GPUVendor.NVIDIA, "av1_nvenc"), (GPUVendor.Intel, "av1_qsv"), (GPUVendor.AMD, "av1_amf"), (GPUVendor.Unknown, "libsvtav1"), (GPUVendor.Unknown, "libaom-av1") },
[Codec.VVC] = new List<(GPUVendor, string)> { (GPUVendor.Unknown, "libvvenc") }
};
private static GPUVendor _cachedVendor = GPUVendor.Unknown; private static GPUVendor _cachedVendor = GPUVendor.Unknown;
private static bool _hasCachedVendor; private static bool _hasCachedVendor;
@@ -137,24 +128,22 @@ namespace ADOFAIRenderer.Util
return null; return null;
} }
public static List<string> GetPrioritizedEncoderCandidates(Codec codec, bool forceSoftware = false) public static List<VideoEncoder> GetPrioritizedEncoders(Codec codec, bool forceSoftware = false)
{ {
if (!CodecMap.TryGetValue(codec, out var list)) return new List<string>(); var encoders = VideoEncoder.All.Where(e => e.Codec == codec).ToList();
var prioritized = new List<string>();
if (forceSoftware) if (forceSoftware)
{ {
prioritized.AddRange(from item in list where item.Item1 == GPUVendor.Unknown select item.Item2); return encoders.Where(e => !e.IsHardware).ToList();
return prioritized;
} }
var vendor = GetActiveGPUVendor(); var vendor = GetActiveGPUVendor();
var others = new List<string>(); var prioritized = new List<VideoEncoder>();
var others = new List<VideoEncoder>();
foreach (var item in list) foreach (var item in encoders)
{ {
if (item.Item1 == vendor) prioritized.Add(item.Item2); if (item.Vendor == vendor) prioritized.Add(item);
else others.Add(item.Item2); else others.Add(item);
} }
prioritized.AddRange(others); prioritized.AddRange(others);