Files
ZooMatch-GP/Assets/ZooMatch/ToolKit/LiveVideoManager.cs
T

577 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 ZooMatch
{
public class LiveVideoManager : SingletonMonoBehaviour<LiveVideoManager>
{
public static string videoBaseUrl = "";
// 封面缓存
private readonly Dictionary<string, Texture2D> coverCache = new Dictionary<string, Texture2D>();
// 封面提取队列(避免同时开多个VideoPlayer
private readonly Queue<CoverTask> coverQueue = new();
// 已下载视频缓存
private readonly HashSet<string> downloadedVideos = new();
// 当前正在下载的视频,用于去重
private readonly HashSet<string> downloadingSet = new();
// ==== 队列 ====
// 普通视频队列和优先队列
private readonly Queue<VideoTask> normalQueue = new();
private readonly Queue<VideoTask> 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<Texture2D> 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<VideoPlayer> 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<string> 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<string> 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<Texture2D> 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<Texture2D> callback)
{
TextureHelper.SetImgLoader(loader, fileName,
(a) => {
if (a != null && a.nativeTexture != null)
{
coverCache[fileName] = a.nativeTexture as Texture2D;
callback?.Invoke(coverCache[fileName]);
}
else
{
Debug.LogWarning($"[LiveVideoManager] 封面加载失败: {fileName}");
callback?.Invoke(null);
}
}, "LiveAlbums/", FolderNames.VideoCoversName);
}
private IEnumerator ExtractCoverFromVideo(string fileName, string videoPath, Action<Texture2D> callback)
{
var go = new GameObject("LiveVideoCoverExtractor_" + fileName);
var vp = go.AddComponent<VideoPlayer>();
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<string> Callback;
public string FileName;
public string LocalPath;
public int MaxRetry;
}
internal class CoverTask
{
public Action<Texture2D> 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;
}
}
}