SHA256
693 lines
30 KiB
C#
693 lines
30 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using MelonLoader;
|
|
using MelonLoader.Utils;
|
|
|
|
namespace ADOFAIRenderer.UI
|
|
{
|
|
public class FFmpegDownloadUI : MonoBehaviour
|
|
{
|
|
private GameObject _buttonContainerObj = null!;
|
|
private GameObject _progressBarObj = null!;
|
|
private GameObject _progressBarFillObj = null!;
|
|
private RectTransform _fillRect = null!;
|
|
private TextMeshProUGUI _progressText = null!;
|
|
private GameObject _statusTextObj = null!;
|
|
private TextMeshProUGUI _statusText = null!;
|
|
|
|
private bool _isDownloading;
|
|
private string DownloadUrl => Application.platform switch
|
|
{
|
|
RuntimePlatform.WindowsPlayer => "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-05-31-13-22/ffmpeg-N-124714-g49a77d37be-win64-gpl-shared.zip",
|
|
_ => "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-05-31-13-22/ffmpeg-N-124714-g49a77d37be-linux64-gpl-shared.tar.xz"
|
|
};
|
|
|
|
private System.Threading.CancellationTokenSource? _cts;
|
|
private readonly System.Collections.Concurrent.ConcurrentQueue<Action> _mainThreadQueue = new();
|
|
|
|
public static bool CheckFFmpegExists()
|
|
{
|
|
var userLibs = MelonEnvironment.UserLibsDirectory;
|
|
if (!Directory.Exists(userLibs)) return false;
|
|
|
|
string[] searchPatterns = Application.platform switch
|
|
{
|
|
RuntimePlatform.WindowsPlayer => new[]
|
|
{
|
|
"avcodec-*.dll", "avformat-*.dll", "avutil-*.dll", "swscale-*.dll", "swresample-*.dll"
|
|
},
|
|
RuntimePlatform.OSXPlayer => new[]
|
|
{
|
|
"libavcodec.*.dylib", "libavformat.*.dylib", "libavutil.*.dylib", "libswscale.*.dylib", "libswresample.*.dylib"
|
|
},
|
|
_ => new[]
|
|
{
|
|
"libavcodec.so*", "libavformat.so*", "libavutil.so*", "libswscale.so*", "libswresample.so*"
|
|
}
|
|
};
|
|
|
|
foreach (var pattern in searchPatterns)
|
|
{
|
|
if (Directory.GetFiles(userLibs, pattern).Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public static void ShowDownloadUI()
|
|
{
|
|
|
|
if (CheckFFmpegExists()) return;
|
|
|
|
var obj = new GameObject("FFmpegDownloadUI");
|
|
DontDestroyOnLoad(obj);
|
|
obj.AddComponent<FFmpegDownloadUI>();
|
|
}
|
|
|
|
public static FFmpegDownloadUI? Instance { get; private set; }
|
|
|
|
private void Awake()
|
|
{
|
|
Instance = this;
|
|
if (CheckFFmpegExists())
|
|
{
|
|
Destroy(gameObject);
|
|
}
|
|
}
|
|
|
|
private static TMP_FontAsset GetNanumSquareNeoBdFontAsset()
|
|
{
|
|
TMP_FontAsset? font = null;
|
|
if (RenderMod.FontBundle != null)
|
|
{
|
|
try
|
|
{
|
|
font = RenderMod.FontBundle.LoadAsset<TMP_FontAsset>("NanumSquareNeoOTF-Bd SDF");
|
|
}
|
|
catch
|
|
{
|
|
// 로딩 실패
|
|
}
|
|
}
|
|
|
|
// 폴백 처리
|
|
if (font == null)
|
|
{
|
|
try
|
|
{
|
|
font = RDString.fontData.fontTMP;
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
}
|
|
if (font == null)
|
|
{
|
|
// ReSharper disable once Unity.UnknownResource
|
|
font = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
|
|
}
|
|
|
|
if (font != null) return font;
|
|
var fonts = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
|
|
if (fonts.Length > 0) font = fonts[0];
|
|
|
|
return font!;
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
// UI 레이어(5) 설정
|
|
gameObject.layer = 5;
|
|
|
|
// Canvas 설정
|
|
var canvas = gameObject.AddComponent<Canvas>();
|
|
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
canvas.sortingOrder = 32767; // 최상위
|
|
|
|
// 해상도 대응 캔버스 스케일러 설정
|
|
var scaler = gameObject.AddComponent<CanvasScaler>();
|
|
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
|
scaler.referenceResolution = new Vector2(1920, 1080);
|
|
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
|
scaler.matchWidthOrHeight = 0.5f;
|
|
|
|
gameObject.AddComponent<GraphicRaycaster>();
|
|
|
|
// 1. 전체 화면 오버레이 배경 생성
|
|
var bgPanelObj = new GameObject("BackgroundPanel");
|
|
bgPanelObj.transform.SetParent(transform, false);
|
|
bgPanelObj.layer = 5;
|
|
|
|
var bgImage = bgPanelObj.AddComponent<Image>();
|
|
bgImage.color = new Color(0f, 0f, 0f, 0.65f); // 어두운 투명 오버레이
|
|
bgImage.raycastTarget = true;
|
|
|
|
var bgRect = bgPanelObj.GetComponent<RectTransform>();
|
|
bgRect.anchorMin = Vector2.zero;
|
|
bgRect.anchorMax = Vector2.one;
|
|
bgRect.offsetMin = Vector2.zero;
|
|
bgRect.offsetMax = Vector2.zero;
|
|
|
|
var font = GetNanumSquareNeoBdFontAsset();
|
|
|
|
// 2. 모달 컨테이너 생성 (다른 UI들과 디자인 일관성을 위해 동일한 아크릴 다크톤과 흰색 굵은 외곽선 적용)
|
|
var modalContainerObj = new GameObject("ModalContainer");
|
|
modalContainerObj.transform.SetParent(bgPanelObj.transform, false);
|
|
modalContainerObj.layer = 5;
|
|
|
|
var modalImage = modalContainerObj.AddComponent<Image>();
|
|
modalImage.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // 다른 UI의 배경 톤과 동일
|
|
|
|
var modalOutline = modalContainerObj.AddComponent<Outline>();
|
|
modalOutline.effectColor = Color.white; // DebugMonitorUI와 일치하는 흰색 외곽선
|
|
modalOutline.effectDistance = new Vector2(3.0f, -3.0f);
|
|
|
|
var modalRect = modalContainerObj.GetComponent<RectTransform>();
|
|
modalRect.anchorMin = new Vector2(0.5f, 0.5f);
|
|
modalRect.anchorMax = new Vector2(0.5f, 0.5f);
|
|
modalRect.pivot = new Vector2(0.5f, 0.5f);
|
|
modalRect.sizeDelta = new Vector2(600, 320);
|
|
|
|
// 4. 타이틀 텍스트 생성
|
|
var titleObj = new GameObject("TitleText");
|
|
titleObj.transform.SetParent(modalContainerObj.transform, false);
|
|
titleObj.layer = 5;
|
|
|
|
var titleText = titleObj.AddComponent<TextMeshProUGUI>();
|
|
titleText.text = "FFmpeg 라이브러리 설치 필요";
|
|
titleText.fontSize = 26;
|
|
titleText.font = font;
|
|
titleText.color = Color.white;
|
|
titleText.alignment = TextAlignmentOptions.Center;
|
|
|
|
var titleRect = titleObj.GetComponent<RectTransform>();
|
|
titleRect.anchorMin = new Vector2(0, 1);
|
|
titleRect.anchorMax = new Vector2(1, 1);
|
|
titleRect.pivot = new Vector2(0.5f, 1);
|
|
titleRect.anchoredPosition = new Vector2(0, -35);
|
|
titleRect.sizeDelta = new Vector2(0, 40);
|
|
|
|
// 5. 본문 설명 텍스트 생성
|
|
var descObj = new GameObject("DescriptionText");
|
|
descObj.transform.SetParent(modalContainerObj.transform, false);
|
|
descObj.layer = 5;
|
|
|
|
var descText = descObj.AddComponent<TextMeshProUGUI>();
|
|
var descStrBuilder = new StringBuilder();
|
|
descStrBuilder.AppendLine("동영상을 렌더링하려면 FFmpeg 공유 라이브러리가 필요합니다.");
|
|
if (Application.platform == RuntimePlatform.OSXPlayer)
|
|
{
|
|
descStrBuilder.AppendLine("OSX 플랫폼의 경우 신뢰 가능한 공개 빌드가 없어 수동으로 FFmpeg를 빌드하여야 합니다.");
|
|
descStrBuilder.AppendLine("아래 버튼을 눌러 ffmpeg 공식 웹사이트를 열 수 있습니다.");
|
|
}
|
|
else
|
|
{
|
|
descStrBuilder.AppendLine("아래 버튼을 눌러 인터넷에서 자동으로 다운로드하여 설치합니다.");
|
|
descStrBuilder.AppendLine("<color=#aaaaaa><size=13>(다운로드 크기: 약 70MB, UserLibs 폴더에 압축 해제됨)</size></color>");
|
|
}
|
|
|
|
descText.text = descStrBuilder.ToString();
|
|
descText.fontSize = 16;
|
|
descText.font = font;
|
|
descText.color = new Color(0.85f, 0.85f, 0.85f, 1f);
|
|
descText.alignment = TextAlignmentOptions.Center;
|
|
descText.lineSpacing = 10;
|
|
|
|
var descRect = descObj.GetComponent<RectTransform>();
|
|
descRect.anchorMin = new Vector2(0, 0.5f);
|
|
descRect.anchorMax = new Vector2(1, 1);
|
|
descRect.pivot = new Vector2(0.5f, 0.5f);
|
|
descRect.anchoredPosition = new Vector2(0, -60);
|
|
descRect.sizeDelta = new Vector2(-40, 100);
|
|
|
|
// 6. 버튼 컨테이너 생성
|
|
_buttonContainerObj = new GameObject("ButtonContainer", typeof(RectTransform));
|
|
_buttonContainerObj.transform.SetParent(modalContainerObj.transform, false);
|
|
_buttonContainerObj.layer = 5;
|
|
|
|
var btnContainerRect = _buttonContainerObj.GetComponent<RectTransform>();
|
|
btnContainerRect.anchorMin = new Vector2(0, 0);
|
|
btnContainerRect.anchorMax = new Vector2(1, 0);
|
|
btnContainerRect.pivot = new Vector2(0.5f, 0);
|
|
btnContainerRect.anchoredPosition = new Vector2(0, 35);
|
|
btnContainerRect.sizeDelta = new Vector2(0, 50);
|
|
|
|
// 7. 다운로드 버튼 생성
|
|
var downloadBtnObj = new GameObject("DownloadButton");
|
|
downloadBtnObj.transform.SetParent(_buttonContainerObj.transform, false);
|
|
downloadBtnObj.layer = 5;
|
|
|
|
var downloadImg = downloadBtnObj.AddComponent<Image>();
|
|
downloadImg.color = new Color(0.0f, 0.8f, 1.0f, 1.0f); // Sleek cyan/blue
|
|
downloadImg.raycastTarget = true;
|
|
|
|
var downloadBtn = downloadBtnObj.AddComponent<Button>();
|
|
SetupButtonStyle(downloadBtn, new Color(0.0f, 0.8f, 1.0f, 1.0f), new Color(0.2f, 0.9f, 1.0f, 1.0f), new Color(0.0f, 0.6f, 0.8f, 1.0f));
|
|
if (Application.platform != RuntimePlatform.OSXPlayer) downloadBtn.onClick.AddListener(StartDownloadProcess);
|
|
else
|
|
{
|
|
downloadBtn.onClick.AddListener(OpenFFmpegWebsite);
|
|
void OpenFFmpegWebsite() { Application.OpenURL("https://ffmpeg.org"); Destroy(gameObject); }
|
|
}
|
|
|
|
var downloadBtnRect = downloadBtnObj.GetComponent<RectTransform>();
|
|
downloadBtnRect.anchorMin = new Vector2(0.5f, 0.5f);
|
|
downloadBtnRect.anchorMax = new Vector2(0.5f, 0.5f);
|
|
downloadBtnRect.pivot = new Vector2(1f, 0.5f);
|
|
downloadBtnRect.anchoredPosition = new Vector2(-15, 0);
|
|
downloadBtnRect.sizeDelta = new Vector2(180, 45);
|
|
|
|
var downloadTextObj = new GameObject("DownloadText");
|
|
downloadTextObj.transform.SetParent(downloadBtnObj.transform, false);
|
|
downloadTextObj.layer = 5;
|
|
|
|
var downloadText = downloadTextObj.AddComponent<TextMeshProUGUI>();
|
|
downloadText.text = Application.platform != RuntimePlatform.OSXPlayer ? "다운로드 및 설치" : "사이트 열기";
|
|
downloadText.fontSize = 15;
|
|
downloadText.font = font;
|
|
downloadText.color = Color.white;
|
|
downloadText.alignment = TextAlignmentOptions.Center;
|
|
|
|
var downloadTextRect = downloadTextObj.GetComponent<RectTransform>();
|
|
downloadTextRect.anchorMin = Vector2.zero;
|
|
downloadTextRect.anchorMax = Vector2.one;
|
|
downloadTextRect.offsetMin = Vector2.zero;
|
|
downloadTextRect.offsetMax = Vector2.zero;
|
|
|
|
// 8. 취소 버튼 생성
|
|
var cancelBtnObj = new GameObject("CancelButton");
|
|
cancelBtnObj.transform.SetParent(_buttonContainerObj.transform, false);
|
|
cancelBtnObj.layer = 5;
|
|
|
|
var cancelImg = cancelBtnObj.AddComponent<Image>();
|
|
cancelImg.color = new Color(0.2f, 0.2f, 0.23f, 1f);
|
|
cancelImg.raycastTarget = true;
|
|
|
|
var cancelBtn = cancelBtnObj.AddComponent<Button>();
|
|
SetupButtonStyle(cancelBtn, new Color(0.2f, 0.2f, 0.23f, 1f), new Color(0.25f, 0.25f, 0.28f, 1f), new Color(0.15f, 0.15f, 0.18f, 1f));
|
|
cancelBtn.onClick.AddListener(CancelProcess);
|
|
|
|
var cancelBtnRect = cancelBtnObj.GetComponent<RectTransform>();
|
|
cancelBtnRect.anchorMin = new Vector2(0.5f, 0.5f);
|
|
cancelBtnRect.anchorMax = new Vector2(0.5f, 0.5f);
|
|
cancelBtnRect.pivot = new Vector2(0f, 0.5f);
|
|
cancelBtnRect.anchoredPosition = new Vector2(15, 0);
|
|
cancelBtnRect.sizeDelta = new Vector2(180, 45);
|
|
|
|
var cancelTextObj = new GameObject("CancelText");
|
|
cancelTextObj.transform.SetParent(cancelBtnObj.transform, false);
|
|
cancelTextObj.layer = 5;
|
|
|
|
var cancelText = cancelTextObj.AddComponent<TextMeshProUGUI>();
|
|
cancelText.text = "취소";
|
|
cancelText.fontSize = 15;
|
|
cancelText.font = font;
|
|
cancelText.color = new Color(0.9f, 0.9f, 0.9f, 1f);
|
|
cancelText.alignment = TextAlignmentOptions.Center;
|
|
|
|
var cancelTextRect = cancelTextObj.GetComponent<RectTransform>();
|
|
cancelTextRect.anchorMin = Vector2.zero;
|
|
cancelTextRect.anchorMax = Vector2.one;
|
|
cancelTextRect.offsetMin = Vector2.zero;
|
|
cancelTextRect.offsetMax = Vector2.zero;
|
|
|
|
// 9. 프로그레스 바 컨테이너 생성
|
|
_progressBarObj = new GameObject("ProgressBarContainer");
|
|
_progressBarObj.transform.SetParent(modalContainerObj.transform, false);
|
|
_progressBarObj.layer = 5;
|
|
_progressBarObj.SetActive(false);
|
|
|
|
var barBgImg = _progressBarObj.AddComponent<Image>();
|
|
barBgImg.color = new Color(0.08f, 0.08f, 0.1f, 1.0f); // DebugMonitorUI의 BarBackgroundColor와 동일
|
|
|
|
var barBgOutline = _progressBarObj.AddComponent<Outline>();
|
|
barBgOutline.effectColor = Color.white; // DebugMonitorUI의 BarOutlineColor와 동일
|
|
barBgOutline.effectDistance = new Vector2(3.0f, -3.0f); // DebugMonitorUI의 BarOutlineDistance와 동일
|
|
|
|
var barRect = _progressBarObj.GetComponent<RectTransform>();
|
|
barRect.anchorMin = new Vector2(0.5f, 0);
|
|
barRect.anchorMax = new Vector2(0.5f, 0);
|
|
barRect.pivot = new Vector2(0.5f, 0);
|
|
barRect.anchoredPosition = new Vector2(0, 75);
|
|
barRect.sizeDelta = new Vector2(480, 24);
|
|
|
|
// 10. 프로그레스 바 채우기 이미지 생성
|
|
_progressBarFillObj = new GameObject("ProgressBarFill");
|
|
_progressBarFillObj.transform.SetParent(_progressBarObj.transform, false);
|
|
_progressBarFillObj.layer = 5;
|
|
|
|
var fillImg = _progressBarFillObj.AddComponent<Image>();
|
|
fillImg.color = new Color(0.0f, 0.8f, 1.0f, 1.0f); // DebugMonitorUI의 BarFillColor와 동일
|
|
|
|
_fillRect = _progressBarFillObj.GetComponent<RectTransform>();
|
|
_fillRect.anchorMin = new Vector2(0, 0);
|
|
_fillRect.anchorMax = new Vector2(0, 1); // anchorMax.x가 동적으로 변경됨
|
|
_fillRect.pivot = new Vector2(0, 0.5f);
|
|
_fillRect.anchoredPosition = Vector2.zero;
|
|
_fillRect.sizeDelta = Vector2.zero;
|
|
|
|
// 11. 프로그레스 텍스트 생성 (진행률 표시)
|
|
var progressTextObj = new GameObject("ProgressBarText");
|
|
progressTextObj.transform.SetParent(_progressBarObj.transform, false);
|
|
progressTextObj.layer = 5;
|
|
|
|
_progressText = progressTextObj.AddComponent<TextMeshProUGUI>();
|
|
_progressText.fontSize = 16; // DebugMonitorUI의 BarTextFontSize와 일치
|
|
_progressText.font = font;
|
|
_progressText.color = Color.white;
|
|
_progressText.alignment = TextAlignmentOptions.Center;
|
|
|
|
var progressTextOutline = progressTextObj.AddComponent<Outline>();
|
|
progressTextOutline.effectColor = new Color(0, 0, 0, 0.8f);
|
|
progressTextOutline.effectDistance = new Vector2(1, -1);
|
|
|
|
var progressTextRect = progressTextObj.GetComponent<RectTransform>();
|
|
progressTextRect.anchorMin = Vector2.zero;
|
|
progressTextRect.anchorMax = Vector2.one;
|
|
progressTextRect.offsetMin = Vector2.zero;
|
|
progressTextRect.offsetMax = Vector2.zero;
|
|
|
|
// 12. 상태 텍스트 생성
|
|
_statusTextObj = new GameObject("StatusText");
|
|
_statusTextObj.transform.SetParent(modalContainerObj.transform, false);
|
|
_statusTextObj.layer = 5;
|
|
_statusTextObj.SetActive(false);
|
|
|
|
_statusText = _statusTextObj.AddComponent<TextMeshProUGUI>();
|
|
_statusText.fontSize = 16; // DebugMonitorUI의 텍스트 크기와 일치
|
|
_statusText.font = font;
|
|
_statusText.color = new Color(0.8f, 0.8f, 0.8f, 1f);
|
|
_statusText.alignment = TextAlignmentOptions.Center;
|
|
|
|
var statusRect = _statusTextObj.GetComponent<RectTransform>();
|
|
statusRect.anchorMin = new Vector2(0, 0);
|
|
statusRect.anchorMax = new Vector2(1, 0);
|
|
statusRect.pivot = new Vector2(0.5f, 0);
|
|
statusRect.anchoredPosition = new Vector2(0, 35);
|
|
statusRect.sizeDelta = new Vector2(0, 30);
|
|
}
|
|
|
|
private void SetupButtonStyle(Button button, Color normalColor, Color hoverColor, Color pressedColor)
|
|
{
|
|
button.transition = Selectable.Transition.ColorTint;
|
|
var colors = button.colors;
|
|
colors.normalColor = normalColor;
|
|
colors.highlightedColor = hoverColor;
|
|
colors.pressedColor = pressedColor;
|
|
colors.disabledColor = new Color(0.3f, 0.3f, 0.3f, 1f);
|
|
button.colors = colors;
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
// 백그라운드 스레드에서 메인 스레드로 넘겨진 작업 순차 실행
|
|
while (_mainThreadQueue.TryDequeue(out var action))
|
|
{
|
|
action?.Invoke();
|
|
}
|
|
}
|
|
|
|
private void ExecuteOnMainThread(Action action)
|
|
{
|
|
_mainThreadQueue.Enqueue(action);
|
|
}
|
|
|
|
private void StartDownloadProcess()
|
|
{
|
|
if (_isDownloading) return;
|
|
_isDownloading = true;
|
|
|
|
// 버튼 숨기고 프로그레스 바 및 상태 텍스트 표시
|
|
_buttonContainerObj.SetActive(false);
|
|
_progressBarObj.SetActive(true);
|
|
_statusTextObj.SetActive(true);
|
|
|
|
_cts = new System.Threading.CancellationTokenSource();
|
|
|
|
// 백그라운드 스레드에서 다운로드 시작
|
|
Task.Run(async () =>
|
|
{
|
|
string tempZipPath = "";
|
|
try
|
|
{
|
|
UpdateStatus("다운로드 시작하는 중...", new Color(0.8f, 0.8f, 0.8f));
|
|
UpdateProgress(0f, "0%");
|
|
|
|
var userLibsDir = MelonEnvironment.UserLibsDirectory;
|
|
if (!Directory.Exists(userLibsDir))
|
|
{
|
|
Directory.CreateDirectory(userLibsDir);
|
|
}
|
|
|
|
string extension = Application.platform switch
|
|
{
|
|
RuntimePlatform.WindowsPlayer => ".zip",
|
|
RuntimePlatform.OSXPlayer => ".zip",
|
|
_ => ".tar.xz"
|
|
};
|
|
tempZipPath = Path.Combine(userLibsDir, "ffmpeg_temp" + extension);
|
|
|
|
var progressReporter = new Progress<float>(percent =>
|
|
{
|
|
UpdateProgress(percent, $"{percent * 100f:F1}%");
|
|
UpdateStatus($"다운로드 중... ({(percent * 100f):F1}%)", new Color(0f, 0.8f, 1f));
|
|
});
|
|
|
|
await DownloadFileAsync(DownloadUrl, tempZipPath, progressReporter, _cts.Token);
|
|
|
|
// 다운로드 완료 후 압축 해제
|
|
UpdateStatus("압축 해제 및 DLL 파일 복사 중...", new Color(0.9f, 0.9f, 0.2f));
|
|
UpdateProgress(0.99f, "압축 해제 중...");
|
|
|
|
await ExtractDllsAsync(tempZipPath, userLibsDir);
|
|
|
|
// 완료 상태 업데이트
|
|
UpdateStatus("FFmpeg 설치 완료!", new Color(0.2f, 0.9f, 0.2f));
|
|
UpdateProgress(1f, "100%");
|
|
|
|
await Task.Delay(1000);
|
|
|
|
// 메인 스레드에서 UI를 삭제
|
|
ExecuteOnMainThread(() =>
|
|
{
|
|
Destroy(gameObject);
|
|
});
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// 취소 시 임시 파일 정리
|
|
if (!string.IsNullOrEmpty(tempZipPath) && File.Exists(tempZipPath))
|
|
{
|
|
try { File.Delete(tempZipPath); }
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
}
|
|
UpdateStatus("다운로드가 취소되었습니다.", new Color(0.9f, 0.5f, 0.2f));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Melon<RenderMod>.Logger.Error($"FFmpeg 다운로드/설치 중 에러 발생: {ex}");
|
|
UpdateStatus($"설치 실패: {ex.Message}", new Color(1f, 0.3f, 0.3f));
|
|
|
|
// 실패 시 버튼 재설정
|
|
ExecuteOnMainThread(() =>
|
|
{
|
|
_isDownloading = false;
|
|
_buttonContainerObj.SetActive(true);
|
|
var downloadBtnText = _buttonContainerObj.transform.Find("DownloadButton/DownloadText")?.GetComponent<TextMeshProUGUI>();
|
|
if (downloadBtnText != null)
|
|
{
|
|
downloadBtnText.text = "재시도 (Retry)";
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
private void CancelProcess()
|
|
{
|
|
if (_isDownloading)
|
|
{
|
|
_cts?.Cancel();
|
|
}
|
|
Destroy(gameObject);
|
|
}
|
|
|
|
private void UpdateStatus(string message, Color color)
|
|
{
|
|
ExecuteOnMainThread(() =>
|
|
{
|
|
if (_statusText != null)
|
|
{
|
|
_statusText.text = message;
|
|
_statusText.color = color;
|
|
}
|
|
});
|
|
}
|
|
|
|
private void UpdateProgress(float percent, string text)
|
|
{
|
|
ExecuteOnMainThread(() =>
|
|
{
|
|
if (_fillRect != null)
|
|
{
|
|
_fillRect.anchorMax = new Vector2(percent, 1f);
|
|
}
|
|
if (_progressText != null)
|
|
{
|
|
_progressText.text = text;
|
|
}
|
|
});
|
|
}
|
|
|
|
private async Task DownloadFileAsync(string url, string destinationPath, IProgress<float> progress, System.Threading.CancellationToken cancellationToken)
|
|
{
|
|
using var client = new HttpClient();
|
|
// User-Agent 헤더 추가하여 차단 방지
|
|
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ADOFAIRenderer/1.0");
|
|
|
|
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var totalBytes = response.Content.Headers.ContentLength ?? -1L;
|
|
await using var contentStream = await response.Content.ReadAsStreamAsync();
|
|
await using var fileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
|
|
|
|
var buffer = new byte[8192];
|
|
var totalRead = 0L;
|
|
int read;
|
|
|
|
while ((read = await contentStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
await fileStream.WriteAsync(buffer, 0, read, cancellationToken);
|
|
totalRead += read;
|
|
|
|
if (totalBytes == -1) continue;
|
|
var progressPercent = (float)totalRead / totalBytes;
|
|
progress.Report(progressPercent);
|
|
}
|
|
}
|
|
|
|
private async Task ExtractDllsAsync(string zipPath, string outputDirectory)
|
|
{
|
|
try
|
|
{
|
|
if (Application.platform == RuntimePlatform.OSXPlayer || Application.platform == RuntimePlatform.WindowsPlayer)
|
|
{
|
|
await Task.Run(() =>
|
|
{
|
|
var targetExtension = Application.platform == RuntimePlatform.OSXPlayer ? ".dylib" : ".dll";
|
|
using var archive = ZipFile.OpenRead(zipPath);
|
|
foreach (var entry in archive.Entries)
|
|
{
|
|
if (entry.FullName.EndsWith(targetExtension, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var targetPath = Path.Combine(outputDirectory, entry.Name);
|
|
|
|
var dir = Path.GetDirectoryName(targetPath);
|
|
if (dir != null && !Directory.Exists(dir))
|
|
{
|
|
Directory.CreateDirectory(dir);
|
|
}
|
|
|
|
entry.ExtractToFile(targetPath, overwrite: true);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
// 리눅스 (또는 기타 플랫폼) - tar.xz 해제
|
|
await Task.Run(() =>
|
|
{
|
|
var tempExtractDir = Path.Combine(outputDirectory, "ffmpeg_temp_extracted");
|
|
if (Directory.Exists(tempExtractDir))
|
|
{
|
|
Directory.Delete(tempExtractDir, true);
|
|
}
|
|
Directory.CreateDirectory(tempExtractDir);
|
|
|
|
var archiveUnixPath = zipPath.Replace('\\', '/');
|
|
var tempExtractUnixDir = tempExtractDir.Replace('\\', '/');
|
|
|
|
var startInfo = new System.Diagnostics.ProcessStartInfo
|
|
{
|
|
FileName = "tar",
|
|
Arguments = $"-xf \"{archiveUnixPath}\" -C \"{tempExtractUnixDir}\"",
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
using (var process = System.Diagnostics.Process.Start(startInfo))
|
|
{
|
|
process!.WaitForExit();
|
|
if (process.ExitCode != 0)
|
|
{
|
|
var error = process.StandardError.ReadToEnd();
|
|
throw new Exception($"tar 압축 해제 실패 (ExitCode: {process.ExitCode}): {error}");
|
|
}
|
|
}
|
|
|
|
// 압축이 풀린 파일 중에서 .so 파일들을 찾아서 outputDirectory로 이동
|
|
var soFiles = Directory.GetFiles(tempExtractDir, "*.so*", SearchOption.AllDirectories);
|
|
foreach (var soFile in soFiles)
|
|
{
|
|
var fileName = Path.GetFileName(soFile);
|
|
var targetPath = Path.Combine(outputDirectory, fileName);
|
|
File.Copy(soFile, targetPath, true);
|
|
}
|
|
|
|
// 임시 디렉토리 정리
|
|
try
|
|
{
|
|
Directory.Delete(tempExtractDir, true);
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
});
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(zipPath))
|
|
{
|
|
try
|
|
{
|
|
File.Delete(zipPath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Melon<RenderMod>.Logger.Warning($"임시 ZIP 파일 삭제 실패: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
_cts?.Dispose();
|
|
if (Instance == this)
|
|
{
|
|
Instance = null;
|
|
}
|
|
}
|
|
}
|
|
} |