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)或多平台优化,请提供更多需求!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注