| New file |
| | |
| | | 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> |
| | | /// 视频是否正在播放 |
| | | /// </summary> |
| | | public bool IsPlaying => videoPlayer != null && videoPlayer.isPlaying; |
| | | |
| | | /// <summary> |
| | | /// 视频是否正在加载 |
| | | /// </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 |
| | | } |