From 2d70fb5ae838651799a11571a8745975d32ca885ce285a9efc5a1df6b090da07 Mon Sep 17 00:00:00 2001 From: copecone Date: Tue, 6 Oct 2026 17:49:05 +0900 Subject: [PATCH] Add License Exception --- {Data => Core/Data}/GlobalSettings.cs | 2 +- {Data => Core}/PresetManager.cs | 0 Core/RenderManager.cs | 1 + Data/Codec.cs | 29 + Data/RateControlMode.cs | 11 + Data/RenderConfig.cs | 1 - LICENSE-EXCEPTION.md | 42 + README.md | 4 +- RenderMod.cs | 1 + UI/Window/RenderConfigWindow.cs | 1 + Util/FFmpeg/Codec.cs | 30 - Util/FFmpeg/FFmpegNativeDecoder.cs | 1459 ++++++++++++------------- Util/FFmpeg/FFmpegNativeEncoder.cs | 1 + Util/FFmpeg/RateControlMode.cs | 12 - Util/FFmpeg/VideoEncoder.cs | 212 ++-- Util/GPUUtil.cs | 226 ++-- 16 files changed, 1038 insertions(+), 994 deletions(-) rename {Data => Core/Data}/GlobalSettings.cs (98%) rename {Data => Core}/PresetManager.cs (100%) create mode 100644 Data/Codec.cs create mode 100644 Data/RateControlMode.cs create mode 100644 LICENSE-EXCEPTION.md delete mode 100644 Util/FFmpeg/Codec.cs delete mode 100644 Util/FFmpeg/RateControlMode.cs diff --git a/Data/GlobalSettings.cs b/Core/Data/GlobalSettings.cs similarity index 98% rename from Data/GlobalSettings.cs rename to Core/Data/GlobalSettings.cs index 959aae6..5c2b385 100644 --- a/Data/GlobalSettings.cs +++ b/Core/Data/GlobalSettings.cs @@ -4,7 +4,7 @@ using MelonLoader; using MelonLoader.Utils; using Newtonsoft.Json; -namespace ADOFAIRenderer.Data; +namespace ADOFAIRenderer.Core.Data; public class GlobalConfig { diff --git a/Data/PresetManager.cs b/Core/PresetManager.cs similarity index 100% rename from Data/PresetManager.cs rename to Core/PresetManager.cs diff --git a/Core/RenderManager.cs b/Core/RenderManager.cs index b491f1c..b002514 100644 --- a/Core/RenderManager.cs +++ b/Core/RenderManager.cs @@ -7,6 +7,7 @@ using System.Buffers; using System.Diagnostics; using System.Linq; using ADOFAIRenderer.API; +using ADOFAIRenderer.Core.Data; using ADOFAIRenderer.Data; using ADOFAIRenderer.UI; using ADOFAIRenderer.UI.Window; diff --git a/Data/Codec.cs b/Data/Codec.cs new file mode 100644 index 0000000..d9ee105 --- /dev/null +++ b/Data/Codec.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using FFmpeg.AutoGen; + +namespace ADOFAIRenderer.Data; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public enum Codec +{ + H264, H265, VP9, AV1, VVC +} + +internal static class CodecUtil +{ + public static readonly Dictionary AudioCodecMatch = new() + { + [Codec.H264] = AVCodecID.AV_CODEC_ID_AAC, + [Codec.H265] = AVCodecID.AV_CODEC_ID_AAC, + [Codec.VP9] = AVCodecID.AV_CODEC_ID_OPUS, + [Codec.AV1] = AVCodecID.AV_CODEC_ID_OPUS, + [Codec.VVC] = AVCodecID.AV_CODEC_ID_OPUS + }; + + public static readonly Dictionary> AvailableSampleRate = new() + { + [AVCodecID.AV_CODEC_ID_AAC] = new List { 44100, 48000, 96000 }, + [AVCodecID.AV_CODEC_ID_OPUS] = new List { 48000 } + }; +} \ No newline at end of file diff --git a/Data/RateControlMode.cs b/Data/RateControlMode.cs new file mode 100644 index 0000000..5e646fe --- /dev/null +++ b/Data/RateControlMode.cs @@ -0,0 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + +namespace ADOFAIRenderer.Data; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public enum RateControlMode +{ + CBR, + VBR, + CQP +} \ No newline at end of file diff --git a/Data/RenderConfig.cs b/Data/RenderConfig.cs index d57554f..381e6d7 100644 --- a/Data/RenderConfig.cs +++ b/Data/RenderConfig.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using ADOFAIRenderer.Util.FFmpeg; using UnityEngine; namespace ADOFAIRenderer.Data; diff --git a/LICENSE-EXCEPTION.md b/LICENSE-EXCEPTION.md new file mode 100644 index 0000000..3e20880 --- /dev/null +++ b/LICENSE-EXCEPTION.md @@ -0,0 +1,42 @@ +# ADOFAIRenderer API & Data Linking Exception (Dynamic Linking Only) + +> **Additional Permission under GNU General Public License version 3 section 7** + +--- + +## 1. Legal Terms (Official) + +Linking this Program statically or dynamically with other modules is making a combined work based on this Program. Thus, the terms and conditions of the GNU General Public License cover the whole combination. + +As a special exception, the copyright holders of this Program give you permission to dynamically link this Program with independent modules that interact with or reference this Program solely through the public application programming interfaces (APIs), types, and data structures declared within the following namespaces: + +- `ADOFAIRenderer.API` +- `ADOFAIRenderer.Data` + +and to copy, convey, and distribute the resulting combined work under terms of your choice, without requiring such combined work or independent modules to be covered under the GNU General Public License version 3, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. + +This exception applies exclusively to dynamic linking. Static linking, including but not limited to combining, embedding, compiling this Program into another binary, or merging assemblies (such as via ILMerge or Costura), is explicitly excluded from this exception and remains fully subject to the terms and copyleft requirements of the GNU General Public License version 3. + +An **"independent module"** is a separate and distinct software module that is not derived from or based on this Program, other than referencing or communicating with the interfaces and types declared within the designated namespaces above. + +This exception does not grant any permission to modify the source code of this Program itself outside the terms of the GNU General Public License version 3. + +In accordance with section 7 of the GNU GPL version 3, if you modify this Program, you may extend this exception to your version of the Program, but you are not obligated to do so. If you do not wish to provide this exception in your derivative work, you may remove this exception statement from your version. + +--- + +## 2. 조항 해설 (Korean Guide) + +본 문서는 GNU General Public License 버전 3 (GPLv3) 제7조(추가적인 허가, Additional Permissions)에 따라 제공되는 공식 예외 조항입니다. + +### 핵심 요약 + +- **동적 링킹(Dynamic Linking)만 허용**: + - 본 예외 조항은 **동적 링킹(런타임 참조 및 어셈블리 동적 로드)**에만 배타적으로 적용됩니다. + - ADOFAIRenderer 또는 그 일부 코드를 대상 모드/프로그램에 직접 병합하거나 포함(ILMerge, Costura, 바이너리 직접 내장/정적 컴파일 등)하는 **정적 링킹(Static Linking)은 본 예외 대상에서 제외**되며, 순수 GPLv3의 카피레프트(전염성) 의무가 그대로 적용됩니다. +- **외부 모드의 라이선스 자유**: + - 제3자 모드 또는 외부 프로그램이 동적 링킹을 통해 `ADOFAIRenderer.API` 및 `ADOFAIRenderer.Data` 네임스페이스의 공개 API, 클래스, 데이터 구조만을 참조·호출하여 연동하는 경우, 해당 모드는 **GPLv3의 카피레프트 의무를 적용받지 않습니다.** + - 외부 모드 개발자는 자신의 모드를 MIT, Apache-2.0, 독점/상용 라이선스 등 원하는 라이선스로 자유롭게 배포할 수 있습니다. +- **ADOFAIRenderer 코어 보호**: + - ADOFAIRenderer 자체의 소스 코드(렌더링 파이프라인, 코어 패치, 내부 유틸리티 등)를 수정하거나 재배포하는 경우에는 본 예외 조항이 적용되지 않으며, 순수 GPLv3 라이선스 조건이 그대로 적용됩니다. + diff --git a/README.md b/README.md index fa0f505..b726936 100644 --- a/README.md +++ b/README.md @@ -90,4 +90,6 @@ The compiled mod will be output to `bin/Release/netstandard2.1/ADOFAIRenderer.dl ## 📄 License -This project is licensed under the GNU General Public License v3.0 (GPLv3). See `LICENSE` for details. +This project is licensed under the **GNU General Public License v3.0 with API & Data Linking Exception (GPLv3 with Exception, Dynamic Linking Only)**. +- Third-party mods and external tools that reference or dynamically link to ADOFAIRenderer solely via the `ADOFAIRenderer.API` and `ADOFAIRenderer.Data` namespaces are exempt from GPLv3 copyleft requirements and may be distributed under terms of their author's choice. (Static linking is excluded from this exception.) +- For full license terms and exception details, see [LICENSE](LICENSE) and [LICENSE-EXCEPTION.md](LICENSE-EXCEPTION.md). diff --git a/RenderMod.cs b/RenderMod.cs index 29ef700..ad5d5a5 100644 --- a/RenderMod.cs +++ b/RenderMod.cs @@ -1,6 +1,7 @@ using System; using ADOFAIRenderer; using ADOFAIRenderer.Core; +using ADOFAIRenderer.Core.Data; using ADOFAIRenderer.Data; using ADOFAIRenderer.UI; using FFmpeg.AutoGen; diff --git a/UI/Window/RenderConfigWindow.cs b/UI/Window/RenderConfigWindow.cs index 5401402..d634832 100644 --- a/UI/Window/RenderConfigWindow.cs +++ b/UI/Window/RenderConfigWindow.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using ADOFAIRenderer.API; using ADOFAIRenderer.Core; +using ADOFAIRenderer.Core.Data; using ADOFAIRenderer.Data; using ADOFAIRenderer.Localization; using ADOFAIRenderer.Util; diff --git a/Util/FFmpeg/Codec.cs b/Util/FFmpeg/Codec.cs deleted file mode 100644 index 399593e..0000000 --- a/Util/FFmpeg/Codec.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using FFmpeg.AutoGen; - -namespace ADOFAIRenderer.Util.FFmpeg -{ - [SuppressMessage("ReSharper", "InconsistentNaming")] - public enum Codec - { - H264, H265, VP9, AV1, VVC - } - - internal static class CodecUtil - { - public static readonly Dictionary AudioCodecMatch = new() - { - [Codec.H264] = AVCodecID.AV_CODEC_ID_AAC, - [Codec.H265] = AVCodecID.AV_CODEC_ID_AAC, - [Codec.VP9] = AVCodecID.AV_CODEC_ID_OPUS, - [Codec.AV1] = AVCodecID.AV_CODEC_ID_OPUS, - [Codec.VVC] = AVCodecID.AV_CODEC_ID_OPUS - }; - - public static readonly Dictionary> AvailableSampleRate = new() - { - [AVCodecID.AV_CODEC_ID_AAC] = new List { 44100, 48000, 96000 }, - [AVCodecID.AV_CODEC_ID_OPUS] = new List { 48000 } - }; - } -} \ No newline at end of file diff --git a/Util/FFmpeg/FFmpegNativeDecoder.cs b/Util/FFmpeg/FFmpegNativeDecoder.cs index 970bf86..3a4611e 100644 --- a/Util/FFmpeg/FFmpegNativeDecoder.cs +++ b/Util/FFmpeg/FFmpegNativeDecoder.cs @@ -3,272 +3,303 @@ using FFmpeg.AutoGen; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; -namespace ADOFAIRenderer.Util.FFmpeg +namespace ADOFAIRenderer.Util.FFmpeg; + +/// +/// Decodes video files in a streaming fashion using FFmpeg's native C API. +/// Unlike a standalone player, it allows reading frames sequentially or seeking/skipping forward on demand. +/// +internal sealed unsafe class FFmpegNativeDecoder : IDisposable { - /// - /// Decodes video files in a streaming fashion using FFmpeg's native C API. - /// Unlike a standalone player, it allows reading frames sequentially or seeking/skipping forward on demand. - /// - internal sealed unsafe class FFmpegNativeDecoder : IDisposable - { - private AVFormatContext* _pFormatContext = null; - private AVCodecContext* _pCodecContext = null; - private AVFrame* _pDecodedFrame = null; - private AVFrame* _pConvertedFrame = null; - private SwsContext* _pSwsContext = null; - private AVPacket* _pPacket = null; + private AVFormatContext* _pFormatContext = null; + private AVCodecContext* _pCodecContext = null; + private AVFrame* _pDecodedFrame = null; + private AVFrame* _pConvertedFrame = null; + private SwsContext* _pSwsContext = null; + private AVPacket* _pPacket = null; - private int _videoStreamIndex = -1; - private long _currentFrameIndex; - private AVRational _timeBase; - private bool _isOpened; - private bool _disposed; - private bool _isDraining; - private readonly object _lock = new(); + private int _videoStreamIndex = -1; + private long _currentFrameIndex; + private AVRational _timeBase; + private bool _isOpened; + private bool _disposed; + private bool _isDraining; + private readonly object _lock = new(); - // Cached frame used to hold matching frames after seeking - private AVFrame* _pCachedFrame = null; - private bool _hasCachedFrame; + // Cached frame used to hold matching frames after seeking + private AVFrame* _pCachedFrame = null; + private bool _hasCachedFrame; - // Field to detect SwsContext updates - private AVPixelFormat _srcPixFmt = AVPixelFormat.AV_PIX_FMT_NONE; - private AVPixelFormat _dstPixFmt = AVPixelFormat.AV_PIX_FMT_NONE; - private int _srcWidth; - private int _srcHeight; + // Field to detect SwsContext updates + private AVPixelFormat _srcPixFmt = AVPixelFormat.AV_PIX_FMT_NONE; + private AVPixelFormat _dstPixFmt = AVPixelFormat.AV_PIX_FMT_NONE; + private int _srcWidth; + private int _srcHeight; - /// Video width in pixels. - public int Width { get; private set; } + /// Video width in pixels. + public int Width { get; private set; } - /// Video height in pixels. - public int Height { get; private set; } + /// Video height in pixels. + public int Height { get; private set; } - /// Frames per second. - public double FPS { get; private set; } + /// Frames per second. + public double FPS { get; private set; } - /// Total duration in seconds. - public double Duration { get; private set; } + /// Total duration in seconds. + public double Duration { get; private set; } - /// Estimated total number of frames. - public long TotalFrames { get; private set; } + /// Estimated total number of frames. + public long TotalFrames { get; private set; } - /// Index of the currently read frame. - public long CurrentFrameIndex => _currentFrameIndex; + /// Index of the currently read frame. + public long CurrentFrameIndex => _currentFrameIndex; - /// Current playback time in seconds. - public double CurrentTime => _currentFrameIndex / FPS; + /// Current playback time in seconds. + public double CurrentTime => _currentFrameIndex / FPS; - /// Pixel format of the original video stream. - public AVPixelFormat PixelFormat => _pCodecContext != null ? _pCodecContext->pix_fmt : AVPixelFormat.AV_PIX_FMT_NONE; + /// Pixel format of the original video stream. + public AVPixelFormat PixelFormat => _pCodecContext != null ? _pCodecContext->pix_fmt : AVPixelFormat.AV_PIX_FMT_NONE; - /// Color range of the original video stream. - public AVColorRange ColorRange => _pCodecContext != null ? _pCodecContext->color_range : AVColorRange.AVCOL_RANGE_UNSPECIFIED; + /// Color range of the original video stream. + public AVColorRange ColorRange => _pCodecContext != null ? _pCodecContext->color_range : AVColorRange.AVCOL_RANGE_UNSPECIFIED; - public FFmpegNativeDecoder(string videoPath) + public FFmpegNativeDecoder(string videoPath) + { + Open(videoPath); + } + + private void Open(string videoPath) + { + lock (_lock) { - Open(videoPath); - } + if (_isOpened || _disposed) return; - private void Open(string videoPath) - { - lock (_lock) + try { - if (_isOpened || _disposed) return; - - try + fixed (AVFormatContext** ppFormatCtx = &_pFormatContext) { - fixed (AVFormatContext** ppFormatCtx = &_pFormatContext) - { - int ret = ffmpeg.avformat_open_input(ppFormatCtx, videoPath, null, null); - if (ret < 0) - throw new Exception($"Failed to open video file: {videoPath} ({GetFFmpegErrorString(ret)})"); - } - - if (ffmpeg.avformat_find_stream_info(_pFormatContext, null) < 0) - throw new Exception("Failed to retrieve stream info."); - - _videoStreamIndex = -1; - for (int i = 0; i < _pFormatContext->nb_streams; i++) - { - if (_pFormatContext->streams[i]->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO) - { - _videoStreamIndex = i; - break; - } - } - - if (_videoStreamIndex == -1) - throw new Exception("Failed to find video stream."); - - var stream = _pFormatContext->streams[_videoStreamIndex]; - _timeBase = stream->time_base; - - // Calculate FPS - if (stream->r_frame_rate.num > 0 && stream->r_frame_rate.den > 0) - { - FPS = ffmpeg.av_q2d(stream->r_frame_rate); - } - else if (stream->avg_frame_rate.num > 0 && stream->avg_frame_rate.den > 0) - { - FPS = ffmpeg.av_q2d(stream->avg_frame_rate); - } - else - { - FPS = 30.0; // Fallback default - } - - var codecId = stream->codecpar->codec_id; - var decoder = ffmpeg.avcodec_find_decoder(codecId); - if (decoder == null) - throw new Exception($"Unsupported video codec: {codecId}"); - - _pCodecContext = ffmpeg.avcodec_alloc_context3(decoder); - if (_pCodecContext == null) - throw new Exception("Failed to allocate codec context."); - - if (ffmpeg.avcodec_parameters_to_context(_pCodecContext, stream->codecpar) < 0) - throw new Exception("Failed to copy codec parameters."); - - // Multi-threading settings for improved decoding performance (auto) - _pCodecContext->thread_count = 0; - _pCodecContext->thread_type = ffmpeg.FF_THREAD_FRAME; - - if (ffmpeg.avcodec_open2(_pCodecContext, decoder, null) < 0) - throw new Exception("Failed to open codec."); - - Width = _pCodecContext->width; - Height = _pCodecContext->height; - - // Calculate total frames and duration - if (stream->nb_frames > 0) - { - TotalFrames = stream->nb_frames; - } - else if (stream->duration > 0) - { - TotalFrames = (long)Math.Round(stream->duration * ffmpeg.av_q2d(stream->time_base) * FPS); - } - else if (_pFormatContext->duration > 0) - { - TotalFrames = (long)Math.Round((_pFormatContext->duration / (double)ffmpeg.AV_TIME_BASE) * FPS); - } - else - { - TotalFrames = 0; - } - - Duration = _pFormatContext->duration > 0 - ? _pFormatContext->duration / (double)ffmpeg.AV_TIME_BASE - : (stream->duration > 0 ? stream->duration * ffmpeg.av_q2d(stream->time_base) : 0); - - _pDecodedFrame = ffmpeg.av_frame_alloc(); - _pConvertedFrame = ffmpeg.av_frame_alloc(); - _pCachedFrame = ffmpeg.av_frame_alloc(); - _pPacket = ffmpeg.av_packet_alloc(); - - _currentFrameIndex = 0; - _isOpened = true; - _isDraining = false; + int ret = ffmpeg.avformat_open_input(ppFormatCtx, videoPath, null, null); + if (ret < 0) + throw new Exception($"Failed to open video file: {videoPath} ({GetFFmpegErrorString(ret)})"); } - catch (Exception) + + if (ffmpeg.avformat_find_stream_info(_pFormatContext, null) < 0) + throw new Exception("Failed to retrieve stream info."); + + _videoStreamIndex = -1; + for (int i = 0; i < _pFormatContext->nb_streams; i++) { - Dispose(); - throw; + if (_pFormatContext->streams[i]->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO) + { + _videoStreamIndex = i; + break; + } } + + if (_videoStreamIndex == -1) + throw new Exception("Failed to find video stream."); + + var stream = _pFormatContext->streams[_videoStreamIndex]; + _timeBase = stream->time_base; + + // Calculate FPS + if (stream->r_frame_rate.num > 0 && stream->r_frame_rate.den > 0) + { + FPS = ffmpeg.av_q2d(stream->r_frame_rate); + } + else if (stream->avg_frame_rate.num > 0 && stream->avg_frame_rate.den > 0) + { + FPS = ffmpeg.av_q2d(stream->avg_frame_rate); + } + else + { + FPS = 30.0; // Fallback default + } + + var codecId = stream->codecpar->codec_id; + var decoder = ffmpeg.avcodec_find_decoder(codecId); + if (decoder == null) + throw new Exception($"Unsupported video codec: {codecId}"); + + _pCodecContext = ffmpeg.avcodec_alloc_context3(decoder); + if (_pCodecContext == null) + throw new Exception("Failed to allocate codec context."); + + if (ffmpeg.avcodec_parameters_to_context(_pCodecContext, stream->codecpar) < 0) + throw new Exception("Failed to copy codec parameters."); + + // Multi-threading settings for improved decoding performance (auto) + _pCodecContext->thread_count = 0; + _pCodecContext->thread_type = ffmpeg.FF_THREAD_FRAME; + + if (ffmpeg.avcodec_open2(_pCodecContext, decoder, null) < 0) + throw new Exception("Failed to open codec."); + + Width = _pCodecContext->width; + Height = _pCodecContext->height; + + // Calculate total frames and duration + if (stream->nb_frames > 0) + { + TotalFrames = stream->nb_frames; + } + else if (stream->duration > 0) + { + TotalFrames = (long)Math.Round(stream->duration * ffmpeg.av_q2d(stream->time_base) * FPS); + } + else if (_pFormatContext->duration > 0) + { + TotalFrames = (long)Math.Round((_pFormatContext->duration / (double)ffmpeg.AV_TIME_BASE) * FPS); + } + else + { + TotalFrames = 0; + } + + Duration = _pFormatContext->duration > 0 + ? _pFormatContext->duration / (double)ffmpeg.AV_TIME_BASE + : (stream->duration > 0 ? stream->duration * ffmpeg.av_q2d(stream->time_base) : 0); + + _pDecodedFrame = ffmpeg.av_frame_alloc(); + _pConvertedFrame = ffmpeg.av_frame_alloc(); + _pCachedFrame = ffmpeg.av_frame_alloc(); + _pPacket = ffmpeg.av_packet_alloc(); + + _currentFrameIndex = 0; + _isOpened = true; + _isDraining = false; + } + catch (Exception) + { + Dispose(); + throw; } } + } - private bool ReadAndSendPacket() + private bool ReadAndSendPacket() + { + while (true) { + ffmpeg.av_packet_unref(_pPacket); + int ret = ffmpeg.av_read_frame(_pFormatContext, _pPacket); + if (ret < 0) return false; // EOF or error + + if (_pPacket->stream_index == _videoStreamIndex) + { + ret = ffmpeg.avcodec_send_packet(_pCodecContext, _pPacket); + ffmpeg.av_packet_unref(_pPacket); + if (ret < 0) + { + throw new Exception($"Error sending packet to decoder: {GetFFmpegErrorString(ret)}"); + } + return true; + } + } + } + + private void InitOrUpdateSwsContext(AVPixelFormat srcFormat, int srcWidth, int srcHeight, AVPixelFormat dstFormat) + { + if (_pSwsContext != null && _srcPixFmt == srcFormat && _srcWidth == srcWidth && _srcHeight == srcHeight && _dstPixFmt == dstFormat) + { + return; + } + + if (_pSwsContext != null) + { + ffmpeg.sws_freeContext(_pSwsContext); + _pSwsContext = null; + } + + _srcPixFmt = srcFormat; + _srcWidth = srcWidth; + _srcHeight = srcHeight; + _dstPixFmt = dstFormat; + + _pSwsContext = ffmpeg.sws_getContext( + srcWidth, srcHeight, srcFormat, + Width, Height, dstFormat, + (int)SwsFlags.SWS_BILINEAR, null, null, null + ); + + if (_pSwsContext == null) + { + throw new Exception("Failed to initialize SwsContext."); + } + } + + private double GetTimestampAndUpdateIndex(AVFrame* frame) + { + long ptsValue = frame->best_effort_timestamp; + if (ptsValue == ffmpeg.AV_NOPTS_VALUE) + { + ptsValue = frame->pts; + } + + double timestamp; + if (ptsValue == ffmpeg.AV_NOPTS_VALUE) + { + timestamp = _currentFrameIndex / FPS; + _currentFrameIndex++; + } + else + { + timestamp = ptsValue * ffmpeg.av_q2d(_timeBase); + _currentFrameIndex = (long)Math.Round(timestamp * FPS); + } + return timestamp; + } + + /// + /// Reads the next video frame and copies it in RGBA format. + /// + /// Pointer to the output buffer storing RGBA data. + /// Byte stride per row of the output buffer (typically Width * 4). + /// Whether to flip vertically to match Unity's texture coordinate system. + /// Timestamp of the output frame in seconds. + /// True if the frame was successfully decoded and copied; false on EOF or failure. + private bool TryReadFrame(byte* outBuffer, int outStride, bool vflip, out double timestamp) + { + timestamp = 0; + lock (_lock) + { + if (!_isOpened || _disposed) return false; + + if (_hasCachedFrame) + { + timestamp = GetTimestampAndUpdateIndex(_pCachedFrame); + InitOrUpdateSwsContext(_pCodecContext->pix_fmt, _pCachedFrame->width, _pCachedFrame->height, AVPixelFormat.AV_PIX_FMT_RGBA); + + if (vflip) + { + _pConvertedFrame->data[0] = outBuffer + outStride * (Height - 1); + _pConvertedFrame->linesize[0] = -outStride; + } + else + { + _pConvertedFrame->data[0] = outBuffer; + _pConvertedFrame->linesize[0] = outStride; + } + + ffmpeg.sws_scale( + _pSwsContext, + _pCachedFrame->data, _pCachedFrame->linesize, + 0, _pCachedFrame->height, + _pConvertedFrame->data, _pConvertedFrame->linesize + ); + + ffmpeg.av_frame_unref(_pCachedFrame); + _hasCachedFrame = false; + return true; + } + while (true) { - ffmpeg.av_packet_unref(_pPacket); - int ret = ffmpeg.av_read_frame(_pFormatContext, _pPacket); - if (ret < 0) return false; // EOF or error - - if (_pPacket->stream_index == _videoStreamIndex) + int ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame); + if (ret == 0) { - ret = ffmpeg.avcodec_send_packet(_pCodecContext, _pPacket); - ffmpeg.av_packet_unref(_pPacket); - if (ret < 0) - { - throw new Exception($"Error sending packet to decoder: {GetFFmpegErrorString(ret)}"); - } - return true; - } - } - } - - private void InitOrUpdateSwsContext(AVPixelFormat srcFormat, int srcWidth, int srcHeight, AVPixelFormat dstFormat) - { - if (_pSwsContext != null && _srcPixFmt == srcFormat && _srcWidth == srcWidth && _srcHeight == srcHeight && _dstPixFmt == dstFormat) - { - return; - } - - if (_pSwsContext != null) - { - ffmpeg.sws_freeContext(_pSwsContext); - _pSwsContext = null; - } - - _srcPixFmt = srcFormat; - _srcWidth = srcWidth; - _srcHeight = srcHeight; - _dstPixFmt = dstFormat; - - _pSwsContext = ffmpeg.sws_getContext( - srcWidth, srcHeight, srcFormat, - Width, Height, dstFormat, - (int)SwsFlags.SWS_BILINEAR, null, null, null - ); - - if (_pSwsContext == null) - { - throw new Exception("Failed to initialize SwsContext."); - } - } - - private double GetTimestampAndUpdateIndex(AVFrame* frame) - { - long ptsValue = frame->best_effort_timestamp; - if (ptsValue == ffmpeg.AV_NOPTS_VALUE) - { - ptsValue = frame->pts; - } - - double timestamp; - if (ptsValue == ffmpeg.AV_NOPTS_VALUE) - { - timestamp = _currentFrameIndex / FPS; - _currentFrameIndex++; - } - else - { - timestamp = ptsValue * ffmpeg.av_q2d(_timeBase); - _currentFrameIndex = (long)Math.Round(timestamp * FPS); - } - return timestamp; - } - - /// - /// Reads the next video frame and copies it in RGBA format. - /// - /// Pointer to the output buffer storing RGBA data. - /// Byte stride per row of the output buffer (typically Width * 4). - /// Whether to flip vertically to match Unity's texture coordinate system. - /// Timestamp of the output frame in seconds. - /// True if the frame was successfully decoded and copied; false on EOF or failure. - private bool TryReadFrame(byte* outBuffer, int outStride, bool vflip, out double timestamp) - { - timestamp = 0; - lock (_lock) - { - if (!_isOpened || _disposed) return false; - - if (_hasCachedFrame) - { - timestamp = GetTimestampAndUpdateIndex(_pCachedFrame); - InitOrUpdateSwsContext(_pCodecContext->pix_fmt, _pCachedFrame->width, _pCachedFrame->height, AVPixelFormat.AV_PIX_FMT_RGBA); + timestamp = GetTimestampAndUpdateIndex(_pDecodedFrame); + InitOrUpdateSwsContext(_pCodecContext->pix_fmt, _pDecodedFrame->width, _pDecodedFrame->height, AVPixelFormat.AV_PIX_FMT_RGBA); if (vflip) { @@ -283,44 +314,86 @@ namespace ADOFAIRenderer.Util.FFmpeg ffmpeg.sws_scale( _pSwsContext, - _pCachedFrame->data, _pCachedFrame->linesize, - 0, _pCachedFrame->height, + _pDecodedFrame->data, _pDecodedFrame->linesize, + 0, _pDecodedFrame->height, _pConvertedFrame->data, _pConvertedFrame->linesize ); - ffmpeg.av_frame_unref(_pCachedFrame); - _hasCachedFrame = false; + ffmpeg.av_frame_unref(_pDecodedFrame); return true; } + if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN)) + { + if (_isDraining) return false; + + if (!ReadAndSendPacket()) + { + _isDraining = true; + ffmpeg.avcodec_send_packet(_pCodecContext, null); + } + } + else if (ret == ffmpeg.AVERROR_EOF) + { + return false; + } + else + { + throw new Exception($"Error during decoding: {GetFFmpegErrorString(ret)}"); + } + } + } + } + + public bool TryReadFrame(IntPtr outBuffer, int outStride, bool vflip, out double timestamp) + { + return TryReadFrame((byte*)outBuffer.ToPointer(), outStride, vflip, out timestamp); + } + + public bool TryReadFrame(NativeArray outBuffer, bool vflip, out double timestamp) + { + if (outBuffer.Length < Width * Height * 4) + { + throw new ArgumentException("Output buffer size is smaller than video resolution.", nameof(outBuffer)); + } + int stride = Width * 4; + return TryReadFrame((byte*)outBuffer.GetUnsafePtr(), stride, vflip, out timestamp); + } + + /// + /// Reads the next video frame and copies its Y, U, and V channel data separately. + /// If the source format is YUV420P family (YUV420P, YUVJ420P), CPU color conversion (sws_scale) is bypassed + /// and memory is copied directly for maximum performance. + /// Other formats are automatically converted to YUV420P before copying. + /// + // ReSharper disable once InconsistentNaming + private bool TryReadYUV420PFrame( + byte* yBuffer, int yStride, + byte* uBuffer, int uStride, + byte* vBuffer, int vStride, + out double timestamp) + { + timestamp = 0; + lock (_lock) + { + if (!_isOpened || _disposed) return false; + + AVFrame* frame; + + if (_hasCachedFrame) + { + frame = _pCachedFrame; + _hasCachedFrame = false; + } + else + { while (true) { int ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame); if (ret == 0) { - timestamp = GetTimestampAndUpdateIndex(_pDecodedFrame); - InitOrUpdateSwsContext(_pCodecContext->pix_fmt, _pDecodedFrame->width, _pDecodedFrame->height, AVPixelFormat.AV_PIX_FMT_RGBA); - - if (vflip) - { - _pConvertedFrame->data[0] = outBuffer + outStride * (Height - 1); - _pConvertedFrame->linesize[0] = -outStride; - } - else - { - _pConvertedFrame->data[0] = outBuffer; - _pConvertedFrame->linesize[0] = outStride; - } - - ffmpeg.sws_scale( - _pSwsContext, - _pDecodedFrame->data, _pDecodedFrame->linesize, - 0, _pDecodedFrame->height, - _pConvertedFrame->data, _pConvertedFrame->linesize - ); - - ffmpeg.av_frame_unref(_pDecodedFrame); - return true; + frame = _pDecodedFrame; + break; } if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN)) @@ -343,333 +416,135 @@ namespace ADOFAIRenderer.Util.FFmpeg } } } - } - public bool TryReadFrame(IntPtr outBuffer, int outStride, bool vflip, out double timestamp) - { - return TryReadFrame((byte*)outBuffer.ToPointer(), outStride, vflip, out timestamp); - } + if (frame == null) return false; - public bool TryReadFrame(NativeArray outBuffer, bool vflip, out double timestamp) - { - if (outBuffer.Length < Width * Height * 4) + timestamp = GetTimestampAndUpdateIndex(frame); + + if (frame->format != (int)AVPixelFormat.AV_PIX_FMT_YUV420P && frame->format != (int)AVPixelFormat.AV_PIX_FMT_YUVJ420P) { - throw new ArgumentException("Output buffer size is smaller than video resolution.", nameof(outBuffer)); + InitOrUpdateSwsContext((AVPixelFormat)frame->format, frame->width, frame->height, AVPixelFormat.AV_PIX_FMT_YUV420P); + + _pConvertedFrame->data[0] = yBuffer; + _pConvertedFrame->data[1] = uBuffer; + _pConvertedFrame->data[2] = vBuffer; + _pConvertedFrame->linesize[0] = yStride; + _pConvertedFrame->linesize[1] = uStride; + _pConvertedFrame->linesize[2] = vStride; + + ffmpeg.sws_scale( + _pSwsContext, + frame->data, frame->linesize, + 0, frame->height, + _pConvertedFrame->data, _pConvertedFrame->linesize + ); } - int stride = Width * 4; - return TryReadFrame((byte*)outBuffer.GetUnsafePtr(), stride, vflip, out timestamp); - } - - /// - /// Reads the next video frame and copies its Y, U, and V channel data separately. - /// If the source format is YUV420P family (YUV420P, YUVJ420P), CPU color conversion (sws_scale) is bypassed - /// and memory is copied directly for maximum performance. - /// Other formats are automatically converted to YUV420P before copying. - /// - // ReSharper disable once InconsistentNaming - private bool TryReadYUV420PFrame( - byte* yBuffer, int yStride, - byte* uBuffer, int uStride, - byte* vBuffer, int vStride, - out double timestamp) - { - timestamp = 0; - lock (_lock) + else { - if (!_isOpened || _disposed) return false; + int w = frame->width; + int h = frame->height; + int uvW = w / 2; + int uvH = h / 2; - AVFrame* frame; - - if (_hasCachedFrame) + // Copy Y plane + for (int y = 0; y < h; y++) { - frame = _pCachedFrame; - _hasCachedFrame = false; - } - else - { - while (true) - { - int ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame); - if (ret == 0) - { - frame = _pDecodedFrame; - break; - } - - if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN)) - { - if (_isDraining) return false; - - if (!ReadAndSendPacket()) - { - _isDraining = true; - ffmpeg.avcodec_send_packet(_pCodecContext, null); - } - } - else if (ret == ffmpeg.AVERROR_EOF) - { - return false; - } - else - { - throw new Exception($"Error during decoding: {GetFFmpegErrorString(ret)}"); - } - } - } - - if (frame == null) return false; - - timestamp = GetTimestampAndUpdateIndex(frame); - - if (frame->format != (int)AVPixelFormat.AV_PIX_FMT_YUV420P && frame->format != (int)AVPixelFormat.AV_PIX_FMT_YUVJ420P) - { - InitOrUpdateSwsContext((AVPixelFormat)frame->format, frame->width, frame->height, AVPixelFormat.AV_PIX_FMT_YUV420P); - - _pConvertedFrame->data[0] = yBuffer; - _pConvertedFrame->data[1] = uBuffer; - _pConvertedFrame->data[2] = vBuffer; - _pConvertedFrame->linesize[0] = yStride; - _pConvertedFrame->linesize[1] = uStride; - _pConvertedFrame->linesize[2] = vStride; - - ffmpeg.sws_scale( - _pSwsContext, - frame->data, frame->linesize, - 0, frame->height, - _pConvertedFrame->data, _pConvertedFrame->linesize + Buffer.MemoryCopy( + frame->data[0] + y * frame->linesize[0], + yBuffer + y * yStride, + w, w ); } - else + + // Copy U plane + for (int y = 0; y < uvH; y++) { - int w = frame->width; - int h = frame->height; - int uvW = w / 2; - int uvH = h / 2; - - // Copy Y plane - for (int y = 0; y < h; y++) - { - Buffer.MemoryCopy( - frame->data[0] + y * frame->linesize[0], - yBuffer + y * yStride, - w, w - ); - } - - // Copy U plane - for (int y = 0; y < uvH; y++) - { - Buffer.MemoryCopy( - frame->data[1] + y * frame->linesize[1], - uBuffer + y * uStride, - uvW, uvW - ); - } - - // Copy V plane - for (int y = 0; y < uvH; y++) - { - Buffer.MemoryCopy( - frame->data[2] + y * frame->linesize[2], - vBuffer + y * vStride, - uvW, uvW - ); - } - } - - ffmpeg.av_frame_unref(frame); - return true; - } - } - - // ReSharper disable once InconsistentNaming - public bool TryReadYUV420PFrame( - NativeArray yBuffer, - NativeArray uBuffer, - NativeArray vBuffer, - out double timestamp) - { - int w = Width; - int h = Height; - int uvW = w / 2; - int uvH = h / 2; - - if (yBuffer.Length < w * h || uBuffer.Length < uvW * uvH || vBuffer.Length < uvW * uvH) - { - throw new ArgumentException("Buffer size is smaller than video YUV plane resolution."); - } - - return TryReadYUV420PFrame( - (byte*)yBuffer.GetUnsafePtr(), w, - (byte*)uBuffer.GetUnsafePtr(), uvW, - (byte*)vBuffer.GetUnsafePtr(), uvW, - out timestamp - ); - } - - /// - /// Reads the next video frame and copies its NV12 format Y and UV channel data separately. - /// If the source format is NV12 family, CPU color conversion (sws_scale) is bypassed - /// and memory is copied directly for maximum performance. - /// Other formats are automatically converted to NV12 before copying. - /// - // ReSharper disable once InconsistentNaming - private bool TryReadNV12Frame( - byte* yBuffer, int yStride, - byte* uvBuffer, int uvStride, - out double timestamp) - { - timestamp = 0; - lock (_lock) - { - if (!_isOpened || _disposed) return false; - - AVFrame* frame; - - if (_hasCachedFrame) - { - frame = _pCachedFrame; - _hasCachedFrame = false; - } - else - { - while (true) - { - int ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame); - if (ret == 0) - { - frame = _pDecodedFrame; - break; - } - - if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN)) - { - if (_isDraining) return false; - - if (!ReadAndSendPacket()) - { - _isDraining = true; - ffmpeg.avcodec_send_packet(_pCodecContext, null); - } - } - else if (ret == ffmpeg.AVERROR_EOF) - { - return false; - } - else - { - throw new Exception($"Error during decoding: {GetFFmpegErrorString(ret)}"); - } - } - } - - if (frame == null) return false; - - timestamp = GetTimestampAndUpdateIndex(frame); - - if (frame->format != (int)AVPixelFormat.AV_PIX_FMT_NV12) - { - InitOrUpdateSwsContext((AVPixelFormat)frame->format, frame->width, frame->height, AVPixelFormat.AV_PIX_FMT_NV12); - - _pConvertedFrame->data[0] = yBuffer; - _pConvertedFrame->data[1] = uvBuffer; - _pConvertedFrame->linesize[0] = yStride; - _pConvertedFrame->linesize[1] = uvStride; - - ffmpeg.sws_scale( - _pSwsContext, - frame->data, frame->linesize, - 0, frame->height, - _pConvertedFrame->data, _pConvertedFrame->linesize + Buffer.MemoryCopy( + frame->data[1] + y * frame->linesize[1], + uBuffer + y * uStride, + uvW, uvW ); } - else + + // Copy V plane + for (int y = 0; y < uvH; y++) { - int w = frame->width; - int h = frame->height; - int uvH = h / 2; - - // Copy Y plane - for (int y = 0; y < h; y++) - { - Buffer.MemoryCopy( - frame->data[0] + y * frame->linesize[0], - yBuffer + y * yStride, - w, w - ); - } - - // Copy UV plane - for (int y = 0; y < uvH; y++) - { - Buffer.MemoryCopy( - frame->data[1] + y * frame->linesize[1], - uvBuffer + y * uvStride, - w, w - ); - } + Buffer.MemoryCopy( + frame->data[2] + y * frame->linesize[2], + vBuffer + y * vStride, + uvW, uvW + ); } - - ffmpeg.av_frame_unref(frame); - return true; - } - } - - // ReSharper disable once InconsistentNaming - public bool TryReadNV12Frame( - NativeArray yBuffer, - NativeArray uvBuffer, - out double timestamp) - { - int w = Width; - int h = Height; - int uvH = h / 2; - - if (yBuffer.Length < w * h || uvBuffer.Length < w * uvH) - { - throw new ArgumentException("Buffer size is smaller than video NV12 plane resolution."); } - return TryReadNV12Frame( - (byte*)yBuffer.GetUnsafePtr(), w, - (byte*)uvBuffer.GetUnsafePtr(), w, - out timestamp - ); + ffmpeg.av_frame_unref(frame); + return true; + } + } + + // ReSharper disable once InconsistentNaming + public bool TryReadYUV420PFrame( + NativeArray yBuffer, + NativeArray uBuffer, + NativeArray vBuffer, + out double timestamp) + { + int w = Width; + int h = Height; + int uvW = w / 2; + int uvH = h / 2; + + if (yBuffer.Length < w * h || uBuffer.Length < uvW * uvH || vBuffer.Length < uvW * uvH) + { + throw new ArgumentException("Buffer size is smaller than video YUV plane resolution."); } - /// - /// Decodes and discards a specified number of frames immediately (for fast forward skipping). - /// - /// Number of frames to discard. - private void SkipFrames(int count) + return TryReadYUV420PFrame( + (byte*)yBuffer.GetUnsafePtr(), w, + (byte*)uBuffer.GetUnsafePtr(), uvW, + (byte*)vBuffer.GetUnsafePtr(), uvW, + out timestamp + ); + } + + /// + /// Reads the next video frame and copies its NV12 format Y and UV channel data separately. + /// If the source format is NV12 family, CPU color conversion (sws_scale) is bypassed + /// and memory is copied directly for maximum performance. + /// Other formats are automatically converted to NV12 before copying. + /// + // ReSharper disable once InconsistentNaming + private bool TryReadNV12Frame( + byte* yBuffer, int yStride, + byte* uvBuffer, int uvStride, + out double timestamp) + { + timestamp = 0; + lock (_lock) { - lock (_lock) + if (!_isOpened || _disposed) return false; + + AVFrame* frame; + + if (_hasCachedFrame) { - if (!_isOpened || _disposed) return; - if (count <= 0) return; - - int skipped = 0; - if (_hasCachedFrame) - { - ffmpeg.av_frame_unref(_pCachedFrame); - _hasCachedFrame = false; - skipped++; - } - - while (skipped < count) + frame = _pCachedFrame; + _hasCachedFrame = false; + } + else + { + while (true) { int ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame); if (ret == 0) { - GetTimestampAndUpdateIndex(_pDecodedFrame); - ffmpeg.av_frame_unref(_pDecodedFrame); - skipped++; - continue; + frame = _pDecodedFrame; + break; } if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN)) { - if (_isDraining) break; + if (_isDraining) return false; if (!ReadAndSendPacket()) { @@ -679,185 +554,309 @@ namespace ADOFAIRenderer.Util.FFmpeg } else if (ret == ffmpeg.AVERROR_EOF) { - break; + return false; } else { - throw new Exception($"Error skipping frames: {GetFFmpegErrorString(ret)}"); + throw new Exception($"Error during decoding: {GetFFmpegErrorString(ret)}"); } } } - } - /// - /// Seeks to a specific playback time in seconds. - /// - /// Target time in seconds to seek to. - public void SeekToTime(double timeSeconds) - { - lock (_lock) + if (frame == null) return false; + + timestamp = GetTimestampAndUpdateIndex(frame); + + if (frame->format != (int)AVPixelFormat.AV_PIX_FMT_NV12) { - if (!_isOpened || _disposed) return; + InitOrUpdateSwsContext((AVPixelFormat)frame->format, frame->width, frame->height, AVPixelFormat.AV_PIX_FMT_NV12); - if (timeSeconds < 0) timeSeconds = 0; - double targetFrameIndex = timeSeconds * FPS; + _pConvertedFrame->data[0] = yBuffer; + _pConvertedFrame->data[1] = uvBuffer; + _pConvertedFrame->linesize[0] = yStride; + _pConvertedFrame->linesize[1] = uvStride; - // If already at target index or very close future, skip frames instead of seek to avoid I-frame overhead - bool useSkip = targetFrameIndex >= _currentFrameIndex && targetFrameIndex - _currentFrameIndex < 60; - if (useSkip) - { - SkipFrames((int)(targetFrameIndex - _currentFrameIndex)); - return; - } - - if (_hasCachedFrame) - { - ffmpeg.av_frame_unref(_pCachedFrame); - _hasCachedFrame = false; - } - - long targetPts = (long)Math.Round(timeSeconds / ffmpeg.av_q2d(_timeBase)); - int ret = ffmpeg.av_seek_frame(_pFormatContext, _videoStreamIndex, targetPts, ffmpeg.AVSEEK_FLAG_BACKWARD); - if (ret < 0) - { - // Fallback to start of file on seek failure - ret = ffmpeg.av_seek_frame(_pFormatContext, _videoStreamIndex, 0, ffmpeg.AVSEEK_FLAG_BACKWARD); - if (ret < 0) - { - throw new Exception($"Seek failed: {GetFFmpegErrorString(ret)}"); - } - } - - // Flush decoder internal cache - ffmpeg.avcodec_flush_buffers(_pCodecContext); - _isDraining = false; - - // Decode and discard frames until the first frame past the specified time is found - while (true) - { - ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame); - if (ret == 0) - { - double currentPts = GetTimestampAndUpdateIndex(_pDecodedFrame); - - // Check from half frame earlier to account for floating point errors - if (currentPts >= timeSeconds - (0.5 / FPS)) - { - // Cache the first matching frame so it will be consumed by the next TryReadFrame call - ffmpeg.av_frame_move_ref(_pCachedFrame, _pDecodedFrame); - _hasCachedFrame = true; - break; - } - - ffmpeg.av_frame_unref(_pDecodedFrame); - continue; - } - - if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN)) - { - if (_isDraining) break; - - if (!ReadAndSendPacket()) - { - _isDraining = true; - ffmpeg.avcodec_send_packet(_pCodecContext, null); - } - } - else if (ret == ffmpeg.AVERROR_EOF) - { - break; - } - else - { - throw new Exception($"Error during seek decoding: {GetFFmpegErrorString(ret)}"); - } - } + ffmpeg.sws_scale( + _pSwsContext, + frame->data, frame->linesize, + 0, frame->height, + _pConvertedFrame->data, _pConvertedFrame->linesize + ); } - } - - /// - /// Seeks to a specific frame index. - /// - /// Target frame index to seek to. - public void SeekToFrame(long frameIndex) - { - SeekToTime(frameIndex / FPS); - } - - private string GetFFmpegErrorString(int errnum) - { - const ulong bufferSize = 1024; - byte* buffer = stackalloc byte[(int)bufferSize]; - if (ffmpeg.av_strerror(errnum, buffer, bufferSize) >= 0) + else { - return System.Runtime.InteropServices.Marshal.PtrToStringAnsi((IntPtr)buffer) ?? $"Unknown error (Code: {errnum})"; + int w = frame->width; + int h = frame->height; + int uvH = h / 2; + + // Copy Y plane + for (int y = 0; y < h; y++) + { + Buffer.MemoryCopy( + frame->data[0] + y * frame->linesize[0], + yBuffer + y * yStride, + w, w + ); + } + + // Copy UV plane + for (int y = 0; y < uvH; y++) + { + Buffer.MemoryCopy( + frame->data[1] + y * frame->linesize[1], + uvBuffer + y * uvStride, + w, w + ); + } } - return $"Unknown error (Code: {errnum})"; - } - public void Dispose() - { - lock (_lock) - { - if (_disposed) return; - _disposed = true; - - if (_pSwsContext != null) - { - ffmpeg.sws_freeContext(_pSwsContext); - _pSwsContext = null; - } - - if (_pDecodedFrame != null) - { - var pFrame = _pDecodedFrame; - ffmpeg.av_frame_free(&pFrame); - _pDecodedFrame = null; - } - - if (_pCachedFrame != null) - { - var pFrame = _pCachedFrame; - ffmpeg.av_frame_free(&pFrame); - _pCachedFrame = null; - } - - if (_pConvertedFrame != null) - { - var pFrame = _pConvertedFrame; - ffmpeg.av_frame_free(&pFrame); - _pConvertedFrame = null; - } - - if (_pPacket != null) - { - var pPacket = _pPacket; - ffmpeg.av_packet_free(&pPacket); - _pPacket = null; - } - - if (_pCodecContext != null) - { - var pCodecCtx = _pCodecContext; - ffmpeg.avcodec_free_context(&pCodecCtx); - _pCodecContext = null; - } - - if (_pFormatContext != null) - { - var pFormatCtx = _pFormatContext; - ffmpeg.avformat_close_input(&pFormatCtx); - _pFormatContext = null; - } - - _isOpened = false; - GC.SuppressFinalize(this); - } - } - - ~FFmpegNativeDecoder() - { - Dispose(); + ffmpeg.av_frame_unref(frame); + return true; } } -} + + // ReSharper disable once InconsistentNaming + public bool TryReadNV12Frame( + NativeArray yBuffer, + NativeArray uvBuffer, + out double timestamp) + { + int w = Width; + int h = Height; + int uvH = h / 2; + + if (yBuffer.Length < w * h || uvBuffer.Length < w * uvH) + { + throw new ArgumentException("Buffer size is smaller than video NV12 plane resolution."); + } + + return TryReadNV12Frame( + (byte*)yBuffer.GetUnsafePtr(), w, + (byte*)uvBuffer.GetUnsafePtr(), w, + out timestamp + ); + } + + /// + /// Decodes and discards a specified number of frames immediately (for fast forward skipping). + /// + /// Number of frames to discard. + private void SkipFrames(int count) + { + lock (_lock) + { + if (!_isOpened || _disposed) return; + if (count <= 0) return; + + int skipped = 0; + if (_hasCachedFrame) + { + ffmpeg.av_frame_unref(_pCachedFrame); + _hasCachedFrame = false; + skipped++; + } + + while (skipped < count) + { + int ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame); + if (ret == 0) + { + GetTimestampAndUpdateIndex(_pDecodedFrame); + ffmpeg.av_frame_unref(_pDecodedFrame); + skipped++; + continue; + } + + if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN)) + { + if (_isDraining) break; + + if (!ReadAndSendPacket()) + { + _isDraining = true; + ffmpeg.avcodec_send_packet(_pCodecContext, null); + } + } + else if (ret == ffmpeg.AVERROR_EOF) + { + break; + } + else + { + throw new Exception($"Error skipping frames: {GetFFmpegErrorString(ret)}"); + } + } + } + } + + /// + /// Seeks to a specific playback time in seconds. + /// + /// Target time in seconds to seek to. + public void SeekToTime(double timeSeconds) + { + lock (_lock) + { + if (!_isOpened || _disposed) return; + + if (timeSeconds < 0) timeSeconds = 0; + double targetFrameIndex = timeSeconds * FPS; + + // If already at target index or very close future, skip frames instead of seek to avoid I-frame overhead + bool useSkip = targetFrameIndex >= _currentFrameIndex && targetFrameIndex - _currentFrameIndex < 60; + if (useSkip) + { + SkipFrames((int)(targetFrameIndex - _currentFrameIndex)); + return; + } + + if (_hasCachedFrame) + { + ffmpeg.av_frame_unref(_pCachedFrame); + _hasCachedFrame = false; + } + + long targetPts = (long)Math.Round(timeSeconds / ffmpeg.av_q2d(_timeBase)); + int ret = ffmpeg.av_seek_frame(_pFormatContext, _videoStreamIndex, targetPts, ffmpeg.AVSEEK_FLAG_BACKWARD); + if (ret < 0) + { + // Fallback to start of file on seek failure + ret = ffmpeg.av_seek_frame(_pFormatContext, _videoStreamIndex, 0, ffmpeg.AVSEEK_FLAG_BACKWARD); + if (ret < 0) + { + throw new Exception($"Seek failed: {GetFFmpegErrorString(ret)}"); + } + } + + // Flush decoder internal cache + ffmpeg.avcodec_flush_buffers(_pCodecContext); + _isDraining = false; + + // Decode and discard frames until the first frame past the specified time is found + while (true) + { + ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame); + if (ret == 0) + { + double currentPts = GetTimestampAndUpdateIndex(_pDecodedFrame); + + // Check from half frame earlier to account for floating point errors + if (currentPts >= timeSeconds - (0.5 / FPS)) + { + // Cache the first matching frame so it will be consumed by the next TryReadFrame call + ffmpeg.av_frame_move_ref(_pCachedFrame, _pDecodedFrame); + _hasCachedFrame = true; + break; + } + + ffmpeg.av_frame_unref(_pDecodedFrame); + continue; + } + + if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN)) + { + if (_isDraining) break; + + if (!ReadAndSendPacket()) + { + _isDraining = true; + ffmpeg.avcodec_send_packet(_pCodecContext, null); + } + } + else if (ret == ffmpeg.AVERROR_EOF) + { + break; + } + else + { + throw new Exception($"Error during seek decoding: {GetFFmpegErrorString(ret)}"); + } + } + } + } + + /// + /// Seeks to a specific frame index. + /// + /// Target frame index to seek to. + public void SeekToFrame(long frameIndex) + { + SeekToTime(frameIndex / FPS); + } + + private string GetFFmpegErrorString(int errnum) + { + const ulong bufferSize = 1024; + byte* buffer = stackalloc byte[(int)bufferSize]; + if (ffmpeg.av_strerror(errnum, buffer, bufferSize) >= 0) + { + return System.Runtime.InteropServices.Marshal.PtrToStringAnsi((IntPtr)buffer) ?? $"Unknown error (Code: {errnum})"; + } + return $"Unknown error (Code: {errnum})"; + } + + public void Dispose() + { + lock (_lock) + { + if (_disposed) return; + _disposed = true; + + if (_pSwsContext != null) + { + ffmpeg.sws_freeContext(_pSwsContext); + _pSwsContext = null; + } + + if (_pDecodedFrame != null) + { + var pFrame = _pDecodedFrame; + ffmpeg.av_frame_free(&pFrame); + _pDecodedFrame = null; + } + + if (_pCachedFrame != null) + { + var pFrame = _pCachedFrame; + ffmpeg.av_frame_free(&pFrame); + _pCachedFrame = null; + } + + if (_pConvertedFrame != null) + { + var pFrame = _pConvertedFrame; + ffmpeg.av_frame_free(&pFrame); + _pConvertedFrame = null; + } + + if (_pPacket != null) + { + var pPacket = _pPacket; + ffmpeg.av_packet_free(&pPacket); + _pPacket = null; + } + + if (_pCodecContext != null) + { + var pCodecCtx = _pCodecContext; + ffmpeg.avcodec_free_context(&pCodecCtx); + _pCodecContext = null; + } + + if (_pFormatContext != null) + { + var pFormatCtx = _pFormatContext; + ffmpeg.avformat_close_input(&pFormatCtx); + _pFormatContext = null; + } + + _isOpened = false; + GC.SuppressFinalize(this); + } + } + + ~FFmpegNativeDecoder() + { + Dispose(); + } +} \ No newline at end of file diff --git a/Util/FFmpeg/FFmpegNativeEncoder.cs b/Util/FFmpeg/FFmpegNativeEncoder.cs index 5b3a9cb..915bd25 100644 --- a/Util/FFmpeg/FFmpegNativeEncoder.cs +++ b/Util/FFmpeg/FFmpegNativeEncoder.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using ADOFAIRenderer.Data; using FFmpeg.AutoGen; using MelonLoader; using Unity.Collections; diff --git a/Util/FFmpeg/RateControlMode.cs b/Util/FFmpeg/RateControlMode.cs deleted file mode 100644 index b790945..0000000 --- a/Util/FFmpeg/RateControlMode.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace ADOFAIRenderer.Util.FFmpeg -{ - [SuppressMessage("ReSharper", "InconsistentNaming")] - public enum RateControlMode - { - CBR, - VBR, - CQP - } -} \ No newline at end of file diff --git a/Util/FFmpeg/VideoEncoder.cs b/Util/FFmpeg/VideoEncoder.cs index e813d45..724a4e0 100644 --- a/Util/FFmpeg/VideoEncoder.cs +++ b/Util/FFmpeg/VideoEncoder.cs @@ -2,112 +2,112 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using ADOFAIRenderer.Data; -namespace ADOFAIRenderer.Util.FFmpeg +namespace ADOFAIRenderer.Util.FFmpeg; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +internal enum EncoderKind { - [SuppressMessage("ReSharper", "InconsistentNaming")] - public enum EncoderKind - { - Software, - NVENC, - AMF, - QSV, - VideoToolbox - } - - [SuppressMessage("ReSharper", "InconsistentNaming")] - internal sealed class VideoEncoder : IEquatable - { - // 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 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 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; - } + Software, + NVENC, + AMF, + QSV, + VideoToolbox } + +[SuppressMessage("ReSharper", "InconsistentNaming")] +internal sealed class VideoEncoder : IEquatable +{ + // 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 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 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; +} \ No newline at end of file diff --git a/Util/GPUUtil.cs b/Util/GPUUtil.cs index fa5b6ef..2e1fdcb 100644 --- a/Util/GPUUtil.cs +++ b/Util/GPUUtil.cs @@ -2,152 +2,152 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.RegularExpressions; +using ADOFAIRenderer.Data; using ADOFAIRenderer.Util.FFmpeg; using MelonLoader; using UnityEngine; -namespace ADOFAIRenderer.Util +namespace ADOFAIRenderer.Util; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +internal enum GPUVendor { - [SuppressMessage("ReSharper", "InconsistentNaming")] - internal enum GPUVendor - { - Unknown, NVIDIA, AMD, Intel, Apple - } + Unknown, NVIDIA, AMD, Intel, Apple +} - internal static class GPUUtil +internal static class GPUUtil +{ + private const string NvidiaPattern = + @"^NVIDIA GeForce [RG]TX [0-9]+(?> SUPER| Ti(?> SUPER)?)?(?> [0-9]+GB)?(?> Laptop GPU)?(?>/PCIe/SSE2)?$"; + + private const string IntelPattern = + @"^Intel(?>\(R\))? (?>U?HD Graphics [0-9]+|Arc\(TM\) [A-C][0-9]+ Graphics)(?> \([^)]+\))?$"; + + // ReSharper disable once InconsistentNaming + private const string AMDPattern = + @"^AMD Radeon RX [0-9]+(?> XT)?$"; + + private static GPUVendor _cachedVendor = GPUVendor.Unknown; + private static bool _hasCachedVendor; + + private static GPUVendor GetActiveGPUVendor() { - private const string NvidiaPattern = - @"^NVIDIA GeForce [RG]TX [0-9]+(?> SUPER| Ti(?> SUPER)?)?(?> [0-9]+GB)?(?> Laptop GPU)?(?>/PCIe/SSE2)?$"; + if (_hasCachedVendor) return _cachedVendor; - private const string IntelPattern = - @"^Intel(?>\(R\))? (?>U?HD Graphics [0-9]+|Arc\(TM\) [A-C][0-9]+ Graphics)(?> \([^)]+\))?$"; - - // ReSharper disable once InconsistentNaming - private const string AMDPattern = - @"^AMD Radeon RX [0-9]+(?> XT)?$"; - - private static GPUVendor _cachedVendor = GPUVendor.Unknown; - private static bool _hasCachedVendor; - - private static GPUVendor GetActiveGPUVendor() - { - if (_hasCachedVendor) return _cachedVendor; - - var gpuName = SystemInfo.graphicsDeviceName; - Melon.Logger.Msg($"Active GPU: {gpuName}"); + var gpuName = SystemInfo.graphicsDeviceName; + Melon.Logger.Msg($"Active GPU: {gpuName}"); - _cachedVendor = GPUVendor.Unknown; - foreach (Match unused in Regex.Matches(gpuName, NvidiaPattern)) + _cachedVendor = GPUVendor.Unknown; + foreach (Match unused in Regex.Matches(gpuName, NvidiaPattern)) + { + _cachedVendor = GPUVendor.NVIDIA; + break; + } + + if (_cachedVendor == GPUVendor.Unknown) + { + foreach (Match unused in Regex.Matches(gpuName, IntelPattern)) { - _cachedVendor = GPUVendor.NVIDIA; + _cachedVendor = GPUVendor.Intel; break; } + } - if (_cachedVendor == GPUVendor.Unknown) + if (_cachedVendor == GPUVendor.Unknown) + { + foreach (Match unused in Regex.Matches(gpuName, AMDPattern)) { - foreach (Match unused in Regex.Matches(gpuName, IntelPattern)) - { - _cachedVendor = GPUVendor.Intel; - break; - } + _cachedVendor = GPUVendor.AMD; + break; } - - if (_cachedVendor == GPUVendor.Unknown) - { - foreach (Match unused in Regex.Matches(gpuName, AMDPattern)) - { - _cachedVendor = GPUVendor.AMD; - break; - } - } - - if (_cachedVendor == GPUVendor.Unknown && Application.platform == RuntimePlatform.OSXPlayer) - { - _cachedVendor = GPUVendor.Apple; - } - - _hasCachedVendor = true; - return _cachedVendor; } - private static double? _cachedNvidiaVersion; - private static bool _hasCachedNvidiaVersion; - - public static double? GetNvidiaDriverVersion() + if (_cachedVendor == GPUVendor.Unknown && Application.platform == RuntimePlatform.OSXPlayer) { - if (_hasCachedNvidiaVersion) return _cachedNvidiaVersion; + _cachedVendor = GPUVendor.Apple; + } + + _hasCachedVendor = true; + return _cachedVendor; + } - _hasCachedNvidiaVersion = true; - _cachedNvidiaVersion = null; + private static double? _cachedNvidiaVersion; + private static bool _hasCachedNvidiaVersion; - if (GetActiveGPUVendor() != GPUVendor.NVIDIA) return null; + public static double? GetNvidiaDriverVersion() + { + if (_hasCachedNvidiaVersion) return _cachedNvidiaVersion; - // 1. Try parsing from UnityEngine.SystemInfo.graphicsDeviceVersion - var deviceVersion = SystemInfo.graphicsDeviceVersion; - if (!string.IsNullOrEmpty(deviceVersion)) + _hasCachedNvidiaVersion = true; + _cachedNvidiaVersion = null; + + if (GetActiveGPUVendor() != GPUVendor.NVIDIA) return null; + + // 1. Try parsing from UnityEngine.SystemInfo.graphicsDeviceVersion + var deviceVersion = SystemInfo.graphicsDeviceVersion; + if (!string.IsNullOrEmpty(deviceVersion)) + { + var match = Regex.Match(deviceVersion, @"\b(\d+)\.(\d+)\.(\d+)\.(\d+)\b"); + if (match.Success) { - var match = Regex.Match(deviceVersion, @"\b(\d+)\.(\d+)\.(\d+)\.(\d+)\b"); - if (match.Success) + if (int.TryParse(match.Groups[3].Value, out var sub) && int.TryParse(match.Groups[4].Value, out int build)) { - if (int.TryParse(match.Groups[3].Value, out var sub) && int.TryParse(match.Groups[4].Value, out int build)) - { - var nvidiaVerInt = sub % 10 * 10000 + build; - _cachedNvidiaVersion = nvidiaVerInt / 100.0; - Melon.Logger.Msg($"Parsed NVIDIA Driver Version from SystemInfo: {_cachedNvidiaVersion:F2}"); - return _cachedNvidiaVersion; - } + var nvidiaVerInt = sub % 10 * 10000 + build; + _cachedNvidiaVersion = nvidiaVerInt / 100.0; + Melon.Logger.Msg($"Parsed NVIDIA Driver Version from SystemInfo: {_cachedNvidiaVersion:F2}"); + return _cachedNvidiaVersion; } } + } - // 2. Fallback to Windows Registry (only on Windows NT platforms via RegistryUtil) - if (System.Environment.OSVersion.Platform != System.PlatformID.Win32NT) return null; - const string classKey = @"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}"; - var subkeys = RegistryUtil.GetHKLMSubKeyNames(classKey); - if (subkeys == null) return null; + // 2. Fallback to Windows Registry (only on Windows NT platforms via RegistryUtil) + if (System.Environment.OSVersion.Platform != System.PlatformID.Win32NT) return null; + const string classKey = @"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}"; + var subkeys = RegistryUtil.GetHKLMSubKeyNames(classKey); + if (subkeys == null) return null; - foreach (var subkey in subkeys) - { - var subkeyPath = classKey + "\\" + subkey; - var provider = RegistryUtil.GetHKLMValue(subkeyPath, "ProviderName"); - if (provider == null || !provider.Contains("NVIDIA")) continue; + foreach (var subkey in subkeys) + { + var subkeyPath = classKey + "\\" + subkey; + var provider = RegistryUtil.GetHKLMValue(subkeyPath, "ProviderName"); + if (provider == null || !provider.Contains("NVIDIA")) continue; - var driverVersionStr = RegistryUtil.GetHKLMValue(subkeyPath, "DriverVersion"); - if (string.IsNullOrEmpty(driverVersionStr)) continue; + var driverVersionStr = RegistryUtil.GetHKLMValue(subkeyPath, "DriverVersion"); + if (string.IsNullOrEmpty(driverVersionStr)) continue; - var regMatch = Regex.Match(driverVersionStr, @"\b(\d+)\.(\d+)\.(\d+)\.(\d+)\b"); - if (!regMatch.Success) continue; - if (!int.TryParse(regMatch.Groups[3].Value, out var sub) || - !int.TryParse(regMatch.Groups[4].Value, out var build)) continue; + var regMatch = Regex.Match(driverVersionStr, @"\b(\d+)\.(\d+)\.(\d+)\.(\d+)\b"); + if (!regMatch.Success) continue; + if (!int.TryParse(regMatch.Groups[3].Value, out var sub) || + !int.TryParse(regMatch.Groups[4].Value, out var build)) continue; - var nvidiaVerInt = sub % 10 * 10000 + build; - _cachedNvidiaVersion = nvidiaVerInt / 100f; - Melon.Logger.Msg($"Parsed NVIDIA Driver Version from Registry (RegistryUtil): {_cachedNvidiaVersion:F2}"); - return _cachedNvidiaVersion; - } - - return null; + var nvidiaVerInt = sub % 10 * 10000 + build; + _cachedNvidiaVersion = nvidiaVerInt / 100f; + Melon.Logger.Msg($"Parsed NVIDIA Driver Version from Registry (RegistryUtil): {_cachedNvidiaVersion:F2}"); + return _cachedNvidiaVersion; } - public static List GetPrioritizedEncoders(Codec codec, bool forceSoftware = false) + return null; + } + + public static List GetPrioritizedEncoders(Codec codec, bool forceSoftware = false) + { + var encoders = VideoEncoder.All.Where(e => e.Codec == codec).ToList(); + if (forceSoftware) { - var encoders = VideoEncoder.All.Where(e => e.Codec == codec).ToList(); - if (forceSoftware) - { - return encoders.Where(e => !e.IsHardware).ToList(); - } - - var vendor = GetActiveGPUVendor(); - var prioritized = new List(); - var others = new List(); - - foreach (var item in encoders) - { - if (item.Vendor == vendor) prioritized.Add(item); - else others.Add(item); - } - - prioritized.AddRange(others); - return prioritized; + return encoders.Where(e => !e.IsHardware).ToList(); } + + var vendor = GetActiveGPUVendor(); + var prioritized = new List(); + var others = new List(); + + foreach (var item in encoders) + { + if (item.Vendor == vendor) prioritized.Add(item); + else others.Add(item); + } + + prioritized.AddRange(others); + return prioritized; } } \ No newline at end of file