From 8d77144cfb756529507dc6927dcdcfd9f4a30750 Mon Sep 17 00:00:00 2001
From: yyl <yyl>
Date: 星期三, 25 二月 2026 14:48:37 +0800
Subject: [PATCH] 视频播放支持

---
 Main/System/Video.meta                  |    8 
 Main/System/Video/VideoWin.cs           |   20 +
 Main/Utility/Extension.cs               |   59 ++++
 Main/System/Video/VideoWin.cs.meta      |   11 
 Main/System/Hero/UIHeroController.cs    |   20 
 Main/System/Video/VideoManager.cs.meta  |   11 
 Main/System/Video/VideoManager.cs       |  117 +++++++++
 Main/ResModule/ResManager.cs            |   18 +
 Main/System/Video/UIVideoPlayer.cs      |  505 ++++++++++++++++++++++++++++++++++++++
 Main/System/Video/UIVideoPlayer.cs.meta |   11 
 10 files changed, 772 insertions(+), 8 deletions(-)

diff --git a/Main/ResModule/ResManager.cs b/Main/ResModule/ResManager.cs
index 48f61f6..b4c7112 100644
--- a/Main/ResModule/ResManager.cs
+++ b/Main/ResModule/ResManager.cs
@@ -6,6 +6,8 @@
 using UnityEngine.Video;
 using Spine.Unity;
 using UnityEngine.UI;
+using Cysharp.Threading.Tasks;
+
 
 
 
@@ -251,6 +253,22 @@
         LoadAssetAsyncInternal<T>(directory, name, callBack, needExt);
     }
 
