using System; using System.Collections; using System.Collections.Generic; using System.IO; using FairyGUI; using SGModule.Common; using SGModule.Common.Base; using Unity.VisualScripting; using UnityEngine; using UnityEngine.Networking; using UnityEngine.Video; namespace BallKingdomCrush { public class LiveVideoManager : SingletonMonoBehaviour { public static string videoBaseUrl = ""; // 封面缓存 private readonly Dictionary coverCache = new Dictionary(); // 封面提取队列(避免同时开多个VideoPlayer) private readonly Queue coverQueue = new(); // 已下载视频缓存 private readonly HashSet downloadedVideos = new(); // 当前正在下载的视频,用于去重 private readonly HashSet downloadingSet = new(); // ==== 队列 ==== // 普通视频队列和优先队列 private readonly Queue normalQueue = new(); private readonly Queue priorityQueue = new(); private bool isExtracting; private Coroutine normalCoroutine; private Coroutine priorityCoroutine; private string videoLocalDir => Path.Combine(TextureHelper.getResPath(), "LiveVideos"); private string coverLocalDir => Path.Combine(TextureHelper.getResPath(), "LiveVideoCovers"); protected override void Awake() { base.Awake(); InitDirs(); InitCache(); } private void InitDirs() { if (!Directory.Exists(videoLocalDir)) Directory.CreateDirectory(videoLocalDir); if (!Directory.Exists(coverLocalDir)) Directory.CreateDirectory(coverLocalDir); } private void InitCache() { var files = Directory.GetFiles(videoLocalDir, "*.mp4"); foreach (var file in files) { var fileName = Path.GetFileNameWithoutExtension(file); downloadedVideos.Add(fileName); } } #region 缓存清理 public void ClearAllCache() { foreach (var tex in coverCache.Values) if (tex != null) Destroy(tex); coverCache.Clear(); if (Directory.Exists(coverLocalDir)) { Directory.Delete(coverLocalDir, true); Directory.CreateDirectory(coverLocalDir); } if (Directory.Exists(videoLocalDir)) { Directory.Delete(videoLocalDir, true); Directory.CreateDirectory(videoLocalDir); downloadedVideos.Clear(); } } #endregion #region 统一加载视频封面 public IEnumerator LoadCover(string fileName, Action callback) { Texture2D tex = null; var persistentPath = Path.Combine(TextureHelper.getResPath(), "LiveVideoCovers", fileName + ".png"); if (File.Exists(persistentPath)) { tex = LoadTextureFromFile(persistentPath); if (tex != null) { callback?.Invoke(tex); yield break; } } var saPath = Path.Combine(Application.streamingAssetsPath, "LiveVideoCovers", fileName + ".jpg"); #if UNITY_ANDROID && !UNITY_EDITOR using (UnityWebRequest www = UnityWebRequestTexture.GetTexture(saPath)) { yield return www.SendWebRequest(); if (www.result == UnityWebRequest.Result.Success) tex = DownloadHandlerTexture.GetContent(www); else Debug.LogWarning($"LoadCover StreamingAssets失败: {www.error}, file: {fileName}"); } #else if (File.Exists(saPath)) try { var data = File.ReadAllBytes(saPath); tex = new Texture2D(2, 2); tex.LoadImage(data); } catch (Exception e) { Debug.LogWarning($"LoadCover StreamingAssets读取失败: {fileName}, error: {e}"); } #endif callback?.Invoke(tex); } #endregion public static IEnumerator LoadVideoToPlayer(VideoPlayer player, string fileName, GLoader loader, Action onComplete, bool play = true) { string localPath = null; var isDone = false; Instance.GetVideoLocalPath(fileName, path => { localPath = path; isDone = true; }); Debug.Log("LoadVideoToPlayer ------1------" + isDone); while (!isDone) yield return null; if (string.IsNullOrEmpty(localPath)) { onComplete?.Invoke(null); yield break; } if (player.IsDestroyed()) { onComplete?.Invoke(null); yield break; } player.source = VideoSource.Url; Debug.Log("LoadVideoToPlayer diaoyongyici: " + fileName); player.url = Application.platform == RuntimePlatform.Android && !Application.isEditor ? localPath : "file://" + localPath; player.isLooping = true; player.playOnAwake = false; var rtWidth = (int)loader.width; var rtHeight = (int)loader.height; var rt = new RenderTexture(rtWidth, rtHeight, 0); rt.Create(); player.targetTexture = rt; if (!loader.isDisposed) { Debug.Log("LoadVideoToPlayer loader is isDisposed: "); loader.texture = new NTexture(rt); loader.visible = false; } player.Prepare(); var timeout = 3f; var timer = 0f; while (!player.IsDestroyed() && !player.isPrepared && timer < timeout) { yield return null; timer += Time.deltaTime; } if (player.IsDestroyed()) { onComplete?.Invoke(null); yield break; } if (!player.isPrepared) { if (rt != null && !rt.Equals(null)) rt.Release(); onComplete?.Invoke(null); yield break; } if (play) player.Play(); else player.Pause(); if (!loader.isDisposed) loader.visible = true; onComplete?.Invoke(player); } #region 视频下载队列(支持普通 + 优先 + 硬取消 + 去重) public void GetVideoLocalPath(string fileName, Action callback, bool priority = true, int maxRetry = 3, bool hardCancel = false) { var localPath = Path.Combine(videoLocalDir, fileName + ".mp4"); // 已下载直接回调 if (downloadedVideos.Contains(fileName) && File.Exists(localPath)) { callback?.Invoke(localPath); return; } // 去重:如果正在下载中,也直接等待回调 if (downloadingSet.Contains(fileName)) { StartCoroutine(WaitForDownload(fileName, callback)); return; } var task = new VideoTask { FileName = fileName, LocalPath = localPath, Callback = callback, MaxRetry = maxRetry }; if (priority) { if (hardCancel) { // 硬取消:停止当前优先队列协程,清空队列 if (priorityCoroutine != null) { StopCoroutine(priorityCoroutine); priorityCoroutine = null; } priorityQueue.Clear(); } priorityQueue.Enqueue(task); if (priorityCoroutine == null) priorityCoroutine = StartCoroutine(ProcessPriorityQueue()); } else { normalQueue.Enqueue(task); if (normalCoroutine == null) normalCoroutine = StartCoroutine(ProcessNormalQueue()); } } // 等待正在下载的视频完成再回调 private IEnumerator WaitForDownload(string fileName, Action callback) { while (downloadingSet.Contains(fileName)) yield return null; var localPath = Path.Combine(videoLocalDir, fileName + ".mp4"); callback?.Invoke(File.Exists(localPath) ? localPath : null); } // 优先队列协程(单任务) private IEnumerator ProcessPriorityQueue() { while (priorityQueue.Count > 0) { var task = priorityQueue.Dequeue(); yield return DownloadVideoCoroutine(task); } priorityCoroutine = null; } // 普通队列协程(单任务) private IEnumerator ProcessNormalQueue() { while (normalQueue.Count > 0) { var task = normalQueue.Dequeue(); yield return DownloadVideoCoroutine(task); } normalCoroutine = null; } // 核心下载协程 private IEnumerator DownloadVideoCoroutine(VideoTask task) { // 再次检查本地是否已有,避免重复下载 if (downloadedVideos.Contains(task.FileName) && File.Exists(task.LocalPath)) { task.Callback?.Invoke(task.LocalPath); yield break; } downloadingSet.Add(task.FileName); var tmpPath = task.LocalPath + ".downloading"; var url = videoBaseUrl + "LiveAlbums/" + task.FileName + ".mp4"; if (File.Exists(tmpPath)) File.Delete(tmpPath); Debug.Log($"[DownloadVideo] 开始下载视频 url== {url}"); var attempt = 0; var success = false; while (attempt < task.MaxRetry) { attempt++; using (var www = UnityWebRequest.Get(url)) { www.downloadHandler = new DownloadHandlerFile(tmpPath, true); yield return www.SendWebRequest(); if (www.result == UnityWebRequest.Result.Success) { if (File.Exists(task.LocalPath)) File.Delete(task.LocalPath); Debug.Log($"[DownloadVideo] 下载成功,开始解密视频 {task.FileName}"); Rescrypt.DecryptFile(tmpPath, task.LocalPath); Debug.Log($"[DownloadVideo] 解密完成,保存路径:{task.LocalPath}"); downloadedVideos.Add(task.FileName); success = true; break; } Debug.LogWarning($"视频下载失败(第 {attempt} 次): {task.FileName}, {www.error}"); if (attempt < task.MaxRetry) yield return new WaitForSeconds(1f); } } if (!success) { Debug.LogError($"视频下载失败,超过最大重试次数:{task.FileName}"); if (File.Exists(tmpPath)) File.Delete(tmpPath); } downloadingSet.Remove(task.FileName); task.Callback?.Invoke(success ? task.LocalPath : null); LiveVideoMemoryManager.RequestCleanup(); } #endregion #region 视频封面 // 你的封面逻辑保持不变 public void GetVideoCover(GLoader loader, string fileName, Action onComplete) { if (coverCache.TryGetValue(fileName, out var cached)) { onComplete?.Invoke(cached); return; } var coverPath = Path.Combine(coverLocalDir, fileName + ".png"); if (File.Exists(coverPath)) { var tex = LoadTextureFromFile(coverPath); coverCache[fileName] = tex; onComplete?.Invoke(tex); return; } StartCoroutine(LoadCover(fileName, onComplete)); coverQueue.Enqueue(new CoverTask { FileName = fileName, Callback = onComplete }); if (!isExtracting) ProcessCoverQueue(loader); } private void ProcessCoverQueue(GLoader loader) { isExtracting = true; while (coverQueue.Count > 0) { var task = coverQueue.Dequeue(); ProcessGetCoverCoroutine(loader, task.FileName, task.Callback); } isExtracting = false; } private void ProcessGetCoverCoroutine(GLoader loader, string fileName, Action callback) { TextureHelper.SetImgLoader(loader, fileName, (a) => { coverCache[fileName] = a.nativeTexture as Texture2D; callback?.Invoke(coverCache[fileName]); }, "LiveAlbums/", FolderNames.VideoCoversName); } private IEnumerator ExtractCoverFromVideo(string fileName, string videoPath, Action callback) { var go = new GameObject("LiveVideoCoverExtractor_" + fileName); var vp = go.AddComponent(); vp.audioOutputMode = VideoAudioOutputMode.None; vp.source = VideoSource.Url; vp.url = Application.platform == RuntimePlatform.Android && !Application.isEditor ? videoPath : "file://" + videoPath; vp.isLooping = false; vp.playOnAwake = false; var rt = new RenderTexture(460,690, 0); vp.targetTexture = rt; vp.Prepare(); var timeout = 5f; var timer = 0f; while (!vp.isPrepared && timer < timeout) { timer += Time.deltaTime; yield return null; } if (!vp.isPrepared) { Debug.LogWarning($"LiveVideoManager: Video '{fileName}' prepare timeout."); Destroy(go); callback?.Invoke(null); yield break; } vp.Pause(); yield return new WaitForEndOfFrame(); var tex = CaptureFrame(vp); SaveCover(fileName, tex); coverCache[fileName] = tex; callback?.Invoke(tex); Destroy(go); } private Texture2D CaptureFrame(VideoPlayer vp) { var rt = vp.targetTexture; RenderTexture.active = rt; var tex = new Texture2D(rt.width, rt.height, TextureFormat.RGB24, false); tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); tex.Apply(); RenderTexture.active = null; vp.Stop(); vp.targetTexture = null; return tex; } private void SaveCover(string fileName, Texture2D tex) { try { var pngData = tex.EncodeToPNG(); var path = Path.Combine(coverLocalDir, fileName + ".png"); File.WriteAllBytes(path, pngData); } catch (Exception e) { Debug.LogWarning($"Save cover PNG failed for '{fileName}': {e}"); } } private Texture2D LoadTextureFromFile(string filePath) { try { var data = File.ReadAllBytes(filePath); var tex = new Texture2D(2, 2); tex.LoadImage(data); return tex; } catch { return null; } } public bool ExistVideo(string fileName) { var path = Path.Combine(videoLocalDir, fileName + ".mp4"); return File.Exists(path); } #endregion } internal class VideoTask { public Action Callback; public string FileName; public string LocalPath; public int MaxRetry; } internal class CoverTask { public Action Callback; public string FileName; } internal static class LiveVideoMemoryManager { private const int CLEANUP_INTERVAL = 30; private const int CLEANUP_THRESHOLD = 10; private static readonly float lastCleanupTime = 0f; private static int downloadCount; public static void RequestCleanup() { downloadCount++; if (downloadCount >= CLEANUP_THRESHOLD || Time.realtimeSinceStartup - lastCleanupTime > CLEANUP_INTERVAL) LiveVideoManager.Instance.StartCoroutine(CleanupCoroutine()); } private static IEnumerator CleanupCoroutine() { yield return null; // Debug.Log("[LiveVideoMemoryManager] 清理内存..."); // yield return Resources.UnloadUnusedAssets(); // GC.Collect(); // // lastCleanupTime = Time.realtimeSinceStartup; // downloadCount = 0; } } }