Unity视频播放性能优化:从VideoPlayer原理到流畅可控的实践方案

Unity视频播放性能优化:从VideoPlayer原理到流畅可控的实践方案 1. 项目概述为什么Unity视频播放是个“技术活”在Unity项目里加个视频播放听起来就像在PPT里插个视频一样简单对吧很多新手开发者一开始都这么想直到他们遇到了播放卡顿、内存飙升、黑屏无响应甚至在某些低端设备上直接闪退的“惊喜”。我见过太多项目前期功能开发飞快一到集成视频播放尤其是高清、长视频时整个应用的性能曲线就变得像过山车一样刺激。这背后远不止是拖一个VideoPlayer组件到场景里那么简单。Unity的VideoPlayer组件是一个功能强大但同时也相当“重量级”的系统。它背后连接着不同平台iOS的AVFoundation、Android的MediaPlayer、Windows的DirectShow/MF等的原生解码器涉及到GPU纹理内存的分配、CPU解码线程的管理、音频视频流的同步以及托管与非托管内存之间的数据交换。一个配置不当的VideoPlayer可以轻易地吃掉几百MB的内存并让CPU使用率居高不下成为你应用性能的“阿喀琉斯之踵”。因此优化Unity视频播放核心目标有两个流畅与可控。流畅意味着无论视频码率多高播放都要丝滑不掉帧、不卡顿可控则意味着内存占用要可预测、可管理不能成为内存泄漏的黑洞。这篇文章我将结合自己踩过的无数个坑从VideoPlayer的工作原理出发拆解如何通过精细化的配置和代码控制实现既流畅又内存友好的视频播放方案。无论你是在做游戏过场动画、应用内的教程视频还是需要动态加载的广告内容这些技巧都能帮你把视频模块打磨得更专业。2. VideoPlayer核心工作机制与性能瓶颈拆解要优化必须先理解它的工作流程。当你调用videoPlayer.Play()时背后发生了一系列复杂操作我们可以将其分为几个关键阶段每个阶段都可能成为性能瓶颈。2.1 视频播放的生命周期与资源流VideoPlayer的工作流可以概括为准备(Prepare) - 解码(Decode) - 上传(Upload) - 渲染(Render)。准备阶段当你设置videoPlayer.url或videoPlayer.clip并调用Prepare()或直接Play()时Unity会开始初始化底层播放器。这个过程包括打开文件或网络流、解析容器格式如MP4、MOV、读取音视频流信息编码格式、分辨率、时长、码率。对于网络视频这个阶段还包含建立连接和缓冲初始数据。如果视频文件很大或网络慢Prepare可能会阻塞主线程导致卡顿。这就是为什么有时点击播放按钮后界面会“冻住”几秒钟的原因。解码阶段这是最耗CPU的环节。解码器通常是硬件解码器但软件解码作为备选将压缩的视频数据H.264, H.265/HEVC解压成一帧帧的YUV或RGB图像数据。解码后的原始帧数据会暂存在一个**环形缓冲区(Ring Buffer)**中。这个缓冲区的大小是关键它决定了视频播放的“预读”能力也直接影响了内存占用。缓冲区太小容易因解码速度跟不上而导致卡顿缓冲区太大则会无谓地占用大量内存。上传阶段解码后的帧数据需要从CPU内存或解码器的专用内存上传(Upload)到GPU的显存中变成一张纹理(Texture)。对于VideoPlayer这张纹理通常是一个RenderTexture。这个上传操作glTexSubImage2D或类似的DX API调用是带宽密集型的。如果每一帧都上传一张4K纹理对移动设备的GPU带宽将是巨大考验。渲染阶段GPU使用上一步得到的纹理根据你设置的Render Mode如渲染到摄像机平面、渲染到RenderTexture等进行最终的画面合成与输出。2.2 主要性能瓶颈与内存陷阱理解了流程我们就能定位常见的“坑点”CPU瓶颈高分辨率、高码率视频的解码特别是使用软件解码时。如果视频编码格式设备不支持硬解如某些老设备不支持HEVCCPU占用会瞬间拉满。GPU内存瓶颈视频纹理是存储在GPU内存中的。一个1080p1920x1080的RGBA32纹理占用内存约为1920 * 1080 * 4 bytes ≈ 7.9 MB。如果是4K视频这个数字会变成约31.7 MB。如果视频播放完毕纹理没有被及时释放或者多个视频纹理同时存在GPU内存很快就会被撑爆导致渲染异常甚至崩溃。托管内存压力虽然视频帧数据本身主要在非托管内存但Unity为了管理VideoPlayer对象、事件回调、以及某些中间数据结构会产生托管堆的分配。频繁地创建、销毁VideoPlayer组件或者在回调中执行不当操作可能引发不必要的垃圾回收(GC)导致帧率波动。I/O与网络延迟对于本地文件磁盘读取速度是瓶颈对于网络视频网络波动和延迟是主要问题。VideoPlayer的缓冲策略如果不够智能就会导致播放中断等待加载。实操心得在移动设备上最需要警惕的是GPU内存和解码CPU开销。一个常见的误区是只关注播放是否卡顿而忽略了后台持续增长的内存占用。很多“黑屏无响应”的问题根源在于内存耗尽后系统杀掉了你的应用进程。3. 实现流畅播放的核心配置与代码策略知道了瓶颈在哪我们就可以有针对性地进行优化。下面这些配置和代码技巧是我从多个上线项目中总结出来的有效方案。3.1 精准控制播放流程Prepare与事件驱动永远不要依赖默认的playOnAwake true。你应该显式地控制播放流程使用Prepare()和相应的事件。using UnityEngine; using UnityEngine.Video; public class OptimizedVideoPlayer : MonoBehaviour { public VideoPlayer videoPlayer; public string videoUrl; // 或 public VideoClip videoClip; void Start() { if (videoPlayer null) videoPlayer gameObject.AddComponentVideoPlayer(); // 1. 关键关闭自动播放 videoPlayer.playOnAwake false; // 2. 关闭首帧等待除非你需要严格的同步 videoPlayer.waitForFirstFrame false; // 3. 设置视频源 videoPlayer.source VideoSource.Url; videoPlayer.url videoUrl; // 如果是VideoClip // videoPlayer.source VideoSource.VideoClip; // videoPlayer.clip videoClip; // 4. 配置渲染模式示例渲染到RenderTexture便于UI显示和后期控制 videoPlayer.renderMode VideoRenderMode.RenderTexture; RenderTexture rt new RenderTexture(1920, 1080, 0, RenderTextureFormat.ARGB32); rt.Create(); videoPlayer.targetTexture rt; // 将这个rt赋值给你的RawImage.material // 5. 订阅关键事件 videoPlayer.prepareCompleted OnVideoPrepared; videoPlayer.errorReceived OnVideoError; videoPlayer.loopPointReached OnVideoEnd; // 6. 开始准备异步不阻塞 videoPlayer.Prepare(); Debug.Log(开始准备视频...); } void OnVideoPrepared(VideoPlayer vp) { Debug.Log($视频准备就绪时长{vp.length}秒分辨率{vp.width}x{vp.height}); // 此时可以安全地开始播放或显示一个“播放按钮” // vp.Play(); } void OnVideoError(VideoPlayer vp, string errorMsg) { Debug.LogError($视频播放出错: {errorMsg}); // 进行错误处理如重试、显示错误界面等 } void OnVideoEnd(VideoPlayer vp) { Debug.Log(视频播放结束); // 执行清理操作见下文内存管理部分 Cleanup(); } // 外部调用的播放控制 public void PlayVideo() { if (videoPlayer.isPrepared) videoPlayer.Play(); else Debug.LogWarning(视频尚未准备就绪无法播放); } }为什么这么做Prepare()是异步的它允许你在后台加载视频资源而不会卡住主线程。在prepareCompleted事件触发后再播放能确保点击播放按钮时立即开始消除等待。通过事件驱动你可以精确地在合适的时机处理UI状态如显示加载圈、播放按钮、错误和播放结束逻辑。3.2 渲染模式Render Mode的选择与优化RenderMode决定了视频画面如何被绘制到屏幕上选择不当会影响性能和效果。渲染模式适用场景性能与内存考量注意事项Camera Far/Near Plane作为3D场景中的背景或贴图如天空盒、屏幕广告牌。直接由摄像机渲染管线处理性能通常较好。但视频纹理会占用一个摄像机渲染层。需要调整targetCameraAlpha来实现透明叠加。注意摄像机的裁剪平面设置确保视频在视野内。Render Texture最常用、最灵活的模式。用于UI显示RawImage、后期处理、或作为其他材质的输入。需要显式创建和管理RenderTexture对象。这是内存管理的核心你必须手动释放它。创建RT时尺寸尽量匹配视频分辨率避免不必要的缩放。使用RenderTextureFormat.Default或ARGB32通常足够。Material Override将视频作为动态纹理应用到3D物体的特定材质属性上。直接修改材质属性性能取决于材质复杂度和渲染次数。需要确保材质的Shader支持纹理属性。API Only不进行自动渲染仅通过videoPlayer.texture获取纹理由你完全控制渲染。最灵活但实现最复杂。你需要自己写代码将纹理应用到需要的地方。适用于高级定制如自定义播放器UI、复杂的多视频合成等。对于大多数UI视频播放需求我强烈推荐使用Render Texture模式。因为它将视频输出隔离到一个独立的纹理对象中你可以轻松地控制它的生命周期并且可以方便地将其赋值给UGUI的RawImage组件实现完美的2D UI集成。3.3 音频输出模式Audio Output Mode的取舍VideoPlayer也负责音频播放这里有三个选项Audio Source将音频路由到一个或多个Unity的AudioSource组件。这是推荐的方式因为它允许你利用Unity完整的音频混合系统包括空间音效、混音组、音量单独控制等。你可以为视频播放专门创建一个AudioSource并精细控制其属性。videoPlayer.audioOutputMode VideoAudioOutputMode.AudioSource; videoPlayer.SetTargetAudioSource(0, yourAudioSource); // 0代表第一个音轨 videoPlayer.controlledAudioTrackCount 1; // 启用一个音轨控制Direct音频绕过Unity的音频系统直接发送到硬件。这可以减少一些延迟但你会失去Unity音频管理的所有功能如全局音量控制、暂停。除非你对音频延迟有极端要求否则不建议使用。None/API Only不输出音频或仅提供音频样本数据。适用于你只需要视频画面或者打算用其他系统处理音频的情况。优化点如果你使用Audio Source模式记得将关联的AudioSource组件的Play On Awake也设为false并由VideoPlayer的事件来控制其播放/暂停避免音频不同步。3.4 跳帧与同步策略Skip On Drop与时钟源这是保证流畅性的高级技巧。Skip On Drop这个属性默认为false。当设置为true时如果VideoPlayer检测到解码或渲染跟不上即“掉帧”它会尝试跳过一些帧来追上音频时钟以保持音画同步。这会导致画面跳跃但能避免音画不同步越来越严重。对于实时性要求高、且音频重要的场景如游戏对话过场建议开启。对于静音播放或背景视频可以关闭以获得更平滑即使稍慢的视觉体验。videoPlayer.skipOnDrop true;Update Mode这个属性决定了VideoPlayer用什么“时钟”来推进播放。GameTime使用受Time.timeScale影响的游戏时间。如果你的游戏暂停Time.timeScale0视频也会暂停。这是最常用的模式。UnscaledGameTime使用不受Time.timeScale影响的游戏时间。即使游戏逻辑暂停视频也会继续播放。适用于播放与游戏逻辑无关的UI动画视频。DSPTime使用音频系统的时钟。这能提供最精确的音频同步但一般用于纯音频或对同步要求极高的专业场景。根据你的游戏状态管理需求来选择合适的模式。4. 内存管理的深度实践从分配到释放内存管理是VideoPlayer优化的重中之重。管理不善轻则内存泄漏重则应用崩溃。4.1 理解VideoPlayer的内存构成VideoPlayer占用的内存主要分三块解码缓冲区位于非托管内存用于存储待解码和已解码的帧数据。大小由视频分辨率、帧率和缓冲策略决定。这部分Unity自动管理但我们可以通过控制预加载量间接影响。目标纹理即RenderTexture位于GPU显存。这是我们必须手动管理的大头。托管堆对象VideoPlayer组件本身、事件委托、字符串等C#对象。4.2 RenderTexture的生命周期管理这是内存泄漏最常见的源头。一个黄金法则谁创建谁销毁。public class ManagedVideoPlayback : MonoBehaviour { private VideoPlayer _videoPlayer; private RenderTexture _videoTexture; public RawImage displayImage; // UGUI的RawImage用于显示 public void LoadAndPlayVideo(string path) { // 1. 清理旧的资源非常重要 Cleanup(); // 2. 创建新的VideoPlayer或复用 _videoPlayer gameObject.AddComponentVideoPlayer(); _videoPlayer.playOnAwake false; // 3. 创建RenderTexture // 建议根据视频实际分辨率创建避免缩放损耗。这里假设已知或使用默认值。 _videoTexture new RenderTexture(1920, 1080, 0); _videoTexture.name DynamicVideoRT; // 命名便于调试 _videoPlayer.renderMode VideoRenderMode.RenderTexture; _videoPlayer.targetTexture _videoTexture; // 4. 赋值给UI if (displayImage ! null) { displayImage.texture _videoTexture; } // 5. 设置源并准备 _videoPlayer.url path; _videoPlayer.Prepare(); _videoPlayer.prepareCompleted (vp) vp.Play(); } public void StopAndCleanup() { if (_videoPlayer ! null) { _videoPlayer.Stop(); } Cleanup(); } private void Cleanup() { // 关键销毁RenderTexture if (_videoTexture ! null) { // 先从UI上解除引用 if (displayImage ! null displayImage.texture _videoTexture) { displayImage.texture null; } // 释放GPU资源 _videoTexture.Release(); // 销毁UnityEngine.Object Destroy(_videoTexture); _videoTexture null; } // 销毁VideoPlayer组件 if (_videoPlayer ! null) { // 取消所有事件订阅避免内存泄漏 _videoPlayer.prepareCompleted null; _videoPlayer.loopPointReached null; _videoPlayer.errorReceived null; // ... 取消其他所有事件订阅 Destroy(_videoPlayer); _videoPlayer null; } } void OnDestroy() { // 确保对象销毁时资源被清理 Cleanup(); } }关键点RenderTexture.Release()立即释放GPU端的纹理资源。这是必须调用的。Destroy(_videoTexture)销毁UnityEngine.Object对象本身。顺序很重要先解除UI引用 (displayImage.texture null)再释放和销毁纹理。否则可能导致UI引用一个已被销毁的纹理产生错误。事件取消订阅如果VideoPlayer组件被销毁但之前订阅的事件没有取消委托引用会阻止垃圾回收器回收包含该方法的对象导致隐式内存泄漏。在Cleanup中将其置为null是良好习惯。4.3 预加载与缓冲策略优化VideoPlayer内部有缓冲机制但我们也可以通过Prepare()来手动控制“预加载”的时机。对于需要秒开的视频可以在用户可能点击前就提前准备。// 假设有一个视频列表预加载下一个可能播放的视频 private VideoPlayer _preloadPlayer; private string _preloadUrl; public void PreloadVideo(string url) { if (_preloadPlayer ! null) { // 如果正在预加载另一个先停止并清理 _preloadPlayer.Stop(); Destroy(_preloadPlayer.gameObject); // 假设预加载播放器在独立GameObject上 } GameObject go new GameObject(PreloadVideoPlayer); _preloadPlayer go.AddComponentVideoPlayer(); _preloadPlayer.playOnAwake false; _preloadPlayer.url url; _preloadUrl url; // 预加载但不播放 _preloadPlayer.Prepare(); } public void PlayPreloadedVideo() { if (_preloadPlayer ! null _preloadPlayer.isPrepared _preloadPlayer.url _preloadUrl) { // 将预加载的播放器“移交”给主播放系统 // ... (这里需要将_preloadPlayer的targetTexture等配置转移到主播放器) _preloadPlayer.Play(); _preloadPlayer null; // 移交后清空引用 } else { // 后备方案正常加载播放 LoadAndPlayVideo(_preloadUrl); } }注意事项预加载会占用额外的内存和CPU资源。在内存紧张的移动设备上不宜同时预加载多个大视频。需要根据设备性能和业务需求做权衡。4.4 针对不同平台的特别优化iOS确保视频编码格式是设备支持的如H.264 Baseline/Main Profile。HEVCH.265虽然压缩率高但在旧设备上可能不支持硬解。使用Application.RequestAuthorization处理相册访问权限避免因权限问题导致播放失败。Android情况更复杂。不同厂商、不同系统版本的解码能力差异巨大。务必在真机上进行广泛的兼容性测试。对于网络视频注意Android 9.0 (Pie) 及以上版本默认禁用明文HTTP流量确保使用HTTPS或配置网络安全策略。WebGLWebGL平台对VideoPlayer的支持有限如不支持VideoClip主要用URL。视频文件需要放在StreamingAssets文件夹下并通过相对路径访问。浏览器的跨域策略(CORS)也可能导致视频加载失败服务器需要正确配置CORS头。5. 高级技巧与疑难问题排查实录掌握了基础配置和内存管理后我们来看看一些能进一步提升体验和稳定性的高级技巧以及如何解决那些令人头疼的常见问题。5.1 无缝循环与淡入淡出实现视频的无缝循环单纯设置isLooping true有时在循环点会有轻微卡顿。一个更平滑的方案是使用两个VideoPlayer交替播放。public class SeamlessLoopVideo : MonoBehaviour { public VideoPlayer videoPlayerA; public VideoPlayer videoPlayerB; private VideoPlayer _currentPlayer; private VideoPlayer _nextPlayer; public RawImage display; void Start() { // 初始化两个播放器指向同一个视频源 SetupPlayer(videoPlayerA); SetupPlayer(videoPlayerB); _currentPlayer videoPlayerA; _nextPlayer videoPlayerB; display.texture videoPlayerA.targetTexture; videoPlayerA.loopPointReached OnLoopPointReached; videoPlayerA.Play(); } void SetupPlayer(VideoPlayer vp) { vp.playOnAwake false; vp.isLooping false; // 禁用单个播放器的循环 vp.renderMode VideoRenderMode.RenderTexture; vp.targetTexture new RenderTexture(1920, 1080, 0); vp.Prepare(); } void OnLoopPointReached(VideoPlayer vp) { // 当前播放器播放完毕时立即切换显示下一个播放器已预加载到开头 display.texture _nextPlayer.targetTexture; _nextPlayer.Play(); // 交换角色 VideoPlayer temp _currentPlayer; _currentPlayer _nextPlayer; _nextPlayer temp; // 重置并准备刚才播放完的播放器作为下一个“备用” _nextPlayer.Stop(); _nextPlayer.frame 0; _nextPlayer.Prepare(); // 为当前播放器重新订阅结束事件 _currentPlayer.loopPointReached OnLoopPointReached; } }对于淡入淡出可以通过控制显示视频的UI元素如RawImage或CanvasGroup的Alpha值结合协程来实现平滑过渡。5.2 性能监控与自适应在运行时监控视频播放的性能并在必要时动态调整。using UnityEngine.Profiling; public class VideoPerformanceMonitor : MonoBehaviour { public VideoPlayer videoPlayer; private System.Diagnostics.Stopwatch _decodeWatch new System.Diagnostics.Stopwatch(); private long _lastFrameCount; private float _adjustmentTimer 0f; public float checkInterval 2.0f; // 每2秒检查一次 void Update() { if (videoPlayer null || !videoPlayer.isPlaying) return; _adjustmentTimer Time.deltaTime; if (_adjustmentTimer checkInterval) { _adjustmentTimer 0f; CheckAndAdjustQuality(); } } void CheckAndAdjustQuality() { // 示例1检查帧率是否过低 float currentFPS 1.0f / Time.deltaTime; if (currentFPS 25f) // 假设目标30fps低于25则认为卡顿 { Debug.LogWarning($帧率过低({currentFPS:F1}FPS)尝试降低播放质量); // 策略降低播放速度或切换到更低码率的视频源如果有 videoPlayer.playbackSpeed 0.9f; // 轻微降速 // 或者如果你有备用的低清视频流 // SwitchToLowerQualityStream(); } else if (currentFPS 55f videoPlayer.playbackSpeed 1.0f) { // 性能恢复尝试恢复正常速度 videoPlayer.playbackSpeed Mathf.Min(videoPlayer.playbackSpeed 0.05f, 1.0f); } // 示例2使用Profiler监控内存开发阶段 long textureMemory Profiler.GetAllocatedMemoryForGraphicsDriver(); // 注意这个值包含所有图形内存不单是视频纹理。需要建立基线进行比较。 // if (textureMemory YOUR_SAFE_THRESHOLD) { ... } } }5.3 常见问题排查速查表以下是我在项目中遇到并解决过的一些典型问题问题现象可能原因排查步骤与解决方案黑屏但音频正常1. 渲染模式或目标设置错误。2. RenderTexture创建失败或未正确赋值。3. 着色器不支持视频纹理。1. 检查renderMode和targetTexture/targetCamera是否正确设置。2. 在代码中创建RT后检查rt.IsCreated()是否为true。3. 如果是Material Override模式检查材质Shader是否有对应的纹理属性。尝试使用Standard Shader或UI/Unlit/Transparent等简单Shader测试。播放卡顿、掉帧1. 视频分辨率/码率超出设备解码能力。2. 硬盘I/O或网络慢缓冲不足。3. 游戏本身CPU占用过高挤占了解码资源。4. GPU带宽不足特别是移动设备。1. 使用性能分析器如Unity Profiler的CPU/GPU模块查看瓶颈。尝试播放更低分辨率的视频。2. 对于网络视频检查缓冲。可适当增加videoPlayer.bufferTime如果平台支持。3. 优化游戏其他模块的CPU开销。尝试开启skipOnDrop。4. 避免在同一帧进行大量其他GPU操作如大量UI重绘、复杂粒子。内存持续增长最终崩溃1.RenderTexture未释放最常见。2. VideoPlayer组件未销毁事件未取消订阅。3. 视频源本身有内存泄漏某些特殊编码。1. 严格遵循“创建-释放”配对在OnDestroy、播放结束、切换视频时调用清理函数。2. 使用Unity Profiler的Memory模块查看RenderTexture和VideoPlayer实例数量是否异常。3. 尝试播放不同的视频文件排除文件本身问题。音频与视频不同步1. 解码性能不足导致视频帧落后。2.Update Mode设置不当。3. 音频输出模式为Direct时与游戏音频系统冲突。1. 开启skipOnDrop。2. 确保UpdateMode设置为GameTime或UnscaledGameTime并与游戏的时间缩放逻辑匹配。3. 优先使用Audio Source输出模式让Unity统一管理音频时钟。在部分Android设备上无法播放1. 视频编码格式不支持如HEVC on old devices。2. 文件路径错误StreamingAssets路径问题。3. 权限问题读取存储权限。1. 统一转码为兼容性最广的H.264 Baseline/Main Profile, AAC音频。2. 使用Application.streamingAssetsPath拼接路径并确保文件在APK中。3. 动态申请READ_EXTERNAL_STORAGE权限。Prepare()耗时过长或失败1. 文件路径不存在或网络URL不可达。2. 文件格式损坏或不支持。3. 同步调用在主线程造成卡顿。1. 监听errorReceived事件获取错误信息。2. 使用Prepare()异步模式并在UI上显示加载状态。3. 对于大文件考虑在后台线程进行初始文件读取但VideoPlayer的Prepare必须在主线程。WebGL平台无法加载视频1. 文件不在StreamingAssets中。2. 服务器未正确配置CORS。3. 使用了VideoClipWebGL不支持。1. 将视频文件放在Assets/StreamingAssets文件夹下。2. 使用videoPlayer.url System.IO.Path.Combine(Application.streamingAssetsPath, “myVideo.mp4”);。3. 确保服务器对视频文件响应头包含Access-Control-Allow-Origin: *。5.4 一个健壮的VideoPlayer管理器示例最后分享一个我常用的VideoPlayer管理器类的简化框架它集成了加载、播放、内存管理和错误处理。using System; using UnityEngine; using UnityEngine.Video; using UnityEngine.UI; [RequireComponent(typeof(RawImage))] public class RobustVideoManager : MonoBehaviour { public event Action OnVideoPrepared; public event Action OnVideoStarted; public event Action OnVideoEnded; public event Actionstring OnVideoError; [SerializeField] private VideoPlayer _videoPlayer; [SerializeField] private RawImage _display; [SerializeField] private GameObject _loadingIndicator; [SerializeField] private bool _autoPlay false; private RenderTexture _currentRT; private string _currentVideoPath; private bool _isPrepared false; void Awake() { if (_display null) _display GetComponentRawImage(); if (_videoPlayer null) _videoPlayer gameObject.AddComponentVideoPlayer(); InitializeVideoPlayer(); } private void InitializeVideoPlayer() { _videoPlayer.playOnAwake false; _videoPlayer.waitForFirstFrame false; _videoPlayer.renderMode VideoRenderMode.RenderTexture; _videoPlayer.audioOutputMode VideoAudioOutputMode.AudioSource; _videoPlayer.prepareCompleted HandlePrepareCompleted; _videoPlayer.started HandleVideoStarted; _videoPlayer.loopPointReached HandleVideoEnded; _videoPlayer.errorReceived HandleVideoError; } public void LoadVideo(string videoPath) { if (string.IsNullOrEmpty(videoPath)) { Debug.LogError(视频路径为空); return; } // 如果正在播放另一个视频先停止 if (_videoPlayer.isPlaying) { _videoPlayer.Stop(); } // 清理上一视频资源 CleanupCurrentTexture(); _currentVideoPath videoPath; _isPrepared false; // 显示加载状态 if (_loadingIndicator ! null) _loadingIndicator.SetActive(true); if (_display ! null) _display.enabled false; // 配置并准备 _videoPlayer.url videoPath; _videoPlayer.Prepare(); } public void Play() { if (!_isPrepared) { Debug.LogWarning(视频未准备就绪无法播放。); // 可以在这里尝试调用 Prepare() 并播放 return; } _videoPlayer.Play(); } public void Pause() _videoPlayer?.Pause(); public void Stop() _videoPlayer?.Stop(); private void HandlePrepareCompleted(VideoPlayer source) { _isPrepared true; Debug.Log($视频准备完成: {source.url}); // 创建与视频分辨率匹配的RenderTexture if (_currentRT ! null) CleanupCurrentTexture(); _currentRT new RenderTexture((int)source.width, (int)source.height, 0); _currentRT.Create(); source.targetTexture _currentRT; // 更新UI显示 if (_display ! null) { _display.texture _currentRT; _display.enabled true; } if (_loadingIndicator ! null) _loadingIndicator.SetActive(false); OnVideoPrepared?.Invoke(); if (_autoPlay) { Play(); } } private void HandleVideoStarted(VideoPlayer source) { Debug.Log(视频开始播放); OnVideoStarted?.Invoke(); } private void HandleVideoEnded(VideoPlayer source) { Debug.Log(视频播放结束); OnVideoEnded?.Invoke(); // 如果不循环可以在这里自动清理 if (!source.isLooping) { // Cleanup(); // 谨慎选择自动清理的时机 } } private void HandleVideoError(VideoPlayer source, string message) { Debug.LogError($视频播放错误: {message}); if (_loadingIndicator ! null) _loadingIndicator.SetActive(false); OnVideoError?.Invoke(message); } private void CleanupCurrentTexture() { if (_currentRT ! null) { if (_display ! null _display.texture _currentRT) { _display.texture null; } _currentRT.Release(); Destroy(_currentRT); _currentRT null; } _isPrepared false; } public void Cleanup() { if (_videoPlayer ! null) { _videoPlayer.Stop(); // 取消事件订阅 _videoPlayer.prepareCompleted - HandlePrepareCompleted; _videoPlayer.started - HandleVideoStarted; _videoPlayer.loopPointReached - HandleVideoEnded; _videoPlayer.errorReceived - HandleVideoError; } CleanupCurrentTexture(); _currentVideoPath null; } void OnDestroy() { Cleanup(); } }这个管理器提供了从加载、状态回调到资源清理的完整生命周期管理你可以直接将其挂载到UI的RawImage对象上使用或者作为更复杂视频系统的基础组件。记住在Unity中处理视频细节决定成败。每一次Prepare()每一个RenderTexture都需要你心中有数手中有策。