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
|
}
|