using System; using System.IO; using ADOFAIRenderer.Util.FFmpeg; using MelonLoader; using UnityEngine; using FFmpeg.AutoGen; namespace ADOFAIRenderer.Util { // ReSharper disable once InconsistentNaming public class FFmpegVideoBGPlayer : MonoBehaviour { private enum GPUMode { None, YUV420P, NV12 } private FFmpegNativeDecoder? _decoder; // CPU fallback rendering private Texture2D? _textureRGBA; // GPU conversion textures private Texture2D? _textureY; private Texture2D? _textureU; private Texture2D? _textureV; private Texture2D? _textureUV; // Compute Shader variables private ComputeShader? _computeShader; private RenderTexture? _renderTexture; private int _kernelIndex = -1; private double _currentFrameTimestamp; private double _nextFrameTimestamp; private GPUMode _gpuMode = GPUMode.None; private RenderTexture? _targetTexture; private bool _isLooping; private bool _isInitialized; public void Init(string videoPath, RenderTexture targetTexture, bool loop) { CleanUp(); _targetTexture = targetTexture; _isLooping = loop; if (!File.Exists(videoPath)) { Melon.Logger.Warning($"[FFmpegVideoBGPlayer] 비디오 파일이 존재하지 않습니다: {videoPath}"); return; } try { _decoder = new FFmpegNativeDecoder(videoPath); _computeShader = RenderMod.ColorConversionShader; int w = _decoder.Width; int h = _decoder.Height; AVPixelFormat pixFmt = _decoder.PixelFormat; bool gpuInitSuccess = false; if (_computeShader != null) { if (pixFmt == AVPixelFormat.AV_PIX_FMT_NV12) { try { _kernelIndex = _computeShader.FindKernel("CSMain_NV12ToRGBA"); _textureY = new Texture2D(w, h, TextureFormat.R8, false); _textureUV = new Texture2D(w / 2, h / 2, TextureFormat.RG16, false); ConfigureTexture(_textureY); ConfigureTexture(_textureUV); _gpuMode = GPUMode.NV12; gpuInitSuccess = true; Melon.Logger.Msg($"[FFmpegVideoBGPlayer] Compute Shader NV12 모드 초기화 완료"); } catch { } } else if (pixFmt == AVPixelFormat.AV_PIX_FMT_YUV420P || pixFmt == AVPixelFormat.AV_PIX_FMT_YUVJ420P) { try { _kernelIndex = _computeShader.FindKernel("CSMain_YUV420PToRGBA"); _textureY = new Texture2D(w, h, TextureFormat.R8, false); _textureU = new Texture2D(w / 2, h / 2, TextureFormat.R8, false); _textureV = new Texture2D(w / 2, h / 2, TextureFormat.R8, false); ConfigureTexture(_textureY); ConfigureTexture(_textureU); ConfigureTexture(_textureV); _gpuMode = GPUMode.YUV420P; gpuInitSuccess = true; Melon.Logger.Msg($"[FFmpegVideoBGPlayer] Compute Shader YUV420P 모드 초기화 완료"); } catch { } } } if (gpuInitSuccess && _computeShader != null) { _renderTexture = new RenderTexture(w, h, 0, RenderTextureFormat.ARGB32); _renderTexture.enableRandomWrite = true; _renderTexture.Create(); } else { _textureRGBA = new Texture2D(w, h, TextureFormat.RGBA32, false); ConfigureTexture(_textureRGBA); _gpuMode = GPUMode.None; Melon.Logger.Warning($"[FFmpegVideoBGPlayer] GPU 가속 미지원 또는 로드 실패로 CPU 폴백(RGBA)으로 진행합니다."); } _currentFrameTimestamp = 0.0; _nextFrameTimestamp = 0.0; _isInitialized = true; Melon.Logger.Msg($"[FFmpegVideoBGPlayer] 초기화 성공 - 해상도: {w}x{h}, FPS: {_decoder.FPS:F2}"); } catch (Exception ex) { Melon.Logger.Error($"[FFmpegVideoBGPlayer] 초기화 중 에러 발생: {ex}"); CleanUp(); } } private void ConfigureTexture(Texture2D tex) { tex.wrapMode = TextureWrapMode.Clamp; tex.filterMode = FilterMode.Bilinear; } private void Update() { if (!_isInitialized || _decoder == null) return; scrConductor cond = scrConductor.instance; if (cond == null || !cond.hasSongStarted) return; double num4 = (cond.separateCountdownTime ? (cond.crotchetAtStart * (double)ADOBase.conductor.adjustedCountdownTicks) : 0.0); double vidOffset = scrVfxPlus.instance ? scrVfxPlus.instance.vidOffset : 0.0; double targetVideoTime = cond.songposition_minusi - num4 + vidOffset; if (targetVideoTime < 0) { if (_targetTexture != null) { RenderTexture active = RenderTexture.active; RenderTexture.active = _targetTexture; GL.Clear(true, true, Color.black); RenderTexture.active = active; } return; } try { if (targetVideoTime < _currentFrameTimestamp || targetVideoTime - _currentFrameTimestamp > 1.0) { _decoder.SeekToTime(targetVideoTime); _currentFrameTimestamp = targetVideoTime; _nextFrameTimestamp = targetVideoTime; } bool frameUpdated = false; while (targetVideoTime >= _nextFrameTimestamp) { double timestamp; bool success = false; if (_gpuMode == GPUMode.NV12 && _textureY != null && _textureUV != null) { success = _decoder.TryReadNV12Frame( _textureY.GetRawTextureData(), _textureUV.GetRawTextureData(), out timestamp ); } else if (_gpuMode == GPUMode.YUV420P && _textureY != null && _textureU != null && _textureV != null) { success = _decoder.TryReadYUV420PFrame( _textureY.GetRawTextureData(), _textureU.GetRawTextureData(), _textureV.GetRawTextureData(), out timestamp ); } else { if (_textureRGBA != null) { success = _decoder.TryReadFrame( _textureRGBA.GetRawTextureData(), vflip: true, out timestamp ); } } if (success) { _currentFrameTimestamp = timestamp; _nextFrameTimestamp = timestamp + (1.0 / _decoder.FPS); frameUpdated = true; } else { if (_isLooping) { _decoder.SeekToTime(0); _currentFrameTimestamp = 0; _nextFrameTimestamp = 1.0 / _decoder.FPS; break; } else { break; } } } if (frameUpdated) { RenderToTarget(); } } catch (Exception ex) { Melon.Logger.Error($"[FFmpegVideoBGPlayer] Update 재생 중 에러 발생: {ex}"); _isInitialized = false; } } private void RenderToTarget() { if (_decoder == null || _targetTexture == null) return; int w = _decoder.Width; int h = _decoder.Height; if (_gpuMode == GPUMode.NV12 && _textureY != null && _textureUV != null && _computeShader != null && _renderTexture != null) { _textureY.Apply(); _textureUV.Apply(); _computeShader.SetTexture(_kernelIndex, "_YTex", _textureY); _computeShader.SetTexture(_kernelIndex, "_UVTex", _textureUV); _computeShader.SetTexture(_kernelIndex, "_Result", _renderTexture); _computeShader.SetInt("Width", w); _computeShader.SetInt("Height", h); int threadGroupsX = (w + 7) / 8; int threadGroupsY = (h + 7) / 8; _computeShader.Dispatch(_kernelIndex, threadGroupsX, threadGroupsY, 1); Graphics.Blit(_renderTexture, _targetTexture); } else if (_gpuMode == GPUMode.YUV420P && _textureY != null && _textureU != null && _textureV != null && _computeShader != null && _renderTexture != null) { _textureY.Apply(); _textureU.Apply(); _textureV.Apply(); _computeShader.SetTexture(_kernelIndex, "_YTex", _textureY); _computeShader.SetTexture(_kernelIndex, "_UTex", _textureU); _computeShader.SetTexture(_kernelIndex, "_VTex", _textureV); _computeShader.SetTexture(_kernelIndex, "_Result", _renderTexture); _computeShader.SetInt("Width", w); _computeShader.SetInt("Height", h); int threadGroupsX = (w + 7) / 8; int threadGroupsY = (h + 7) / 8; _computeShader.Dispatch(_kernelIndex, threadGroupsX, threadGroupsY, 1); Graphics.Blit(_renderTexture, _targetTexture); } else if (_textureRGBA != null) { _textureRGBA.Apply(); Graphics.Blit(_textureRGBA, _targetTexture); } } private void OnDestroy() { CleanUp(); } public void CleanUp() { _isInitialized = false; if (_decoder != null) { _decoder.Dispose(); _decoder = null; Melon.Logger.Msg("[FFmpegVideoBGPlayer] 디코더 해제 완료."); } if (_textureRGBA != null) { Destroy(_textureRGBA); _textureRGBA = null; } if (_textureY != null) Destroy(_textureY); if (_textureU != null) Destroy(_textureU); if (_textureV != null) Destroy(_textureV); if (_textureUV != null) Destroy(_textureUV); _textureY = null; _textureU = null; _textureV = null; _textureUV = null; if (_renderTexture != null) { _renderTexture.Release(); Destroy(_renderTexture); _renderTexture = null; } } } }