Unity音视频播放监听器封装笔记:VideoPlayer + AudioSource事件触发与编辑器扩展
【Unity笔记】Unity音视频播放监听器封装笔记:VideoPlayer + AudioSource事件触发与编辑器扩展
引言
Unity 的 VideoPlayer 和 AudioSource 组件提供了强大的音视频播放能力,但原生 API 事件系统较为有限。本封装通过事件监听器、播放状态机和进度回调实现完整的音视频控制,支持准备完成、播放开始/结束、循环检测、错误处理等事件。结合自定义 Inspector 和编辑器扩展,提供可视化配置和预览功能,适用于游戏过场动画、MV 播放器、直播流集成等场景。
核心技术包括 VideoPlayer 事件委托、AudioSource 同步、协程进度监控和序列化优化。支持 Unity 2021.3+(VideoPlayer 增强版),兼容 URP/HDRP,时间复杂度 O(1) 事件触发,适用于移动端和 PC 平台。
核心事件系统设计
1. 播放状态枚举与事件定义
using UnityEngine;
using UnityEngine.Video;
using UnityEngine.Events;
using System;
public enum VideoPlayState
{
Idle, // 空闲
Preparing, // 准备中
Ready, // 准备完成
Playing, // 播放中
Paused, // 暂停
Completed, // 播放完成
Error, // 错误状态
Looping // 循环播放
}
[System.Serializable]
public class VideoEvent : UnityEvent { }
[System.Serializable]
public class VideoProgressEvent : UnityEvent<float> { } // 进度 0-1
[System.Serializable]
public class VideoTimeEvent : UnityEvent<double> { } // 当前时间(秒)
[System.Serializable]
public class VideoErrorEvent : UnityEvent<string> { } // 错误信息
public class VideoPlayerListener : MonoBehaviour
{
[Header("VideoPlayer 组件")]
public VideoPlayer videoPlayer;
public AudioSource audioSource;
[Header("播放配置")]
public bool autoPlay = false;
public bool loop = false;
public float updateInterval = 0.1f; // 进度更新间隔
[Header("事件绑定")]
public VideoEvent onPrepareStart = new VideoEvent();
public VideoEvent onPrepareComplete = new VideoEvent();
public VideoEvent onPlayStart = new VideoEvent();
public VideoEvent onPlayPause = new VideoEvent();
public VideoEvent onPlayResume = new VideoEvent();
public VideoEvent onPlayComplete = new VideoEvent();
public VideoEvent onLoopStart = new VideoEvent();
public VideoProgressEvent onProgressUpdate = new VideoProgressEvent();
public VideoTimeEvent onTimeUpdate = new VideoTimeEvent();
public VideoErrorEvent onError = new VideoErrorEvent();
private VideoPlayState currentState = VideoPlayState.Idle;
private Coroutine progressCoroutine;
private bool isPrepared = false;
public VideoPlayState CurrentState => currentState;
public bool IsPrepared => isPrepared;
public double Duration => videoPlayer.length;
public double CurrentTime => videoPlayer.time;
}
VideoPlayer 完整事件监听实现
2. 核心监听器逻辑
public class VideoPlayerListener : MonoBehaviour
{
void Start()
{
InitializeVideoPlayer();
}
private void InitializeVideoPlayer()
{
if (videoPlayer == null)
videoPlayer = GetComponent<VideoPlayer>();
if (audioSource == null)
audioSource = GetComponent<AudioSource>();
// 绑定 VideoPlayer 原生事件
videoPlayer.prepareCompleted += OnPrepareCompleted;
videoPlayer.loopPointReached += OnLoopPointReached;
videoPlayer.errorReceived += OnErrorReceived;
// 同步 AudioSource(如果使用)
if (audioSource != null)
{
videoPlayer.audioOutputMode = VideoAudioOutputMode.AudioSource;
videoPlayer.SetDirectAudioVolume(0, 1f);
videoPlayer.controlledAudioTrackCount = 1;
videoPlayer.EnableAudioTrack(0, true);
}
if (autoPlay)
{
PrepareAndPlay();
}
}
/// <summary>
/// 准备播放(异步)
/// </summary>
public void PrepareAndPlay()
{
if (videoPlayer == null) return;
SetState(VideoPlayState.Preparing);
onPrepareStart?.Invoke();
videoPlayer.Prepare();
}
private void OnPrepareCompleted(VideoPlayer source)
{
isPrepared = true;
SetState(VideoPlayState.Ready);
onPrepareComplete?.Invoke();
if (autoPlay)
{
Play();
}
}
public void Play()
{
if (!isPrepared)
{
PrepareAndPlay();
return;
}
videoPlayer.Play();
SetState(VideoPlayState.Playing);
onPlayStart?.Invoke();
// 启动进度监控
if (progressCoroutine != null)
StopCoroutine(progressCoroutine);
progressCoroutine = StartCoroutine(ProgressMonitor());
}
public void Pause()
{
if (videoPlayer.isPlaying)
{
videoPlayer.Pause();
SetState(VideoPlayState.Paused);
onPlayPause?.Invoke();
}
}
public void Resume()
{
videoPlayer.Play();
SetState(VideoPlayState.Playing);
onPlayResume?.Invoke();
if (progressCoroutine == null)
progressCoroutine = StartCoroutine(ProgressMonitor());
}
public void Stop()
{
videoPlayer.Stop();
SetState(VideoPlayState.Idle);
if (progressCoroutine != null)
{
StopCoroutine(progressCoroutine);
progressCoroutine = null;
}
}
private void OnLoopPointReached(VideoPlayer source)
{
if (loop)
{
SetState(VideoPlayState.Looping);
onLoopStart?.Invoke();
onPlayComplete?.Invoke(); // 循环也触发完成事件
}
else
{
SetState(VideoPlayState.Completed);
onPlayComplete?.Invoke();
Stop(); // 非循环自动停止
}
}
private void OnErrorReceived(VideoPlayer source, string message)
{
SetState(VideoPlayState.Error);
onError?.Invoke(message);
Debug.LogError($"VideoPlayer Error: {message}");
}
private IEnumerator ProgressMonitor()
{
while (videoPlayer.isPlaying && currentState == VideoPlayState.Playing)
{
float progress = (float)(videoPlayer.time / videoPlayer.length);
double currentTime = videoPlayer.time;
onProgressUpdate?.Invoke(progress);
onTimeUpdate?.Invoke(currentTime);
yield return new WaitForSeconds(updateInterval);
}
}
private void SetState(VideoPlayState state)
{
currentState = state;
Debug.Log($"Video state changed to: {state}");
}
// 音量控制
public void SetVolume(float volume)
{
if (audioSource != null)
audioSource.volume = volume;
else
videoPlayer.SetDirectAudioVolume(0, volume);
}
void OnDestroy()
{
if (videoPlayer != null)
{
videoPlayer.prepareCompleted -= OnPrepareCompleted;
videoPlayer.loopPointReached -= OnLoopPointReached;
videoPlayer.errorReceived -= OnErrorReceived;
}
}
}
AudioSource 同步与高级控制
3. 音频同步与多轨支持
public class AdvancedVideoAudio : VideoPlayerListener
{
[Header("音频设置")]
public AudioClip[] additionalAudioTracks;
public bool syncAudioWithVideo = true;
private AudioSource[] audioSources;
protected override void InitializeVideoPlayer()
{
base.InitializeVideoPlayer();
// 多音频轨支持
if (additionalAudioTracks != null && additionalAudioTracks.Length > 0)
{
SetupMultiAudioTracks();
}
// 音频同步
if (syncAudioWithVideo)
{
StartCoroutine(SyncAudioWithVideo());
}
}
private void SetupMultiAudioTracks()
{
audioSources = new AudioSource[additionalAudioTracks.Length];
for (int i = 0; i < additionalAudioTracks.Length; i++)
{
GameObject audioObj = new GameObject($"AudioTrack_{i}");
audioObj.transform.SetParent(transform);
audioSources[i] = audioObj.AddComponent<AudioSource>();
audioSources[i].clip = additionalAudioTracks[i];
audioSources[i].playOnAwake = false;
}
}
private IEnumerator SyncAudioWithVideo()
{
while (true)
{
if (videoPlayer.isPlaying && audioSources != null)
{
double videoTime = videoPlayer.time;
foreach (var source in audioSources)
{
if (source.isPlaying)
{
// 同步音频时间(简化实现)
source.time = (float)(videoTime % source.clip.length);
}
}
}
yield return new WaitForSeconds(0.016f); // ~60FPS 同步
}
}
public void SwitchAudioTrack(int trackIndex)
{
if (audioSources != null && trackIndex < audioSources.Length)
{
// 静音所有音频源
foreach (var source in audioSources)
source.mute = true;
// 激活指定轨道
audioSources[trackIndex].mute = false;
if (!audioSources[trackIndex].isPlaying)
audioSources[trackIndex].Play();
}
}
}
编辑器扩展与可视化配置
4. 自定义 Inspector
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using UnityEngine.Video;
[CustomEditor(typeof(VideoPlayerListener))]
public class VideoPlayerListenerEditor : Editor
{
private SerializedProperty videoPlayerProp;
private SerializedProperty audioSourceProp;
private SerializedProperty autoPlayProp;
private SerializedProperty loopProp;
private void OnEnable()
{
videoPlayerProp = serializedObject.FindProperty("videoPlayer");
audioSourceProp = serializedObject.FindProperty("audioSource");
autoPlayProp = serializedObject.FindProperty("autoPlay");
loopProp = serializedObject.FindProperty("loop");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(videoPlayerProp);
EditorGUILayout.PropertyField(audioSourceProp);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(autoPlayProp);
EditorGUILayout.PropertyField(loopProp);
// 事件绑定区域
EditorGUILayout.Space();
EditorGUILayout.LabelField("事件监听", EditorStyles.boldLabel);
DrawEventFields();
// 控制按钮
EditorGUILayout.Space();
EditorGUILayout.LabelField("播放控制", EditorStyles.boldLabel);
VideoPlayerListener listener = target as VideoPlayerListener;
if (listener != null && listener.videoPlayer != null)
{
if (GUILayout.Button("准备播放"))
{
listener.PrepareAndPlay();
}
if (GUILayout.Button("播放"))
{
listener.Play();
}
if (GUILayout.Button("暂停"))
{
listener.Pause();
}
if (GUILayout.Button("停止"))
{
listener.Stop();
}
// 进度显示
EditorGUILayout.Space();
EditorGUILayout.LabelField($"状态: {listener.CurrentState}");
if (listener.IsPrepared)
{
EditorGUILayout.LabelField($"时长: {listener.Duration:F2}s");
EditorGUILayout.LabelField($"当前时间: {listener.CurrentTime:F2}s");
}
}
serializedObject.ApplyModifiedProperties();
}
private void DrawEventFields()
{
EditorGUILayout.PropertyField(serializedObject.FindProperty("onPrepareStart"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("onPrepareComplete"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("onPlayStart"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("onPlayComplete"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("onError"));
}
}
#endif
5. 视频资源预设创建器
#if UNITY_EDITOR
[CreateAssetMenu(fileName = "VideoPreset", menuName = "Video/Video Play Preset", order = 1)]
public class VideoPlayPreset : ScriptableObject
{
[Header("视频资源")]
public VideoClip videoClip;
public string url; // 远程 URL
[Header("播放设置")]
public bool autoPlay = true;
public bool loop = false;
public float volume = 1f;
[Header("目标组件")]
public VideoPlayerListener targetListener;
[ContextMenu("应用预设")]
public void ApplyPreset()
{
if (targetListener == null)
{
Debug.LogError("请指定目标 VideoPlayerListener");
return;
}
VideoPlayer vp = targetListener.videoPlayer;
if (vp == null) return;
// 设置视频源
if (!string.IsNullOrEmpty(url))
{
vp.url = url;
}
else if (videoClip != null)
{
vp.clip = videoClip;
}
// 应用设置
vp.isLooping = loop;
vp.playOnAwake = autoPlay;
targetListener.SetVolume(volume);
targetListener.loop = loop;
targetListener.autoPlay = autoPlay;
// 自动准备播放
if (autoPlay)
{
targetListener.PrepareAndPlay();
}
EditorUtility.SetDirty(targetListener);
AssetDatabase.SaveAssets();
Debug.Log("Video preset applied successfully");
}
}
// 编辑器菜单
public static class VideoEditorTools
{
[MenuItem("Tools/Video/创建播放器")]
static void CreateVideoPlayer()
{
GameObject go = new GameObject("VideoPlayer");
VideoPlayer vp = go.AddComponent<VideoPlayer>();
AudioSource asrc = go.AddComponent<AudioSource>();
VideoPlayerListener listener = go.AddComponent<VideoPlayerListener>();
listener.videoPlayer = vp;
listener.audioSource = asrc;
Selection.activeGameObject = go;
}
}
#endif
高级功能:播放列表与字幕支持
6. 播放列表管理
[System.Serializable]
public class VideoPlaylistItem
{
public VideoClip clip;
public string url;
public float startTime = 0f;
public bool skip = false;
}
public class VideoPlaylistPlayer : VideoPlayerListener
{
[Header("播放列表")]
public VideoPlaylistItem[] playlist;
public bool shuffle = false;
public bool repeatPlaylist = false;
private int currentIndex = 0;
public UnityEvent onPlaylistItemChanged;
public UnityEvent<int> onPlaylistComplete;
public override void Play()
{
if (playlist.Length == 0)
{
base.Play();
return;
}
LoadPlaylistItem(currentIndex);
base.Play();
}
private void LoadPlaylistItem(int index)
{
if (index >= playlist.Length) return;
var item = playlist[index];
if (item.skip)
{
NextItem();
return;
}
videoPlayer.time = item.startTime;
if (!string.IsNullOrEmpty(item.url))
videoPlayer.url = item.url;
else if (item.clip != null)
videoPlayer.clip = item.clip;
currentIndex = index;
onPlaylistItemChanged?.Invoke();
}
public void NextItem()
{
currentIndex = (currentIndex + 1) % playlist.Length;
if (currentIndex == 0 && !repeatPlaylist)
{
onPlaylistComplete?.Invoke(playlist.Length);
Stop();
return;
}
LoadPlaylistItem(currentIndex);
Play();
}
public void PreviousItem()
{
currentIndex = (currentIndex - 1 + playlist.Length) % playlist.Length;
LoadPlaylistItem(currentIndex);
Play();
}
}
7. 字幕系统集成
[System.Serializable]
public class SubtitleEntry
{
public double time;
[TextArea] public string text;
public Color color = Color.white;
}
public class VideoSubtitleSystem : MonoBehaviour
{
public VideoPlayerListener videoListener;
public SubtitleEntry[] subtitles;
public UnityEngine.UI.Text subtitleText;
private Coroutine subtitleCoroutine;
void Start()
{
videoListener.onTimeUpdate.AddListener(UpdateSubtitles);
}
private void UpdateSubtitles(double currentTime)
{
if (subtitleCoroutine != null)
StopCoroutine(subtitleCoroutine);
subtitleCoroutine = StartCoroutine(DisplaySubtitle(currentTime));
}
private IEnumerator DisplaySubtitle(double currentTime)
{
SubtitleEntry currentSubtitle = null;
foreach (var subtitle in subtitles)
{
if (Mathf.Abs((float)(currentTime - subtitle.time)) < 0.5f)
{
currentSubtitle = subtitle;
break;
}
}
if (currentSubtitle != null)
{
subtitleText.text = currentSubtitle.text;
subtitleText.color = currentSubtitle.color;
subtitleText.gameObject.SetActive(true);
}
else
{
subtitleText.gameObject.SetActive(false);
}
yield return null;
}
}
使用示例与最佳实践
8. 完整使用示例
public class GameCutsceneManager : MonoBehaviour
{
public VideoPlayerListener cutscenePlayer;
public GameObject[] sceneObjects;
void Start()
{
// 绑定事件
cutscenePlayer.onPrepareComplete.AddListener(() => {
Debug.Log("过场动画准备完成");
FadeInScene();
});
cutscenePlayer.onPlayComplete.AddListener(() => {
Debug.Log("过场动画播放完成");
EndCutscene();
});
cutscenePlayer.onError.AddListener((error) => {
Debug.LogError($"过场动画错误: {error}");
LoadFallbackScene();
});
// 开始播放
cutscenePlayer.PrepareAndPlay();
}
private void FadeInScene()
{
// 淡入场景物体
foreach (var obj in sceneObjects)
obj.SetActive(true);
}
private void EndCutscene()
{
// 切换到游戏场景
UnityEngine.SceneManagement.SceneManager.LoadScene("GameScene");
}
private void LoadFallbackScene()
{
// 加载备用场景
UnityEngine.SceneManagement.SceneManager.LoadScene("FallbackScene");
}
}
9. 性能优化建议
优化项 | 建议 | 效果 |
---|---|---|
进度更新 | 调整 updateInterval(0.1-0.5s) | 降低 CPU 占用 |
内存管理 | 播放完成后调用 Resources.UnloadUnusedAssets | 释放纹理内存 |
远程视频 | 使用缓存代理服务器 | 减少延迟 |
移动端 | 降低分辨率,使用 H.264 编码 | 提升兼容性 |
10. 常见问题解决
问题 | 原因 | 解决方案 |
---|---|---|
黑屏 | 视频格式不支持 | 使用 MP4/H.264,检查平台兼容 |
无声音 | AudioSource 配置错误 | 检查 audioOutputMode 和轨道启用 |
延迟高 | 网络问题 | 使用本地缓存或 CDN 加速 |
iOS 崩溃 | 内存不足 | 降低分辨率,启用 VideoPlayer.skipOnDrop |
此封装提供完整的 VideoPlayer 事件系统和编辑器集成,支持复杂播放场景和 XR 项目集成。如需直播流支持(RTMP/HLS)或多平台优化,请提供更多需求!