Add CBR, VBR, and CQP Bitrate Control Options and Dynamic UI Generation

This commit is contained in:
copecone committed 2026-06-01 22:13:26 +09:00
1 parent d7ca416980
commit c6356a28f0
7 files changed
+222 -33

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+25 -3
View File
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Buffers; using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using ADOFAIRenderer.UI; using ADOFAIRenderer.UI;
using ADOFAIRenderer.Util; using ADOFAIRenderer.Util;
using ADOFAIRenderer.Util.FFmpeg; using ADOFAIRenderer.Util.FFmpeg;
@@ -52,6 +53,14 @@ namespace ADOFAIRenderer.Core
public int throttleCount; public int throttleCount;
public bool isThrottling; public bool isThrottling;
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum BitrateControlMode
{
CBR,
VBR,
CQP
}
public struct RenderSettings public struct RenderSettings
{ {
public double TargetFPS; public double TargetFPS;
@@ -60,13 +69,19 @@ namespace ADOFAIRenderer.Core
public int RenderWidth; public int RenderWidth;
public int RenderHeight; public int RenderHeight;
public BitrateControlMode BitrateMode;
public int Bitrate; public int Bitrate;
public int MaxBitrate;
public int QP;
public string Codec; public string Codec;
public double EndWaitTime; public double EndWaitTime;
public bool ShowHitText; public bool ShowHitText;
public int? RandomSeed; public int? RandomSeed;
public RenderSettings(double targetFPS, double videoFPS, int renderWidth, int renderHeight, int bitrate, string codec, double endWaitTime, bool showHitText, int? randomSeed) public RenderSettings(double targetFPS, double videoFPS, int renderWidth, int renderHeight,
BitrateControlMode bitrateMode, int bitrate, int maxBitrate, int qp,
string codec, double endWaitTime, bool showHitText, int? randomSeed)
{ {
TargetFPS = targetFPS; TargetFPS = targetFPS;
VideoFPS = videoFPS; VideoFPS = videoFPS;
@@ -74,7 +89,11 @@ namespace ADOFAIRenderer.Core
RenderWidth = renderWidth; RenderWidth = renderWidth;
RenderHeight = renderHeight; RenderHeight = renderHeight;
BitrateMode = bitrateMode;
Bitrate = bitrate; Bitrate = bitrate;
MaxBitrate = maxBitrate;
QP = qp;
Codec = codec; Codec = codec;
EndWaitTime = endWaitTime; EndWaitTime = endWaitTime;
ShowHitText = showHitText; ShowHitText = showHitText;
@@ -440,7 +459,10 @@ namespace ADOFAIRenderer.Core
var targetFps = Configuration.TargetFPS; var targetFps = Configuration.TargetFPS;
var videoFps = Configuration.VideoFPS; var videoFps = Configuration.VideoFPS;
float bitrate = Configuration.Bitrate; var bitrateMode = Configuration.BitrateMode;
var bitrate = Configuration.Bitrate;
var maxBitrate = Configuration.MaxBitrate;
var qp = Configuration.QP;
var codecStr = Configuration.Codec; var codecStr = Configuration.Codec;
var selectedCodec = VideoFFmpeg.Codec.H264_Default; var selectedCodec = VideoFFmpeg.Codec.H264_Default;
@@ -478,7 +500,7 @@ namespace ADOFAIRenderer.Core
{ {
_allVideoFFmpegInstances.Add(videoFFmpeg); _allVideoFFmpegInstances.Add(videoFFmpeg);
} }
videoFFmpeg.Start(_currentOutPath, bitrate); videoFFmpeg.Start(_currentOutPath, bitrateMode, bitrate, maxBitrate, qp);
while (true) while (true)
{ {
+159 -28
View File
@@ -15,16 +15,24 @@ namespace ADOFAIRenderer.UI
[SerializeField] private TMP_InputField videoFPSInput = null!; [SerializeField] private TMP_InputField videoFPSInput = null!;
[SerializeField] private TMP_InputField renderWidthInput = null!; [SerializeField] private TMP_InputField renderWidthInput = null!;
[SerializeField] private TMP_InputField renderHeightInput = null!; [SerializeField] private TMP_InputField renderHeightInput = null!;
[SerializeField] private TMP_InputField bitrateInput = null!;
[SerializeField] private TMP_InputField waitAfterEndInput = null!; [SerializeField] private TMP_InputField waitAfterEndInput = null!;
[SerializeField] private TMP_InputField randomSeedInput = null!; [SerializeField] private TMP_InputField randomSeedInput = null!;
[Header("Dropdown")] [Header("Dropdowns")]
[SerializeField] private TMP_Dropdown codecDropdown = null!; [SerializeField] private TMP_Dropdown codecDropdown = null!;
[SerializeField] private TMP_Dropdown bitrateModeDropdown = null!;
[Header("Toggle")]
[Header("Toggles")]
[SerializeField] private Toggle showHitTextToggle = null!; [SerializeField] private Toggle showHitTextToggle = null!;
[Header("Bitrate Dynamic UI Container")]
[SerializeField] private RectTransform bitrateContainer = null!;
// Dynamically instantiated inputs
private TMP_InputField _bitrateInput = null!;
private TMP_InputField _maxBitrateInput = null!;
private TMP_InputField _qpInput = null!;
private Button _buttonComponent = null!; private Button _buttonComponent = null!;
private void Start() private void Start()
@@ -34,11 +42,93 @@ namespace ADOFAIRenderer.UI
RenderMod.FixEventSystem(); RenderMod.FixEventSystem();
if (codecDropdown == null) return; if (codecDropdown != null)
codecDropdown.ClearOptions(); {
codecDropdown.ClearOptions();
var options = System.Enum.GetNames(typeof(VideoFFmpeg.Codec)).ToList();
codecDropdown.AddOptions(options);
}
// 1. Initialize Bitrate Mode Dropdown
if (bitrateModeDropdown != null)
{
bitrateModeDropdown.ClearOptions();
var modeOptions = System.Enum.GetNames(typeof(RenderManager.BitrateControlMode)).ToList();
bitrateModeDropdown.AddOptions(modeOptions);
bitrateModeDropdown.onValueChanged.AddListener(delegate { UpdateUIByMode(); });
}
// 2. Dynamically instantiate input fields using targetFPSInput as a styling template
if (bitrateContainer != null && targetFPSInput != null)
{
// Instantiate bitrateInput
var bitrateInputObj = Instantiate(targetFPSInput.gameObject, bitrateContainer);
bitrateInputObj.name = "Dynamic_BitrateInput";
_bitrateInput = bitrateInputObj.GetComponent<TMP_InputField>();
_bitrateInput.text = "15000";
SetPlaceholderText(_bitrateInput, "비트레이트 (kbps)");
// Instantiate maxBitrateInput
var maxBitrateInputObj = Instantiate(targetFPSInput.gameObject, bitrateContainer);
maxBitrateInputObj.name = "Dynamic_MaxBitrateInput";
_maxBitrateInput = maxBitrateInputObj.GetComponent<TMP_InputField>();
_maxBitrateInput.text = "30000";
SetPlaceholderText(_maxBitrateInput, "최대 비트레이트 (kbps)");
// Instantiate qpInput
var qpInputObj = Instantiate(targetFPSInput.gameObject, bitrateContainer);
qpInputObj.name = "Dynamic_QPInput";
_qpInput = qpInputObj.GetComponent<TMP_InputField>();
_qpInput.text = "18";
SetPlaceholderText(_qpInput, "QP (0-51) / CRF");
}
// 3. Perform initial UI state updates based on default mode
UpdateUIByMode();
}
private void SetPlaceholderText(TMP_InputField inputField, string text)
{
if (inputField.placeholder == null) return;
var tmpText = inputField.placeholder.GetComponent<TMP_Text>();
var options = System.Enum.GetNames(typeof(VideoFFmpeg.Codec)).ToList(); if (tmpText)
codecDropdown.AddOptions(options); {
tmpText.text = text;
}
}
private void UpdateUIByMode()
{
if (bitrateModeDropdown == null) return;
if (_bitrateInput == null || _maxBitrateInput == null || _qpInput == null) return;
var selectedMode = (RenderManager.BitrateControlMode)bitrateModeDropdown.value;
switch (selectedMode)
{
case RenderManager.BitrateControlMode.CBR:
_bitrateInput.gameObject.SetActive(true);
_maxBitrateInput.gameObject.SetActive(false);
_qpInput.gameObject.SetActive(false);
break;
case RenderManager.BitrateControlMode.VBR:
_bitrateInput.gameObject.SetActive(true);
_maxBitrateInput.gameObject.SetActive(true);
_qpInput.gameObject.SetActive(false);
break;
case RenderManager.BitrateControlMode.CQP:
_bitrateInput.gameObject.SetActive(false);
_maxBitrateInput.gameObject.SetActive(false);
_qpInput.gameObject.SetActive(true);
break;
}
if (bitrateContainer != null)
{
Canvas.ForceUpdateCanvases();
LayoutRebuilder.ForceRebuildLayoutImmediate(bitrateContainer);
}
} }
private void Update() private void Update()
@@ -74,11 +164,49 @@ namespace ADOFAIRenderer.UI
Melon<RenderMod>.Logger.Error($"올바르지 않은 Render Height 값입니다. 1에서 {SystemInfo.maxTextureSize} 사이의 정수여야 합니다."); Melon<RenderMod>.Logger.Error($"올바르지 않은 Render Height 값입니다. 1에서 {SystemInfo.maxTextureSize} 사이의 정수여야 합니다.");
return; return;
} }
if (!int.TryParse(bitrateInput.text, out var bitrate) || bitrate <= 0)
var bitrateMode = RenderManager.BitrateControlMode.CBR;
if (bitrateModeDropdown != null)
{ {
Melon<RenderMod>.Logger.Error("올바르지 않은 Bitrate 값입니다. 0보다 큰 정수여야 합니다."); bitrateMode = (RenderManager.BitrateControlMode)bitrateModeDropdown.value;
return;
} }
var bitrate = 0;
var maxBitrate = 0;
var qp = 18;
if (bitrateMode == RenderManager.BitrateControlMode.CBR || bitrateMode == RenderManager.BitrateControlMode.VBR)
{
if (!int.TryParse(_bitrateInput.text, out bitrate) || bitrate <= 0)
{
Melon<RenderMod>.Logger.Error("올바르지 않은 Bitrate 값입니다. 0보다 큰 정수여야 합니다.");
return;
}
}
if (bitrateMode == RenderManager.BitrateControlMode.VBR)
{
if (!int.TryParse(_maxBitrateInput.text, out maxBitrate) || maxBitrate <= 0)
{
Melon<RenderMod>.Logger.Error("올바르지 않은 Max Bitrate 값입니다. 0보다 큰 정수여야 합니다.");
return;
}
if (maxBitrate < bitrate)
{
Melon<RenderMod>.Logger.Error("Max Bitrate는 Target Bitrate보다 크거나 같아야 합니다.");
return;
}
}
if (bitrateMode == RenderManager.BitrateControlMode.CQP)
{
if (!int.TryParse(_qpInput.text, out qp) || qp < 0 || qp > 51)
{
Melon<RenderMod>.Logger.Error("올바르지 않은 QP 값입니다. 0에서 51 사이의 정수여야 합니다.");
return;
}
}
if (!double.TryParse(waitAfterEndInput.text, out var waitAfterEnd) || waitAfterEnd < 0) if (!double.TryParse(waitAfterEndInput.text, out var waitAfterEnd) || waitAfterEnd < 0)
{ {
Melon<RenderMod>.Logger.Error("올바르지 않은 Wait After End 값입니다. 0 이상의 숫자여야 합니다."); Melon<RenderMod>.Logger.Error("올바르지 않은 Wait After End 값입니다. 0 이상의 숫자여야 합니다.");
@@ -109,18 +237,20 @@ namespace ADOFAIRenderer.UI
if (RenderManager.Instance) if (RenderManager.Instance)
{ {
RenderManager.Instance.Configuration = new RenderManager.RenderSettings RenderManager.Instance.Configuration = new RenderManager.RenderSettings(
{ targetFPS,
TargetFPS = targetFPS, videoFPS,
VideoFPS = videoFPS, renderWidth,
RenderWidth = renderWidth, renderHeight,
RenderHeight = renderHeight, bitrateMode,
Bitrate = bitrate, bitrate,
Codec = selectedCodecName, maxBitrate,
EndWaitTime = waitAfterEnd, qp,
ShowHitText = showHitText, selectedCodecName,
RandomSeed = parsedRandomSeed waitAfterEnd,
}; showHitText,
parsedRandomSeed
);
} }
RenderManager.Instance!.SetupCaptureDirectory(); RenderManager.Instance!.SetupCaptureDirectory();
@@ -133,12 +263,13 @@ namespace ADOFAIRenderer.UI
if (string.IsNullOrWhiteSpace(videoFPSInput.text)) return true; if (string.IsNullOrWhiteSpace(videoFPSInput.text)) return true;
if (string.IsNullOrWhiteSpace(renderWidthInput.text)) return true; if (string.IsNullOrWhiteSpace(renderWidthInput.text)) return true;
if (string.IsNullOrWhiteSpace(renderHeightInput.text)) return true; if (string.IsNullOrWhiteSpace(renderHeightInput.text)) return true;
if (string.IsNullOrWhiteSpace(bitrateInput.text)) return true;
if (string.IsNullOrWhiteSpace(waitAfterEndInput.text)) return true; if (string.IsNullOrWhiteSpace(waitAfterEndInput.text)) return true;
// 드롭다운은 기본적으로 항상 무언가 선택되어 있지만(0번 항목 등), if (_bitrateInput != null && _bitrateInput.gameObject.activeSelf && string.IsNullOrWhiteSpace(_bitrateInput.text)) return true;
// 만약 '선택 안함(-1)' 같은 예외 처리가 필요하다면 아래처럼 검사할 수 있습니다. if (_maxBitrateInput != null && _maxBitrateInput.gameObject.activeSelf && string.IsNullOrWhiteSpace(_maxBitrateInput.text)) return true;
return codecDropdown.value < 0; if (_qpInput != null && _qpInput.gameObject.activeSelf && string.IsNullOrWhiteSpace(_qpInput.text)) return true;
return codecDropdown.value < 0 || (bitrateModeDropdown != null && bitrateModeDropdown.value < 0);
} }
} }
} }
+38 -2
View File
@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using ADOFAIRenderer.Core;
namespace ADOFAIRenderer.Util.FFmpeg namespace ADOFAIRenderer.Util.FFmpeg
{ {
@@ -55,14 +56,49 @@ namespace ADOFAIRenderer.Util.FFmpeg
VideoCodec = codec; VideoCodec = codec;
} }
public void Start(string path, float bitrate) public void Start(string path, RenderManager.BitrateControlMode mode, int bitrate, int maxBitrate, int qp)
{ {
if (File.Exists(path)) if (File.Exists(path))
{ {
File.Delete(path); File.Delete(path);
} }
var command = "-y -f rawvideo -vcodec rawvideo -pixel_format rgba -colorspace rgb -color_range 2 -video_size " + Width + "x" + Height + " -framerate " + FrameRate + " -threads 4 -loglevel quiet -i - " + Codec2String(VideoCodec) + " -pix_fmt yuv420p -fps_mode cfr " + " -r " + VideoFPS + $" -b:v {(int)bitrate}k" + " \"" + path + "\"";
string bitrateArgs = mode switch
{
RenderManager.BitrateControlMode.CBR => $"-b:v {bitrate}k -minrate {bitrate}k -maxrate {bitrate}k -bufsize {bitrate * 2}k",
RenderManager.BitrateControlMode.VBR => $"-b:v {bitrate}k -maxrate {maxBitrate}k -bufsize {maxBitrate * 2}k",
RenderManager.BitrateControlMode.CQP => GetCqpArgs(VideoCodec, qp),
_ => $"-b:v {bitrate}k"
};
var command = "-y -f rawvideo -vcodec rawvideo -pixel_format rgba -colorspace rgb -color_range 2 -video_size " +
Width + "x" + Height + " -framerate " + FrameRate + " -threads 4 -loglevel quiet -i - " +
Codec2String(VideoCodec) + " " + bitrateArgs + " -pix_fmt yuv420p -fps_mode cfr -r " +
VideoFPS + " \"" + path + "\"";
Execute(command); Execute(command);
} }
private static string GetCqpArgs(Codec codec, int qp)
{
return codec switch
{
Codec.H264_NVIDIA => $"-rc constqp -qp {qp}",
Codec.HEVC_NVIDIA => $"-rc constqp -qp {qp}",
Codec.AV1_NVIDIA => $"-rc constqp -qp {qp}",
Codec.VP9_NVIDIA => $"-rc constqp -qp {qp}",
Codec.H264_QSV => $"-global_quality {qp}",
Codec.HEVC_QSV => $"-global_quality {qp}",
Codec.AV1_QSV => $"-global_quality {qp}",
Codec.H264_Default => $"-crf {qp}",
Codec.HEVC_Default => $"-crf {qp}",
Codec.AV1_Default => $"-crf {qp}",
Codec.VP9_Default => $"-crf {qp}",
_ => $"-qp {qp}"
};
}
} }
} }