SHA256
Add Basic Native Encoder Object Structure (Not Working)
This commit is contained in:
1 parent
c6356a28f0
commit
bfdfa9beb7
10 files changed
+375
-255
No files matched your search
@@ -85,4 +85,8 @@
|
||||
<EmbeddedResource Include="Bundle\Linux\scenes" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FFmpeg.AutoGen" Version="8.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+2
-11
@@ -53,14 +53,6 @@ namespace ADOFAIRenderer.Core
|
||||
public int throttleCount;
|
||||
public bool isThrottling;
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public enum BitrateControlMode
|
||||
{
|
||||
CBR,
|
||||
VBR,
|
||||
CQP
|
||||
}
|
||||
|
||||
public struct RenderSettings
|
||||
{
|
||||
public double TargetFPS;
|
||||
@@ -69,7 +61,7 @@ namespace ADOFAIRenderer.Core
|
||||
public int RenderWidth;
|
||||
public int RenderHeight;
|
||||
|
||||
public BitrateControlMode BitrateMode;
|
||||
public RateControlMode BitrateMode;
|
||||
public int Bitrate;
|
||||
public int MaxBitrate;
|
||||
public int QP;
|
||||
@@ -80,7 +72,7 @@ namespace ADOFAIRenderer.Core
|
||||
public int? RandomSeed;
|
||||
|
||||
public RenderSettings(double targetFPS, double videoFPS, int renderWidth, int renderHeight,
|
||||
BitrateControlMode bitrateMode, int bitrate, int maxBitrate, int qp,
|
||||
RateControlMode bitrateMode, int bitrate, int maxBitrate, int qp,
|
||||
string codec, double endWaitTime, bool showHitText, int? randomSeed)
|
||||
{
|
||||
TargetFPS = targetFPS;
|
||||
@@ -442,7 +434,6 @@ namespace ADOFAIRenderer.Core
|
||||
DontDestroyOnLoad(_debugMonitorObj);
|
||||
}
|
||||
|
||||
FFmpegProcess.FFmpegPath = ffmpegPath;
|
||||
lock (ImageQueue)
|
||||
{
|
||||
ImageQueue.Clear();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using ADOFAIRenderer;
|
||||
using ADOFAIRenderer.Core;
|
||||
using ADOFAIRenderer.Util;
|
||||
using FFmpeg.AutoGen;
|
||||
using MelonLoader;
|
||||
using MelonLoader.Utils;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
@@ -19,6 +21,8 @@ namespace ADOFAIRenderer
|
||||
|
||||
public override void OnInitializeMelon()
|
||||
{
|
||||
ffmpeg.RootPath = MelonEnvironment.UserLibsDirectory;
|
||||
|
||||
var renderManagerObj = new GameObject("Render Manager");
|
||||
var updateHookerObj = new GameObject("Update Hooker");
|
||||
Object.DontDestroyOnLoad(renderManagerObj);
|
||||
|
||||
+11
-11
@@ -45,7 +45,7 @@ namespace ADOFAIRenderer.UI
|
||||
if (codecDropdown != null)
|
||||
{
|
||||
codecDropdown.ClearOptions();
|
||||
var options = System.Enum.GetNames(typeof(VideoFFmpeg.Codec)).ToList();
|
||||
var options = System.Enum.GetNames(typeof(Codec)).ToList();
|
||||
codecDropdown.AddOptions(options);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace ADOFAIRenderer.UI
|
||||
if (bitrateModeDropdown != null)
|
||||
{
|
||||
bitrateModeDropdown.ClearOptions();
|
||||
var modeOptions = System.Enum.GetNames(typeof(RenderManager.BitrateControlMode)).ToList();
|
||||
var modeOptions = System.Enum.GetNames(typeof(RateControlMode)).ToList();
|
||||
bitrateModeDropdown.AddOptions(modeOptions);
|
||||
bitrateModeDropdown.onValueChanged.AddListener(delegate { UpdateUIByMode(); });
|
||||
}
|
||||
@@ -103,21 +103,21 @@ namespace ADOFAIRenderer.UI
|
||||
if (bitrateModeDropdown == null) return;
|
||||
if (_bitrateInput == null || _maxBitrateInput == null || _qpInput == null) return;
|
||||
|
||||
var selectedMode = (RenderManager.BitrateControlMode)bitrateModeDropdown.value;
|
||||
var selectedMode = (RateControlMode)bitrateModeDropdown.value;
|
||||
|
||||
switch (selectedMode)
|
||||
{
|
||||
case RenderManager.BitrateControlMode.CBR:
|
||||
case RateControlMode.CBR:
|
||||
_bitrateInput.gameObject.SetActive(true);
|
||||
_maxBitrateInput.gameObject.SetActive(false);
|
||||
_qpInput.gameObject.SetActive(false);
|
||||
break;
|
||||
case RenderManager.BitrateControlMode.VBR:
|
||||
case RateControlMode.VBR:
|
||||
_bitrateInput.gameObject.SetActive(true);
|
||||
_maxBitrateInput.gameObject.SetActive(true);
|
||||
_qpInput.gameObject.SetActive(false);
|
||||
break;
|
||||
case RenderManager.BitrateControlMode.CQP:
|
||||
case RateControlMode.CQP:
|
||||
_bitrateInput.gameObject.SetActive(false);
|
||||
_maxBitrateInput.gameObject.SetActive(false);
|
||||
_qpInput.gameObject.SetActive(true);
|
||||
@@ -165,17 +165,17 @@ namespace ADOFAIRenderer.UI
|
||||
return;
|
||||
}
|
||||
|
||||
var bitrateMode = RenderManager.BitrateControlMode.CBR;
|
||||
var bitrateMode = RateControlMode.CBR;
|
||||
if (bitrateModeDropdown != null)
|
||||
{
|
||||
bitrateMode = (RenderManager.BitrateControlMode)bitrateModeDropdown.value;
|
||||
bitrateMode = (RateControlMode)bitrateModeDropdown.value;
|
||||
}
|
||||
|
||||
var bitrate = 0;
|
||||
var maxBitrate = 0;
|
||||
var qp = 18;
|
||||
|
||||
if (bitrateMode == RenderManager.BitrateControlMode.CBR || bitrateMode == RenderManager.BitrateControlMode.VBR)
|
||||
if (bitrateMode == RateControlMode.CBR || bitrateMode == RateControlMode.VBR)
|
||||
{
|
||||
if (!int.TryParse(_bitrateInput.text, out bitrate) || bitrate <= 0)
|
||||
{
|
||||
@@ -184,7 +184,7 @@ namespace ADOFAIRenderer.UI
|
||||
}
|
||||
}
|
||||
|
||||
if (bitrateMode == RenderManager.BitrateControlMode.VBR)
|
||||
if (bitrateMode == RateControlMode.VBR)
|
||||
{
|
||||
if (!int.TryParse(_maxBitrateInput.text, out maxBitrate) || maxBitrate <= 0)
|
||||
{
|
||||
@@ -198,7 +198,7 @@ namespace ADOFAIRenderer.UI
|
||||
}
|
||||
}
|
||||
|
||||
if (bitrateMode == RenderManager.BitrateControlMode.CQP)
|
||||
if (bitrateMode == RateControlMode.CQP)
|
||||
{
|
||||
if (!int.TryParse(_qpInput.text, out qp) || qp < 0 || qp > 51)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace ADOFAIRenderer.Util.FFmpeg
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public enum Codec
|
||||
{
|
||||
H264, H265, AV1, VP9
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
using System;
|
||||
using FFmpeg.AutoGen;
|
||||
using HarmonyLib;
|
||||
using Unity.Collections;
|
||||
|
||||
namespace ADOFAIRenderer.Util.FFmpeg
|
||||
{
|
||||
public unsafe class FFmpegNativeEncoder : IDisposable
|
||||
{
|
||||
public struct EncoderSettings
|
||||
{
|
||||
public int Width;
|
||||
public int Height;
|
||||
public double FPS;
|
||||
|
||||
public Codec Codec;
|
||||
public RateControlMode Mode;
|
||||
|
||||
public int QualityValue; // CRF
|
||||
public int TargetBitrate; // VBR
|
||||
public int MaxBitrate; // VBR
|
||||
}
|
||||
|
||||
// FFmpeg 네이티브 컨텍스트 및 구조체 포인터들
|
||||
private AVFormatContext* _pFormatContext = null;
|
||||
private AVCodecContext* _pCodecContext = null;
|
||||
private AVStream* _pStream = null;
|
||||
|
||||
// 프레임 버퍼 및 스케일러 포인터들
|
||||
private AVFrame* _pSrcFrame = null; // Unity에서 넘어온 원시 RGBA 프레임용
|
||||
private AVFrame* _pDstFrame = null; // 인코더가 요구하는 YUV420P 프레임용
|
||||
private SwsContext* _pSwsContext = null; // RGBA -> YUV420P 변환 스케일러
|
||||
|
||||
private long _frameIndex = 0;
|
||||
private bool _isInitialized = false;
|
||||
private bool _disposed = false;
|
||||
|
||||
private readonly EncoderSettings _settings;
|
||||
|
||||
public FFmpegNativeEncoder(EncoderSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private AVCodecID GetAVCodecID()
|
||||
{
|
||||
return _settings.Codec switch
|
||||
{
|
||||
Codec.H264 => AVCodecID.AV_CODEC_ID_H264,
|
||||
Codec.H265 => AVCodecID.AV_CODEC_ID_HEVC,
|
||||
Codec.AV1 => AVCodecID.AV_CODEC_ID_AV1,
|
||||
Codec.VP9 => AVCodecID.AV_CODEC_ID_VP9,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
public void Start(string outputPath)
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
|
||||
try
|
||||
{
|
||||
fixed (AVFormatContext** ppFormatCtx = &_pFormatContext)
|
||||
{
|
||||
ffmpeg.avformat_alloc_output_context2(ppFormatCtx, null, null, outputPath);
|
||||
}
|
||||
|
||||
if (_pFormatContext == null) throw new Exception("출력 포맷 컨텍스트를 생성하지 못했습니다.");
|
||||
|
||||
var pCodec = ffmpeg.avcodec_find_encoder(GetAVCodecID());
|
||||
if (pCodec == null) throw new Exception("코덱 인코더를 찾을 수 없습니다.");
|
||||
|
||||
_pStream = ffmpeg.avformat_new_stream(_pFormatContext, null);
|
||||
if (_pStream == null) throw new Exception("비디오 스트림을 생성하지 못했습니다.");
|
||||
|
||||
_pCodecContext = ffmpeg.avcodec_alloc_context3(pCodec);
|
||||
_pCodecContext->width = _settings.Width;
|
||||
_pCodecContext->height = _settings.Height;
|
||||
_pCodecContext->pix_fmt = AVPixelFormat.AV_PIX_FMT_YUV420P;
|
||||
|
||||
var fpsRational = RationalUtil.Double2MaxPrecisionInt(_settings.FPS);
|
||||
_pCodecContext->time_base = new AVRational
|
||||
{ num = fpsRational.Denominator, den = fpsRational.Numerator };
|
||||
_pCodecContext->framerate = new AVRational
|
||||
{ num = fpsRational.Numerator, den = fpsRational.Denominator };
|
||||
_pCodecContext->gop_size = (int)Math.Round(_settings.FPS * 2);
|
||||
|
||||
_pCodecContext->max_b_frames = 3;
|
||||
_pCodecContext->has_b_frames = 1;
|
||||
|
||||
if ((_pFormatContext->oformat->flags & ffmpeg.AVFMT_GLOBALHEADER) != 0)
|
||||
{
|
||||
_pCodecContext->flags |= ffmpeg.AV_CODEC_FLAG_GLOBAL_HEADER;
|
||||
}
|
||||
|
||||
ApplyQualitySettings();
|
||||
ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "slow", 0);
|
||||
|
||||
if (ffmpeg.avcodec_open2(_pCodecContext, pCodec, null) < 0)
|
||||
throw new Exception("지정된 설정으로 코덱을 여는 데 실패했습니다.");
|
||||
|
||||
if (ffmpeg.avcodec_parameters_from_context(_pStream->codecpar, _pCodecContext) < 0)
|
||||
throw new Exception("코덱 파라미터를 스트림에 복사하는 데 실패했습니다.");
|
||||
|
||||
if ((_pFormatContext->oformat->flags & ffmpeg.AVFMT_NOFILE) == 0)
|
||||
{
|
||||
if (ffmpeg.avio_open(&_pFormatContext->pb, outputPath, ffmpeg.AVIO_FLAG_WRITE) < 0)
|
||||
throw new Exception($"출력 파일 {outputPath}를 열 수 없습니다.");
|
||||
}
|
||||
|
||||
if (ffmpeg.avformat_write_header(_pFormatContext, null) < 0)
|
||||
throw new Exception("파일 헤더를 작성하는 데 실패했습니다.");
|
||||
|
||||
_pSrcFrame = ffmpeg.av_frame_alloc();
|
||||
_pDstFrame = ffmpeg.av_frame_alloc();
|
||||
if (_pSrcFrame == null || _pDstFrame == null)
|
||||
throw new Exception("프레임 구조체를 할당하지 못했습니다.");
|
||||
|
||||
_pDstFrame->format = (int)AVPixelFormat.AV_PIX_FMT_YUV420P;
|
||||
_pDstFrame->width = _settings.Width;
|
||||
_pDstFrame->height = _settings.Height;
|
||||
|
||||
if (ffmpeg.av_frame_get_buffer(_pDstFrame, 0) < 0)
|
||||
throw new Exception("YUV420P 프레임 버퍼를 할당하는 데 실패했습니다.");
|
||||
|
||||
_pSwsContext = ffmpeg.sws_getContext(
|
||||
_settings.Width, _settings.Height, AVPixelFormat.AV_PIX_FMT_RGBA,
|
||||
_settings.Width, _settings.Height, AVPixelFormat.AV_PIX_FMT_YUV420P,
|
||||
(int)SwsFlags.SWS_BILINEAR, null, null, null);
|
||||
|
||||
if (_pSwsContext == null)
|
||||
throw new Exception("소프트웨어 스케일러를 초기화하는 데 실패했습니다.");
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyQualitySettings()
|
||||
{
|
||||
switch (_settings.Mode)
|
||||
{
|
||||
case RateControlMode.CBR:
|
||||
_pCodecContext->bit_rate = _pCodecContext->rc_max_rate = _pCodecContext->rc_min_rate = _settings.TargetBitrate;
|
||||
_pCodecContext->rc_buffer_size = _settings.TargetBitrate;
|
||||
|
||||
ffmpeg.av_opt_set(_pCodecContext->priv_data, "nal-hrd", "cbr", 0);
|
||||
break;
|
||||
|
||||
case RateControlMode.VBR:
|
||||
_pCodecContext->bit_rate = _settings.TargetBitrate;
|
||||
var maxRate = _settings.MaxBitrate > _settings.TargetBitrate
|
||||
? _settings.MaxBitrate : (int)(_settings.TargetBitrate * 1.5);
|
||||
|
||||
_pCodecContext->rc_max_rate = maxRate;
|
||||
_pCodecContext->rc_buffer_size = maxRate * 2;
|
||||
break;
|
||||
|
||||
case RateControlMode.CQP:
|
||||
var qpValue = _settings.QualityValue > 0 ? _settings.QualityValue : 18;
|
||||
ffmpeg.av_opt_set(_pCodecContext->priv_data, "qp", qpValue.ToString(), 0);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteFrameFromBuffer(byte* buffer)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void WriteFrameFromNativeBuffer(NativeArray<byte> buffer)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void WriteFrame()
|
||||
{
|
||||
ffmpeg.sws_scale(
|
||||
_pSwsContext,
|
||||
_pSrcFrame->data, _pSrcFrame->linesize,
|
||||
0, _settings.Height,
|
||||
_pDstFrame->data, _pDstFrame->linesize
|
||||
);
|
||||
|
||||
// 3. 타임스탬프 설정
|
||||
_pDstFrame->pts = _frameIndex++;
|
||||
|
||||
// 4. 인코더로 프레임 전달 및 패킷 쓰기
|
||||
SendFrameAndWritePackets(_pDstFrame);
|
||||
}
|
||||
|
||||
private void SendFrameAndWritePackets(AVFrame* pFrame)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
// 만약 End()를 호출하지 않고 명시적으로 Dispose가 되는 경우도 있으므로 안전하게 End() 트리거
|
||||
if (_isInitialized)
|
||||
{
|
||||
try { End(); } catch { /* 무시 */ }
|
||||
}
|
||||
|
||||
// Sws 스케일러 해제
|
||||
if (_pSwsContext != null)
|
||||
{
|
||||
ffmpeg.sws_freeContext(_pSwsContext);
|
||||
_pSwsContext = null;
|
||||
}
|
||||
|
||||
// 프레임 메모리 해제
|
||||
if (_pSrcFrame != null)
|
||||
{
|
||||
var pFrame = _pSrcFrame;
|
||||
ffmpeg.av_frame_free(&pFrame);
|
||||
_pSrcFrame = null;
|
||||
}
|
||||
if (_pDstFrame != null)
|
||||
{
|
||||
var pFrame = _pDstFrame;
|
||||
ffmpeg.av_frame_free(&pFrame);
|
||||
_pDstFrame = null;
|
||||
}
|
||||
|
||||
// 코덱 컨텍스트 해제
|
||||
if (_pCodecContext != null)
|
||||
{
|
||||
var pCodecCtx = _pCodecContext;
|
||||
ffmpeg.avcodec_free_context(&pCodecCtx);
|
||||
_pCodecContext = null;
|
||||
}
|
||||
|
||||
// 포맷 컨텍스트 및 I/O 닫기
|
||||
if (_pFormatContext != null)
|
||||
{
|
||||
if ((_pFormatContext->oformat->flags & ffmpeg.AVFMT_NOFILE) == 0)
|
||||
{
|
||||
var pPb = _pFormatContext->pb;
|
||||
if (pPb != null)
|
||||
{
|
||||
ffmpeg.avio_closep(&pPb);
|
||||
_pFormatContext->pb = null;
|
||||
}
|
||||
}
|
||||
ffmpeg.avformat_free_context(_pFormatContext);
|
||||
_pFormatContext = null;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
~FFmpegNativeEncoder()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using Unity.Collections;
|
||||
|
||||
namespace ADOFAIRenderer.Util.FFmpeg
|
||||
{
|
||||
public class FFmpegProcess : IDisposable
|
||||
{
|
||||
public static string FFmpegPath = "";
|
||||
|
||||
private Process? _ffmpegProcess;
|
||||
private Stream? _inputStream;
|
||||
|
||||
public bool IsEnd => _ffmpegProcess is { HasExited: true };
|
||||
|
||||
protected void Execute(string command)
|
||||
{
|
||||
if (string.IsNullOrEmpty(command))
|
||||
{
|
||||
throw new ArgumentException("Command is empty");
|
||||
}
|
||||
if (string.IsNullOrEmpty(FFmpegPath) || !File.Exists(FFmpegPath))
|
||||
{
|
||||
throw new FileNotFoundException($"ffmpeg binary not found at '{FFmpegPath}'");
|
||||
}
|
||||
|
||||
_ffmpegProcess = new Process();
|
||||
_ffmpegProcess.StartInfo.FileName = FFmpegPath;
|
||||
_ffmpegProcess.StartInfo.Arguments = command;
|
||||
_ffmpegProcess.StartInfo.UseShellExecute = false;
|
||||
_ffmpegProcess.StartInfo.RedirectStandardInput = true;
|
||||
_ffmpegProcess.StartInfo.CreateNoWindow = true;
|
||||
_ffmpegProcess.Start();
|
||||
_inputStream = _ffmpegProcess.StandardInput.BaseStream;
|
||||
}
|
||||
|
||||
public void SendDataFromBuffer(byte[] buffer, int length = -1)
|
||||
{
|
||||
if (_inputStream == null) return;
|
||||
var writeLength = length < 0 ? buffer.Length : length;
|
||||
_inputStream.Write(buffer, 0, writeLength);
|
||||
_inputStream.Flush();
|
||||
}
|
||||
|
||||
public void SendDataFromNativeBuffer(IntPtr nativePtr, int length)
|
||||
{
|
||||
if (_inputStream == null) return;
|
||||
StreamUtil.WriteNativePtrToStream(nativePtr, length, _inputStream);
|
||||
_inputStream.Flush();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (_ffmpegProcess == null || _ffmpegProcess.HasExited || _inputStream == null) return;
|
||||
try
|
||||
{
|
||||
_inputStream.Flush();
|
||||
_inputStream.Close();
|
||||
|
||||
// Allow FFmpeg to finish encoding and exit gracefully without any timeout to prevent video corruption
|
||||
_ffmpegProcess.WaitForExit();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore any issues during termination
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ffmpegProcess.Dispose();
|
||||
_ffmpegProcess = null;
|
||||
_inputStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Kill(int timeoutMs = 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_ffmpegProcess != null && !_ffmpegProcess.HasExited)
|
||||
{
|
||||
_ffmpegProcess.Kill();
|
||||
if (timeoutMs > 0)
|
||||
{
|
||||
_ffmpegProcess.WaitForExit(timeoutMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore process already exited or access denied errors during cleanup
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
_inputStream?.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
_ffmpegProcess?.Dispose();
|
||||
_ffmpegProcess = null;
|
||||
_inputStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Kill();
|
||||
}
|
||||
|
||||
public static byte[] RentRawBytes(int size)
|
||||
{
|
||||
var array = ArrayPool<byte>.Shared.Rent(size);
|
||||
return array;
|
||||
}
|
||||
|
||||
protected static byte[] ToRawBytes<T>(NativeArray<T> arr) where T : struct
|
||||
{
|
||||
var val = new NativeSlice<T>(arr).SliceConvert<byte>();
|
||||
var array = new byte[val.Length];
|
||||
val.CopyTo(array);
|
||||
return array;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace ADOFAIRenderer.Util.FFmpeg
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public enum RateControlMode
|
||||
{
|
||||
CBR,
|
||||
VBR,
|
||||
CQP
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using ADOFAIRenderer.Core;
|
||||
|
||||
namespace ADOFAIRenderer.Util.FFmpeg
|
||||
{
|
||||
public class VideoFFmpeg : FFmpegProcess
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public enum Codec
|
||||
{
|
||||
H264_Default,
|
||||
H264_NVIDIA,
|
||||
H264_QSV,
|
||||
HEVC_Default,
|
||||
HEVC_NVIDIA,
|
||||
HEVC_QSV,
|
||||
AV1_Default,
|
||||
AV1_NVIDIA,
|
||||
AV1_QSV,
|
||||
VP9_Default,
|
||||
VP9_NVIDIA
|
||||
}
|
||||
|
||||
public int Width;
|
||||
public int Height;
|
||||
public double FrameRate;
|
||||
public double VideoFPS;
|
||||
public Codec VideoCodec;
|
||||
|
||||
private static string Codec2String(Codec c)
|
||||
{
|
||||
return c switch
|
||||
{
|
||||
Codec.H264_Default => "-vf vflip",
|
||||
Codec.H264_NVIDIA => "-c:v h264_nvenc -vf vflip",
|
||||
Codec.H264_QSV => "-c:v h264_qsv -vf vflip",
|
||||
Codec.HEVC_Default => "-c:v libx265 -vf vflip",
|
||||
Codec.HEVC_NVIDIA => "-c:v hevc_nvenc -vf vflip",
|
||||
Codec.HEVC_QSV => "-c:v hevc_qsv -vf vflip",
|
||||
Codec.AV1_Default => "-c:v libaom-av1 -vf vflip",
|
||||
Codec.AV1_NVIDIA => "-c:v av1_nvenc -vf vflip",
|
||||
Codec.AV1_QSV => "-c:v av1_qsv -vf vflip",
|
||||
Codec.VP9_Default => "-c:v libvpx-vp9 -vf vflip",
|
||||
Codec.VP9_NVIDIA => "-c:v vp9_nvenc -vf vflip",
|
||||
_ => "",
|
||||
};
|
||||
}
|
||||
|
||||
public VideoFFmpeg(int w, int h, double frame, double videoFrame, Codec codec)
|
||||
{
|
||||
Width = w;
|
||||
Height = h;
|
||||
FrameRate = frame;
|
||||
VideoFPS = videoFrame;
|
||||
VideoCodec = codec;
|
||||
}
|
||||
|
||||
public void Start(string path, RenderManager.BitrateControlMode mode, int bitrate, int maxBitrate, int qp)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(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);
|
||||
}
|
||||
|
||||
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}"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
|
||||
namespace ADOFAIRenderer.Util
|
||||
{
|
||||
public class RationalUtil
|
||||
{
|
||||
public static (int Numerator, int Denominator) Double2MaxPrecisionInt(double value,
|
||||
int maxDenominator = 100000000)
|
||||
{
|
||||
// 1. 예외 처리 및 부호 판별
|
||||
if (value == 0.0) return (0, 1);
|
||||
|
||||
var isNegative = value < 0;
|
||||
var sign = isNegative ? -1.0 : 1.0;
|
||||
var target = Math.Abs(value);
|
||||
// 2. 스턴-브로코 / 연속분수 경계값 초기화
|
||||
// 아래 두 행의 분수 (0/1, 1/0) 사이에서 시작하여 타겟에 가까워집니다.
|
||||
long m00 = 0, m01 = 1;
|
||||
long m10 = 1, m11 = 0;
|
||||
|
||||
var x = target;
|
||||
|
||||
// int 범위 오버플로우를 방지하면서 반복 처리
|
||||
while (true)
|
||||
{
|
||||
var ai = (long)Math.Floor(x);
|
||||
|
||||
// 다음 단계의 분자, 분모 계산 (오버플로우 방지를 위해 long으로 임시 계산)
|
||||
var nextNumerator = ai * m10 + m00;
|
||||
var nextDenominator = ai * m11 + m01;
|
||||
|
||||
// int 범위를 벗어나거나, 설정한 최대 분모를 초과하면 이전 단계의 최적값을 반환
|
||||
if (nextNumerator > int.MaxValue || nextDenominator > maxDenominator)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// 행렬 업데이트
|
||||
m00 = m10; m01 = m11;
|
||||
m10 = nextNumerator; m11 = nextDenominator;
|
||||
|
||||
// 오차가 거의 없으면 (정밀도 도달) 종료
|
||||
if (Math.Abs(x - ai) < 1e-15) break;
|
||||
|
||||
// 다음 연속분수 항 준비
|
||||
x = 1.0 / (x - ai);
|
||||
}
|
||||
|
||||
// 3. 최종 결정된 int 기반의 분자 분모
|
||||
var finalNumerator = (int)m10;
|
||||
var finalDenominator = (int)m11;
|
||||
|
||||
// 부호 복원
|
||||
if (isNegative) finalNumerator = -finalNumerator;
|
||||
|
||||
return (finalNumerator, finalDenominator);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user