+    public async UniTask<T> LoadAssetAsync<T>(string directory, string name, bool needExt = true) where T : UnityEngine.Object
+    {
+        var tcs = new UniTaskCompletionSource<T>();
+        LoadAssetAsync<T>(directory, name, (isLoaded, asset) => {
+            if (isLoaded)
+            {
+                tcs.TrySetResult(asset as T);
+            }
+            else
+            {
+                tcs.TrySetException(new Exception($"Failed to load asset: {directory}/{name}"));
+            }
+        }, needExt);
+        return await tcs.Task;
+    }
+
     private void LoadSpriteAsync<T>(string atlasName, string spriteName, Action<bool, UnityEngine.Object> callBack) where T : UnityEngine.Object
     {
 #if !UNITY_EDITOR
diff --git a/Main/System/Hero/UIHeroController.cs b/Main/System/Hero/UIHeroController.cs
index dd7ae75..7ae549e 100644
--- a/Main/System/Hero/UIHeroController.cs
+++ b/Main/System/Hero/UIHeroController.cs
@@ -1,5 +1,6 @@
 
 using System;
+using Spine;
 using Spine.Unity;
 using UnityEngine;
 using UnityEngine.UI;
@@ -10,7 +11,7 @@
 	private int skinID;
 	protected SkeletonGraphic skeletonGraphic;
 
-	protected Spine.AnimationState spineAnimationState;
+	public Spine.AnimationState spineAnimationState;
 	private GameObject instanceGO;
 
 	public Action onComplete;
@@ -26,7 +27,7 @@
 				if (isLh)
 				{
 					var skinConfigTmp = HeroSkinConfig.Get(skinID);
-					if (skinConfigTmp != null && skinConfigTmp.Tachie.Contains("SkeletonData"))
+					if (skinConfigTmp != null && skinConfigTmp.Tachie.IsSpine())
 					{
 						skeletonGraphic.enabled = true;
 					}
@@ -60,7 +61,7 @@
 
 			//绔嬬粯鐗规畩澶勭悊锛屾病鏈塻pine鍔ㄧ敾鐨勬敼鐢ㄥ浘鐗�
 			var lhImg = this.AddMissingComponent<RawImage>();
-			if (!skinConfig.Tachie.Contains("SkeletonData"))
+			if (!skinConfig.Tachie.IsSpine())
 			{
 				//鍥剧墖鏇挎崲
 				lhImg.SetTexture2DPNG(skinConfig.Tachie);
@@ -150,7 +151,10 @@
 		spineAnimationState.Complete += OnAnimationComplete;
 	}
 
-
+	public bool HasAnimation(string motionName)
+	{
+		return skeletonGraphic != null && skeletonGraphic.Skeleton != null && skeletonGraphic.Skeleton.ContainsMotion(motionName);
+	}
 
 
 	protected void OnDestroy()
@@ -171,15 +175,15 @@
 	/// <param name="motionName">鍔ㄤ綔鍚�</param>
 	/// <param name="loop">寰幆</param>
 	/// <param name="replay">濡傛灉鐩稿悓鍔ㄤ綔鏄惁鍐嶆閲嶆挱锛屾瘮濡傝窇姝ラ噸鎾氨浼氳烦甯т笉椤烘粦</param>
-	public virtual void PlayAnimation(string motionName, bool loop = false, bool replay = true)
+	public virtual TrackEntry PlayAnimation(string motionName, bool loop = false, bool replay = true)
 	{
-		if (spineAnimationState == null) return;
+		if (spineAnimationState == null) return null;
 
 		if (GetCurrentAnimationName() == motionName && !replay)
-			return;
+			return null;
 
 		// 鐩存帴浣跨敤 ToString() 鑰屼笉鏄皟鐢� GetAnimationName
-		spineAnimationState.SetAnimation(0, motionName.ToString(), loop);
+		return spineAnimationState.SetAnimation(0, motionName.ToString(), loop);
 	}
 
 	// 鎾斁绗竴涓姩鐢伙紙浣滀负榛樿鍔ㄧ敾锛�
diff --git a/Main/System/Video.meta b/Main/System/Video.meta
new file mode 100644
index 0000000..bf20a4e
--- /dev/null
+++ b/Main/System/Video.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9f8b60fd5cb8de143a61b1a3ea0fc728
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/Video/UIVideoPlayer.cs b/Main/System/Video/UIVideoPlayer.cs
new file mode 100644
index 0000000..1b005b2
--- /dev/null
+++ b/Main/System/Video/UIVideoPlayer.cs
@@ -0,0 +1,505 @@
+using System;
+using UnityEngine;
+using UnityEngine.Video;
+using UnityEngine.UI;
+
+/// <summary>
+/// UI瑙嗛鎾斁鍣ㄧ粍浠� - 鏀寔WebM鏍煎紡閫忔槑瑙嗛鎾斁
+/// </summary>
+public class UIVideoPlayer : MonoBehaviour
+{
+    [Header("缁勪欢寮曠敤")]
+    public VideoPlayer videoPlayer;
+    public RawImage videoImage;
+
+    [Header("璋冭瘯淇℃伅")]
+    public const string directory = "Video";
+    public string videoName;
+    public bool isLoop = false;
+    
+    [Header("璧勬簮绠$悊")]
+    [Tooltip("鎾斁瀹屾垚鍚庢槸鍚﹁嚜鍔ㄥ嵏杞借祫婧愶紙浠呭綋闈炲惊鐜挱鏀炬椂鏈夋晥锛�")]
+    public bool autoUnloadOnFinish = true;
+    
+    [Header("鍥炶皟浜嬩欢")]
+    public Action onComplete;
+    public Action onPrepared;
+
+    private RenderTexture _renderTexture;
+    private VideoClip _loadedClip;
+    private bool _isPrepared;
+    private bool _isLoading;
+    private Action<bool> _onPrepareCallback;
+
+    #region Public API
+
+    /// <summary>
+    /// 鍔犺浇骞舵挱鏀捐棰戯紙閫氳繃璧勬簮绯荤粺鍔犺浇 VideoClip锛�
+    /// </summary>
+    /// <param name="_videoName">瑙嗛鏂囦欢鍚嶏紙涓嶅惈鎵╁睍鍚嶏級</param>
+    /// <param name="_loop">鏄惁寰幆鎾斁</param>
+    /// <param name="_onComplete">鎾斁瀹屾垚鍥炶皟锛堜粎鍦ㄩ潪寰幆鎾斁鏃惰皟鐢級</param>
+    public async void LoadAndPlayVideo(string _videoName, bool _loop = false, Action _onComplete = null)
+    {
+        if (!ValidateComponents()) return;
+        if (_isLoading)
+        {
+            Debug.LogWarning($"UIVideoPlayer: Already loading video. Please wait.");
+            return;
+        }
+
+        videoName = _videoName;
+        isLoop = _loop;
+        onComplete = _onComplete;
+        _isPrepared = false;
+        _isLoading = true;
+
+        // 娓呯悊鏃ц祫婧�
+        ReleaseRenderTexture();
+        
+        // 闅愯棌鎴栬缃┖鐘舵�侀鑹�
+        SetEmptyState();
+
+        // 閫氳繃 ResManager 鍔犺浇 VideoClip 璧勬簮锛堟敮鎸� AB 鎵撳寘锛�
+        _loadedClip = await ResManager.Instance.LoadAssetAsync<VideoClip>(directory, videoName, false);
+        
+        // 妫�鏌ユ槸鍚﹀湪鍔犺浇杩囩▼涓鍙栨秷
+        if (!_isLoading)
+        {
+            _loadedClip = null;
+            return;
+        }
+        
+        _isLoading = false;
+        
+        if (_loadedClip == null)
+        {
+            Debug.LogError($"UIVideoPlayer: Failed to load VideoClip: {directory}/{videoName}");
+            _isPrepared = false;
+            return;
+        }
+
+        // 鍒涘缓骞剁粦瀹� RenderTexture锛堟寜瑙嗛瀹為檯灏哄锛�
+        _renderTexture = new RenderTexture((int)_loadedClip.width, (int)_loadedClip.height, 0);
+        videoPlayer.targetTexture = _renderTexture;
+        videoImage.texture = _renderTexture;
+        videoImage.SetNativeSize();
+
+        
+        // 鏄剧ず瑙嗛
+        SetActiveState();
+
+        videoPlayer.source = VideoSource.VideoClip;
+        videoPlayer.clip = _loadedClip;
+        videoPlayer.isLooping = isLoop;
+        videoPlayer.prepareCompleted += OnVideoPrepared;
+        
+        // 闈炲惊鐜挱鏀炬椂娉ㄥ唽瀹屾垚浜嬩欢
+        if (!isLoop)
+        {
+            videoPlayer.loopPointReached += OnVideoFinished;
+        }
+        
+        videoPlayer.Prepare();
+    }
+
+    /// <summary>
+    /// 鍔犺浇骞舵挱鏀炬寚瀹氳棰�
+    /// </summary>
+    public void LoadAndPlay(string videoName, bool loop = false, Action onCompleteCallback = null)
+    {
+        LoadAndPlayVideo(videoName, loop, onCompleteCallback);
+    }
+
+    [ContextMenu("鎾斁")]
+    public void Play()
+    {
+        if (!ValidateComponents()) return;
+        if (_isPrepared && !videoPlayer.isPlaying)
+        {
+            videoPlayer.Play();
+        }
+    }
+    
+    [ContextMenu("鏆傚仠")]
+    public void Pause()
+    {
+        if (!ValidateComponents()) return;
+        if (videoPlayer.isPlaying)
+        {
+            videoPlayer.Pause();
+        }
+    }
+    
+    [ContextMenu("鎭㈠")]
+    public void Resume()
+    {
+        if (!ValidateComponents()) return;
+        if (_isPrepared && !videoPlayer.isPlaying)
+        {
+            videoPlayer.Play();
+        }
+    }
+
+    /// <summary>
+    /// 棰勫姞杞借棰戯紙涓嶈嚜鍔ㄦ挱鏀撅級
+    /// </summary>
+    /// <param name="_videoName">瑙嗛鏂囦欢鍚嶏紙涓嶅惈鎵╁睍鍚嶏級</param>
+    /// <param name="onPreparedOK">棰勫姞杞藉畬鎴愬洖璋冿紝鍙傛暟涓烘槸鍚︽垚鍔�</param>
+    public async void Prepare(string _videoName, Action<bool> onPreparedOK)
+    {
+        if (!ValidateComponents())
+        {
+            onPreparedOK?.Invoke(false);
+            return;
+        }
+        
+        if (_isLoading)
+        {
+            Debug.LogWarning($"UIVideoPlayer: Already loading video. Please wait.");
+            onPreparedOK?.Invoke(false);
+            return;
+        }
+
+        videoName = _videoName;
+        _onPrepareCallback = onPreparedOK;
+        _isPrepared = false;
+        _isLoading = true;
+
+        // 娓呯悊鏃ц祫婧�
+        ReleaseRenderTexture();
+        SetEmptyState();
+
+        // 閫氳繃 ResManager 鍔犺浇 VideoClip 璧勬簮锛堟敮鎸� AB 鎵撳寘锛�
+        _loadedClip = await ResManager.Instance.LoadAssetAsync<VideoClip>(directory, videoName, false);
+        
+        // 妫�鏌ユ槸鍚﹀湪鍔犺浇杩囩▼涓鍙栨秷
+        if (!_isLoading)
+        {
+            _loadedClip = null;
+            return;
+        }
+        
+        _isLoading = false;
+        
+        if (_loadedClip == null)
+        {
+            Debug.LogError($"UIVideoPlayer: Failed to load VideoClip: {directory}/{videoName}");
+            _isPrepared = false;
+            _onPrepareCallback?.Invoke(false);
+            _onPrepareCallback = null;
+            return;
+        }
+
+        // 鍒涘缓骞剁粦瀹� RenderTexture锛堟寜瑙嗛瀹為檯灏哄锛�
+        _renderTexture = new RenderTexture((int)_loadedClip.width, (int)_loadedClip.height, 0);
+        videoPlayer.targetTexture = _renderTexture;
+        videoImage.texture = _renderTexture;
+        videoImage.SetNativeSize();
+        
+        SetActiveState();
+
+        videoPlayer.source = VideoSource.VideoClip;
+        videoPlayer.clip = _loadedClip;
+        videoPlayer.prepareCompleted += OnVideoPreparedForPreload;
+        
+        videoPlayer.Prepare();
+    }
+    
+    /// <summary>
+    /// 閫氳繃 URL 鍔犺浇骞舵挱鏀捐棰�
+    /// </summary>
+    /// <param name="url">瑙嗛 URL 鍦板潃</param>
+    /// <param name="_loop">鏄惁寰幆鎾斁</param>
+    /// <param name="_onComplete">鎾斁瀹屾垚鍥炶皟锛堜粎鍦ㄩ潪寰幆鎾斁鏃惰皟鐢級</param>
+    public void LoadAndPlayFromURL(string url, bool _loop = false, Action _onComplete = null)
+    {
+        if (!ValidateComponents()) return;
+        if (_isLoading)
+        {
+            Debug.LogWarning($"UIVideoPlayer: Already loading video. Please wait.");
+            return;
+        }
+
+        videoName = url;
+        isLoop = _loop;
+        onComplete = _onComplete;
+        _isPrepared = false;
+        _isLoading = true;
+
+        // 娓呯悊鏃ц祫婧�
+        ReleaseRenderTexture();
+        SetEmptyState();
+
+        // 鏍规嵁 URL 瑙嗛鍒涘缓榛樿灏哄鐨� RenderTexture锛堝噯澶囧畬鎴愬悗浼氳皟鏁达級
+        _renderTexture = new RenderTexture(1920, 1080, 0);
+        videoPlayer.targetTexture = _renderTexture;
+        videoImage.texture = _renderTexture;
+        
+        SetActiveState();
+
+        videoPlayer.source = VideoSource.Url;
+        videoPlayer.url = url;
+        videoPlayer.isLooping = isLoop;
+        videoPlayer.prepareCompleted += OnVideoPreparedForURL;
+        
+        // 闈炲惊鐜挱鏀炬椂娉ㄥ唽瀹屾垚浜嬩欢
+        if (!isLoop)
+        {
+            videoPlayer.loopPointReached += OnVideoFinished;
+        }
+        
+        videoPlayer.Prepare();
+    }
+    
+    [ContextMenu("鍋滄")]
+    public void Stop()
+    {
+        if (!ValidateComponents()) return;
+        videoPlayer.Stop();
+    }
+    
+    [ContextMenu("閲嶆柊鎾斁")]
+    public void Restart()
+    {
+        if (!ValidateComponents()) return;
+        if (_isPrepared)
+        {
+            videoPlayer.Stop();
+            videoPlayer.Play();
+        }
+    }
+
+    /// <summary>
+    /// 鍗歌浇瑙嗛璧勬簮
+    /// </summary>
+    public void Unload()
+    {
+        if (videoPlayer == null) return;
+
+        // 鍙栨秷鎵�鏈変簨浠惰闃�
+        videoPlayer.prepareCompleted -= OnVideoPrepared;
+        videoPlayer.prepareCompleted -= OnVideoPreparedForPreload;
+        videoPlayer.prepareCompleted -= OnVideoPreparedForURL;
+        videoPlayer.loopPointReached -= OnVideoFinished;
+        
+        videoPlayer.Stop();
+        videoPlayer.clip = null;
+        videoPlayer.url = null;
+        videoPlayer.targetTexture = null;
+        
+        if (videoImage != null)
+        {
+            videoImage.texture = null;
+        }
+
+        _loadedClip = null;
+        ReleaseRenderTexture();
+        
+        // 鎭㈠绌虹姸鎬�
+        SetEmptyState();
+
+        _isPrepared = false;
+        _isLoading = false;
+        onComplete = null;
+        onPrepared = null;
+    }
+
+    #endregion
+
+    #region Properties
+
+    /// <summary>
+    /// 瑙嗛鏄惁宸插噯澶囧ソ
+    /// </summary>
+    public bool IsPrepared => _isPrepared;
+
+    /// <summary>
+    /// 瑙嗛鏄惁姝e湪鎾斁
+    /// </summary>
+    public bool IsPlaying => videoPlayer != null && videoPlayer.isPlaying;
+
+    /// <summary>
+    /// 瑙嗛鏄惁姝e湪鍔犺浇
+    /// </summary>
+    public bool IsLoading => _isLoading;
+
+    /// <summary>
+    /// 褰撳墠鎾斁鏃堕棿锛堢锛�
+    /// </summary>
+    public double CurrentTime => videoPlayer != null ? videoPlayer.time : 0;
+
+    /// <summary>
+    /// 瑙嗛鎬绘椂闀匡紙绉掞級
+    /// </summary>
+    public double Duration => _loadedClip != null ? _loadedClip.length : 0;
+
+    #endregion
+
+    #region Private Methods
+
+    [ContextMenu("閲嶆柊鍔犺浇")]
+    private void Reload()
+    {
+        LoadAndPlayVideo(videoName, isLoop);
+    }
+
+    private void OnVideoPrepared(VideoPlayer vp)
+    {
+        vp.prepareCompleted -= OnVideoPrepared;
+        _isPrepared = true;
+        _isLoading = false;
+        onPrepared?.Invoke();
+        vp.Play();
+    }
+    
+    private void OnVideoPreparedForPreload(VideoPlayer vp)
+    {
+        vp.prepareCompleted -= OnVideoPreparedForPreload;
+        _isPrepared = true;
+        _isLoading = false;
+        _onPrepareCallback?.Invoke(true);
+        _onPrepareCallback = null;
+        // 棰勫姞杞藉畬鎴愬悗涓嶈嚜鍔ㄦ挱鏀�
+    }
+    
+    private void OnVideoPreparedForURL(VideoPlayer vp)
+    {
+        vp.prepareCompleted -= OnVideoPreparedForURL;
+        _isPrepared = true;
+        _isLoading = false;
+        
+        // 璋冩暣 RenderTexture 灏哄涓哄疄闄呰棰戝昂瀵�
+        if (_renderTexture != null && vp.texture != null)
+        {
+            var newRT = new RenderTexture((int)vp.width, (int)vp.height, 0);
+            ReleaseRenderTexture();
+            _renderTexture = newRT;
+            vp.targetTexture = _renderTexture;
+            videoImage.texture = _renderTexture;
+            videoImage.SetNativeSize();
+        }
+        
+        onPrepared?.Invoke();
+        vp.Play();
+    }
+
+    private void OnVideoFinished(VideoPlayer vp)
+    {
+        onComplete?.Invoke();
+        
+        // 鏍规嵁璁剧疆鑷姩娓呯悊璧勬簮
+        if (autoUnloadOnFinish)
+        {
+            Unload();
+        }
+    }
+
+    private void ReleaseRenderTexture()
+    {
+        if (_renderTexture != null)
+        {
+            _renderTexture.Release();
+            Destroy(_renderTexture);
+            _renderTexture = null;
+        }
+    }
+    
+    /// <summary>
+    /// 璁剧疆绌虹姸鎬侊紙鏈姞杞借棰戞椂锛�
+    /// </summary>
+    private void SetEmptyState()
+    {
+        if (videoImage != null)
+        {
+            videoImage.enabled = false;
+        }
+    }
+    
+    /// <summary>
+    /// 璁剧疆婵�娲荤姸鎬侊紙瑙嗛宸插姞杞斤級
+    /// </summary>
+    private void SetActiveState()
+    {
+        if (videoImage != null)
+        {
+            videoImage.enabled = true;
+        }
+    }
+    
+    /// <summary>
+    /// 楠岃瘉蹇呴渶缁勪欢
+    /// </summary>
+    private bool ValidateComponents()
+    {
+        if (videoPlayer == null)
+        {
+            Debug.LogError("UIVideoPlayer: VideoPlayer component is not assigned.");
+            return false;
+        }
+        if (videoImage == null)
+        {
+            Debug.LogError("UIVideoPlayer: RawImage component is not assigned.");
+            return false;
+        }
+        return true;
+    }
+
+    #endregion
+
+    #region Unity Lifecycle
+
+    private void Awake()
+    {
+        // 鍒濆鍖栨椂璁剧疆涓虹┖鐘舵��
+        SetEmptyState();
+    }
+
+    private void OnDestroy()
+    {
+        Unload();
+    }
+
+    /// <summary>
+    /// 鍙栨秷棰勫姞杞�
+    /// </summary>
+    public void CancelPrepare()
+    {
+        if (videoPlayer == null) return;
+        
+        // 鍙栨秷鎵�鏈夊噯澶囩浉鍏崇殑浜嬩欢璁㈤槄
+        videoPlayer.prepareCompleted -= OnVideoPrepared;
+        videoPlayer.prepareCompleted -= OnVideoPreparedForPreload;
+        videoPlayer.prepareCompleted -= OnVideoPreparedForURL;
+        
+        // 鍋滄 VideoPlayer锛堝鏋滄鍦ㄥ姞杞斤級
+        if (_isLoading)
+        {
+            videoPlayer.Stop();
+        }
+        
+        // 娓呯悊璧勬簮
+        videoPlayer.clip = null;
+        videoPlayer.url = null;
+        videoPlayer.targetTexture = null;
+        
+        if (videoImage != null)
+        {
+            videoImage.texture = null;
+        }
+        
+        _loadedClip = null;
+        ReleaseRenderTexture();
+        SetEmptyState();
+        
+        // 閲嶇疆鐘舵��
+        _isPrepared = false;
+        _isLoading = false;
+        
+        // 瑙﹀彂澶辫触鍥炶皟
+        _onPrepareCallback?.Invoke(false);
+        _onPrepareCallback = null;
+    }
+
+    #endregion
+}
diff --git a/Main/System/Video/UIVideoPlayer.cs.meta b/Main/System/Video/UIVideoPlayer.cs.meta
new file mode 100644
index 0000000..a6bce43
--- /dev/null
+++ b/Main/System/Video/UIVideoPlayer.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 780579551ee7fe442bc047278c4b96d6
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/Video/VideoManager.cs b/Main/System/Video/VideoManager.cs
new file mode 100644
index 0000000..18f7ac3
--- /dev/null
+++ b/Main/System/Video/VideoManager.cs
@@ -0,0 +1,117 @@
+
+
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+public class VideoManager : Singleton<VideoManager>
+{
+    private GameObjectPoolManager.GameObjectPool uiVideoPool;
+
+    private Dictionary<string, UIVideoPlayer> prepareDict = new Dictionary<string, UIVideoPlayer>();
+
+    private Dictionary<string, Action<bool, UIVideoPlayer>> prepareCallbacks = new Dictionary<string, Action<bool, UIVideoPlayer>>();
+
+    public void Init()
+    {
+        GameObject uiVideoPrefab = UILoader.LoadPrefab("UIVideoPlayer");
+        uiVideoPool = GameObjectPoolManager.Instance.GetPool(uiVideoPrefab);
+    }
+
+    public void Release()
+    {
+        if (uiVideoPool != null)
+        {
+            uiVideoPool.Clear();
+            uiVideoPool = null;
+        }
+    }
+
+    private UIVideoPlayer GetUIVideoPlayer()
+    {
+        GameObject videoObj = uiVideoPool.Request();
+        if (videoObj != null)
+        {
+            return videoObj.GetComponent<UIVideoPlayer>();
+        }
+        return null;
+    }
+
+    public UIVideoPlayer PrepareVideo(string videoName, Action<bool, UIVideoPlayer> onPrepared)
+    {
+        //  濡傛灉宸茬粡鍦ㄥ噯澶囦腑锛岀洿鎺ヨ繑鍥� 骞剁粰鍑烘彁绀� 宸茬粡鍦ㄥ姞杞藉噯澶囦腑
+        if (prepareDict.TryGetValue(videoName, out UIVideoPlayer existingPlayer))
+        {
+            Debug.LogError("Video " + videoName + " is already being prepared.");
+            return existingPlayer;
+        }
+
+
+        UIVideoPlayer uiVideoPlayer = GetUIVideoPlayer();
+        if (uiVideoPlayer != null)
+        {
+            string _videoName = videoName; // 閬垮厤闂寘闂
+            prepareDict.Add(_videoName, uiVideoPlayer);
+            prepareCallbacks.Add(_videoName, onPrepared);
+            uiVideoPlayer.Prepare(videoName, (success) =>
+            {
+                OnPrepareOK(videoName, success, uiVideoPlayer);
+            });
+        }
+        else
+        {
+            Debug.LogError("Failed to get UIVideoPlayer from pool.");
+            onPrepared?.Invoke(false, null);
+        }
+        return uiVideoPlayer;
+    }
+
+
+    public void CancelPrepare(UIVideoPlayer uIVideoPlayer)
+    {
+        if (uIVideoPlayer != null)
+            uIVideoPlayer.CancelPrepare();
+        {
+            uiVideoPool.Release(uIVideoPlayer.gameObject);
+            // 浠� prepareDict 绉婚櫎
+            string keyToRemove = null;
+            foreach (var kv in prepareDict)
+            {
+                if (kv.Value == uIVideoPlayer)
+                {
+                    keyToRemove = kv.Key;
+                    break;
+                }
+            }
+            if (keyToRemove != null)
+            {
+                prepareDict.Remove(keyToRemove);
+                prepareCallbacks.Remove(keyToRemove);
+            }
+        }
+    }
+
+    private void OnPrepareOK(string videoName, bool success, UIVideoPlayer uiVideoPlayer)
+    {
+        // 瀹夊叏绉婚櫎鍜屽洖璋�
+        if (prepareDict.ContainsKey(videoName))
+        {
+            prepareDict.Remove(videoName);
+        }
+        Action<bool, UIVideoPlayer> callback = null;
+        if (prepareCallbacks.TryGetValue(videoName, out callback))
+        {
+            callback?.Invoke(success, uiVideoPlayer);
+            prepareCallbacks.Remove(videoName);
+        }
+    }
+
+    public void ReleaseVideoPlayer(UIVideoPlayer uIVideoPlayer)
+    {
+        if (uIVideoPlayer != null)
+        {
+            CancelPrepare(uIVideoPlayer);
+            uiVideoPool.Release(uIVideoPlayer.gameObject);
+        }
+    }
+}
\ No newline at end of file
diff --git a/Main/System/Video/VideoManager.cs.meta b/Main/System/Video/VideoManager.cs.meta
new file mode 100644
index 0000000..bee492c
--- /dev/null
+++ b/Main/System/Video/VideoManager.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 14d20be0871f2e24d96b79440816d7cb
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/Video/VideoWin.cs b/Main/System/Video/VideoWin.cs
new file mode 100644
index 0000000..290345a
--- /dev/null
+++ b/Main/System/Video/VideoWin.cs
@@ -0,0 +1,20 @@
+using System;
+using UnityEngine;
+using UnityEngine.Video;
+using UnityEngine.UI;
+
+public class VideoWin : UIBase
+{
+    public UIVideoPlayer videoPlayer;
+    protected override void OnOpen()
+    {
+        // 瑙嗛鍑嗗瀹屾垚鍚庝細鑷姩鎾斁
+    }
+
+    protected override void OnPreClose()
+    {
+        base.OnPreClose();
+        videoPlayer.Unload();
+    }
+
+}
diff --git a/Main/System/Video/VideoWin.cs.meta b/Main/System/Video/VideoWin.cs.meta
new file mode 100644
index 0000000..3494092
--- /dev/null
+++ b/Main/System/Video/VideoWin.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: cf2a5c5ccb4f22f4c9408bd3b3efed69
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/Utility/Extension.cs b/Main/Utility/Extension.cs
index b5d01c7..9725ea7 100644
--- a/Main/Utility/Extension.cs
+++ b/Main/Utility/Extension.cs
@@ -1,5 +1,7 @@
 using System;
 using System.Collections.Generic;
+using Spine;
+using Spine.Unity;
 
 public static class Extension
 {
@@ -74,4 +76,61 @@
             }
         }
     }
