SHA256
Add CBR, VBR, and CQP Bitrate Control Options and Dynamic UI Generation
This commit is contained in:
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
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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}"
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user