+
+    public static bool IsSpine(this string _resName)
+    {
+        if (string.IsNullOrEmpty(_resName))
+        {
+            return false;
+        }
+
+        return _resName.Contains("SkeletonData");
+    }
+
+    public static bool IsVideo(this string _resName)
+    {
+        if (string.IsNullOrEmpty(_resName))
+        {
+            return false;
+        }
+
+        return _resName.EndsWith(".mp4");
+    }
+
+    public static bool ContainsMotion(this Spine.Skeleton skeleton, string motionName)
+    {
+        if (skeleton == null || string.IsNullOrEmpty(motionName))
+        {
+            return false;
+        }
+
+        for (int i = 0; i < skeleton.Data.Animations.Count; i++)
+        {
+            if (skeleton.Data.Animations.Items[i].Name.ToLower() == motionName.ToLower())
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public static Spine.Animation GetSpineAnimation(this Spine.Skeleton skeleton, string motionName)
+    {
+        if (skeleton == null || string.IsNullOrEmpty(motionName))
+        {
+            return null;
+        }
+
+        for (int i = 0; i < skeleton.Data.Animations.Count; i++)
+        {
+            if (skeleton.Data.Animations.Items[i].Name.ToLower() == motionName.ToLower())
+            {
+                return skeleton.Data.Animations.Items[i];
+            }
+        }
+
+        return null;
+    }
+
 }
\ No newline at end of file

--
Gitblit v1.8.0