ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Unity WebGL视频播放全攻略:从原理到实战解决黑屏卡顿

Unity WebGL视频播放全攻略:从原理到实战解决黑屏卡顿

1. 项目概述:为什么Unity WebGL视频播放是个“老大难”?

如果你在Unity里做过视频播放,可能会觉得这功能简单到不值一提——拖个Video Player组件,指定一个MP4文件,运行,搞定。但当你信心满满地把项目打包成WebGL,准备在浏览器里大展拳脚时,现实往往会给你当头一棒:黑屏、无声、卡顿,或者干脆弹出一个“Video format not supported”的错误。这种从编辑器到WebGL的“水土不服”,是很多Unity开发者,尤其是刚接触Web平台的同行,踩过的第一个大坑。

这个项目标题“Unity WebGL视频播放全攻略”,精准地戳中了这个痛点。它不是一个简单的功能教程,而是一套针对WebGL这个特殊“运行环境”的完整解决方案。WebGL不是另一个操作系统,它是在浏览器沙箱里运行的,这意味着它没有直接的文件系统访问权限,音频视频的编解码能力完全依赖于浏览器内核,并且受到严格的安全策略限制。你电脑上那个能播的MP4,到了浏览器里,可能因为编码格式、码率、甚至一个不起眼的元数据问题,就彻底罢工了。所以,实现WebGL视频播放,核心思路必须从“Unity怎么播”转变为“浏览器允许Unity怎么播”。

这篇文章,就是为你拆解这个转变过程。我会从一个从业十年的老鸟视角,带你从零开始,不依赖任何第三方插件,只用Unity原生组件和一点点脚本,构建一个能在WebGL上稳定、流畅播放内嵌MP4视频的完整方案。无论你是想为你的WebGL游戏添加过场动画,还是为产品展示页面嵌入演示视频,这套方法都能让你避开我当年踩过的所有坑,直达终点。我们不仅会实现功能,更会深入解释每一个步骤背后的“为什么”,让你知其然,更知其所以然。

2. 核心思路与方案选型:为什么“内嵌”是WebGL的最佳拍档?

在深入代码之前,我们必须先统一思想:在WebGL里播放视频,路径不止一条,但“内嵌”往往是综合成本最低、兼容性最稳妥的选择。所谓“内嵌”,就是把MP4文件作为资源打包进你的WebGL构建产物(Build)里,而不是从远程服务器动态加载。这听起来有点反直觉,毕竟网络加载更灵活,但请听我分析。

2.1 远程加载 vs. 内嵌资源:一场关于稳定性的博弈

远程加载视频听起来很美:视频文件独立于游戏包,更新视频无需重新打包和部署整个WebGL应用。但在WebGL的实践里,这条路布满荆棘。首先,是跨域问题(CORS)。浏览器出于安全考虑,默认禁止从一个域名(你的WebGL托管服务器)向另一个域名(你的视频文件服务器)发起请求,除非视频服务器明确设置了允许跨域的HTTP头。这个配置权往往不在你手里,或者配置起来非常麻烦。其次,是网络不确定性。用户的网络波动会导致视频加载卡顿、缓冲,严重影响体验,尤其是作为关键内容的过场动画。最后,还有HTTPS/HTTP混合内容警告。如果你的页面是HTTPS的,而视频源是HTTP的,现代浏览器会直接阻止加载。

相比之下,内嵌资源虽然增大了初始加载包体,但它换来了绝对的稳定性和可控性。视频文件和其他资源(如图片、场景)一起被打包、压缩,并通过Unity WebGL的加载系统统一管理。这意味着:

  1. 零跨域问题:所有资源同源,浏览器开绿灯。
  2. 加载可预测:视频的加载进度可以整合进你的整体加载界面,用户体验是线性的、完整的。
  3. 离线可用:一旦页面加载完毕,视频即可播放,不依赖后续网络。

对于片长在几十秒到两三分钟的宣传片、过场动画、UI反馈视频,内嵌是黄金选择。只有当视频文件巨大(如超过50MB),或者需要极高频更新时,我们才需要考虑设计更复杂的流媒体或CDN方案。

2.2 Unity Video Player组件的“两面性”

确定了内嵌路线,我们就要请出Unity的核心工具:VideoPlayer组件。这个组件在编辑器模式下非常强大,支持多种源(URL、VideoClip),但在WebGL下,它的能力被浏览器“套上了枷锁”。最关键的一点是:WebGL平台下,VideoPlayersource类型设置为VideoClip是无效的。很多新手会像在PC或移动端一样,在Video Player组件上直接拖入一个Video Clip,打包后自然黑屏。这是因为VideoClip是Unity内部封装的一种资源格式,而WebGL的播放最终是靠浏览器的<video>标签,它只认直接的视频文件路径(URL)。

所以,在WebGL中,我们必须将VideoPlayersource类型设置为URL,然后给它一个指向我们内嵌视频文件的路径。这个路径,就是接下来我们要解决的核心问题:如何让Unity在WebGL构建里,找到并访问那个被打包进去的MP4文件。

2.3 视频格式的“潜规则”:不是所有MP4都能播

这是另一个至关重要的前置知识。MP4只是一个容器格式,里面封装了视频轨和音频轨的编码数据。浏览器对内部编码的支持程度千差万别。经过多年的实践和踩坑,我总结出一条WebGL视频格式的“金科玉律”:

使用H.264编码视频 + AAC编码音频的MP4文件。

  • 为什么是H.264?它是目前所有现代浏览器(Chrome, Firefox, Safari, Edge)都硬件加速支持最广泛的视频编码标准,解码效率高,功耗低。
  • 为什么是AAC?它是H.264视频的“官配”音频编码,兼容性最好。

你需要用格式转换工具(如FFmpeg、HandBrake)确保你的源文件符合这个标准。一个常见的错误是使用MOV或其他容器格式直接改名成MP4,或者MP4内部封装了MP3音频,这些在WebGL下都极有可能失败。

实操心得:我习惯用FFmpeg命令进行批量转换和验证,一条命令搞定:ffmpeg -i input.mp4 -c:v libx264 -profile:v high -level 4.2 -preset slow -crf 23 -c:a aac -b:a 128k output.mp4。这条命令指定了H.264 High Profile编码,并保证了音频是AAC。转换后,务必在浏览器中直接用<video>标签测试一下这个MP4文件能否正常播放,这是排查问题的第一步。

3. 项目实战:构建可复用的WebGL视频播放系统

理论清晰了,我们开始动手。我将分步构建一个包含完整错误处理和播放控制的视频播放管理器。

3.1 资源准备与导入设置

首先,将你的MP4视频文件拖入Unity项目的Assets文件夹,例如Assets/StreamingAssets/Videos/目录下。这里有一个关键选择:是否使用StreamingAssets文件夹?

StreamingAssets文件夹内的资源在打包后会被原封不动地复制到输出目录,可以通过特定路径直接访问。对于WebGL,这些资源位于构建后的StreamingAssets子目录中。将视频放在这里是个好习惯,因为它逻辑清晰,并且Unity提供了Application.streamingAssetsPath这个API来获取路径。

在Unity编辑器中,选中导入的MP4文件,查看Inspector面板。你需要关注以下设置:

  • 平台覆盖:确保选中“WebGL”平台。
  • 导入为:对于视频,保持默认设置即可。Unity不会对符合标准的MP4进行重编码。
  • 注意:不要尝试在Unity中将它创建为VideoClip类型的资源(比如通过右键菜单),因为正如前文所述,VideoClip在WebGL中无法用于VideoPlayer

3.2 编写核心播放脚本:VideoPlayerManager

我们将创建一个名为VideoPlayerManager的C#脚本,它将是控制视频播放的大脑。

using UnityEngine; using UnityEngine.Video; using System.Collections; public class VideoPlayerManager : MonoBehaviour { [Header("视频配置")] [Tooltip("在StreamingAssets文件夹下的视频相对路径,例如:Videos/myTrailer.mp4")] public string videoRelativePath = "Videos/sample.mp4"; [Tooltip("渲染视频的RawImage UI组件(可选)")] public UnityEngine.UI.RawImage targetRawImage; [Tooltip("是否在Awake时自动初始化")] public bool initOnAwake = true; [Header("播放控制")] public bool playOnPrepareCompleted = true; public bool loop = false; // 内部组件引用 private VideoPlayer videoPlayer; private AudioSource audioSource; // 视频准备状态 private bool isPrepared = false; void Awake() { if (initOnAwake) { InitializeVideoPlayer(); } } /// <summary> /// 初始化VideoPlayer组件 /// </summary> public void InitializeVideoPlayer() { // 确保只有一个VideoPlayer videoPlayer = GetComponent<VideoPlayer>(); if (videoPlayer == null) { videoPlayer = gameObject.AddComponent<VideoPlayer>(); } // 获取或添加AudioSource用于输出音频 audioSource = GetComponent<AudioSource>(); if (audioSource == null) { audioSource = gameObject.AddComponent<AudioSource>(); } // 配置VideoPlayer核心属性 videoPlayer.playOnAwake = false; videoPlayer.waitForFirstFrame = true; // 等待第一帧准备好再开始,避免黑屏闪烁 videoPlayer.skipOnDrop = true; // 允许在性能不足时丢帧,保证音频同步 videoPlayer.isLooping = loop; // 关键步骤:设置音频输出到AudioSource videoPlayer.audioOutputMode = VideoAudioOutputMode.AudioSource; videoPlayer.SetTargetAudioSource(0, audioSource); // 0表示第一个音轨 // 构建WebGL下的视频文件URL string videoUrl = ConstructVideoURL(videoRelativePath); Debug.Log($"准备播放视频,URL: {videoUrl}"); videoPlayer.source = VideoSource.Url; videoPlayer.url = videoUrl; // 如果有目标RawImage,则设置渲染纹理 if (targetRawImage != null) { // 创建一个RenderTexture RenderTexture renderTexture = new RenderTexture(1920, 1080, 24, RenderTextureFormat.ARGB32); renderTexture.Create(); videoPlayer.targetTexture = renderTexture; targetRawImage.texture = renderTexture; } else { // 如果没有指定RawImage,则渲染到当前物体的Renderer上(如Plane) videoPlayer.renderMode = VideoRenderMode.MaterialOverride; // 这里需要你根据实际情况设置目标Renderer和材质属性名,例如: // videoPlayer.targetMaterialRenderer = GetComponent<Renderer>(); // videoPlayer.targetMaterialProperty = "_MainTex"; } // 注册关键事件回调 videoPlayer.prepareCompleted += OnVideoPrepared; videoPlayer.errorReceived += OnVideoError; videoPlayer.loopPointReached += OnVideoLoopPointReached; // 开始准备视频 videoPlayer.Prepare(); } /// <summary> /// 构建适用于WebGL和Editor的视频URL /// </summary> private string ConstructVideoURL(string relativePath) { string url; // 处理路径中的反斜杠,确保是正斜杠 relativePath = relativePath.Replace("\\", "/"); #if UNITY_WEBGL && !UNITY_EDITOR // WebGL运行时:路径基于StreamingAssets文件夹 // 注意:WebGL中,StreamingAssets的路径是相对URL,例如 "/StreamingAssets/Videos/sample.mp4" // 但直接使用 Application.streamingAssetsPath 通常能得到正确格式 url = System.IO.Path.Combine(Application.streamingAssetsPath, relativePath); // 确保是file://协议(对于打包资源)或 http(s)://协议 if (!url.StartsWith("http")) { // 对于本地文件,需要 file:// 协议,但在WebGL的虚拟文件系统中,直接使用路径即可 // 实际测试中,直接使用 Application.streamingAssetsPath 组合的路径通常有效 } #else // 编辑器或其他平台:使用 file:// 协议 string fullPath = System.IO.Path.Combine(Application.streamingAssetsPath, relativePath); url = "file://" + fullPath; #endif return url; } /// <summary> /// 视频准备完成回调 /// </summary> private void OnVideoPrepared(VideoPlayer vp) { Debug.Log("视频准备就绪,时长: " + vp.length + " 秒"); isPrepared = true; if (playOnPrepareCompleted) { Play(); } // 可以在这里触发UI事件,比如隐藏加载圈 } /// <summary> /// 开始播放 /// </summary> public void Play() { if (!isPrepared) { Debug.LogWarning("视频尚未准备就绪,正在尝试准备..."); videoPlayer.Prepare(); return; } videoPlayer.Play(); if (audioSource != null) { audioSource.Play(); } Debug.Log("开始播放视频"); } /// <summary> /// 暂停播放 /// </summary> public void Pause() { if (videoPlayer.isPlaying) { videoPlayer.Pause(); Debug.Log("视频已暂停"); } } /// <summary> /// 停止播放并重置 /// </summary> public void Stop() { videoPlayer.Stop(); isPrepared = false; Debug.Log("视频已停止"); } /// <summary> /// 跳转到指定时间(秒) /// </summary> public void Seek(float timeInSeconds) { if (isPrepared) { videoPlayer.time = timeInSeconds; } } /// <summary> /// 视频播放错误回调 /// </summary> private void OnVideoError(VideoPlayer vp, string errorMsg) { Debug.LogError($"视频播放出错: {errorMsg}"); // 这里可以触发错误处理UI,比如显示“加载失败”提示 isPrepared = false; } /// <summary> /// 视频循环结束回调(当loop为false时) /// </summary> private void OnVideoLoopPointReached(VideoPlayer vp) { Debug.Log("视频播放完毕"); // 可以触发播放完成事件,比如自动播放下一个视频或显示UI } void OnDestroy() { // 清理事件注册,防止内存泄漏 if (videoPlayer != null) { videoPlayer.prepareCompleted -= OnVideoPrepared; videoPlayer.errorReceived -= OnVideoError; videoPlayer.loopPointReached -= OnVideoLoopPointReached; videoPlayer.Stop(); if (videoPlayer.targetTexture != null) { videoPlayer.targetTexture.Release(); } } } }

脚本关键点解析:

  1. ConstructVideoURL方法:这是脚本的灵魂。它通过平台编译指令 (#if UNITY_WEBGL && !UNITY_EDITOR) 区分了编辑器和WebGL运行时的路径构造方式。在编辑器中,我们使用file://协议加绝对路径;在WebGL中,我们直接使用Application.streamingAssetsPath组合路径。这个路径在WebGL构建中会指向正确的资源位置。
  2. 事件驱动:我们没有在Update里轮询状态,而是通过注册prepareCompletederrorReceived等回调函数来响应视频状态变化。这是更高效、更清晰的做法。
  3. 音频分离:显式地将VideoPlayer的音频输出模式设置为AudioSource,并关联一个AudioSource组件。这让你能像控制普通音频一样控制视频音量、混合等。
  4. 资源清理:在OnDestroy中释放RenderTexture并注销事件,这是良好的编程习惯,能避免WebGL应用中的内存泄漏。

3.3 场景搭建与测试

  1. 在场景中创建一个UICanvas
  2. Canvas下创建一个RawImage组件,它将用于显示视频画面。调整其大小和位置。
  3. 创建一个空的GameObject,命名为“VideoManager”。
  4. VideoPlayerManager脚本挂载到“VideoManager”上。
  5. 在Inspector中,将targetRawImage字段拖拽赋值为你创建的RawImage
  6. 确保videoRelativePath字段填写正确,例如你的视频文件在Assets/StreamingAssets/Videos/下名为intro.mp4,则此处填写Videos/intro.mp4
  7. 你还可以添加两个UI按钮(Play, Pause),并为其OnClick()事件分别绑定VideoPlayerManager实例的Play()Pause()方法。

在Unity编辑器中运行,如果路径正确,你应该能看到视频在RawImage中播放。这验证了脚本逻辑的基本正确性。

4. WebGL构建与部署的关键配置

在编辑器里跑通只是成功了一半,真正的考验在打包之后。

4.1 Player Settings 关键配置

打开File -> Build Settings -> Player Settings...,切换到WebGL平台。

  • Resolution and Presentation:
    • Default Canvas Width/Height: 根据你的视频分辨率或游戏设计设置。
    • Run In Background: 如果你的视频播放需要后台运行(比如作为背景),请勾选。
  • Publishing Settings:
    • Compression Format: 选择gzip。这是目前最通用、压缩比和性能平衡最好的格式。Brotli压缩率更高,但需要服务器支持。
    • Data Caching:务必勾选。这能利用浏览器的缓存机制,让用户第二次访问时加载飞快,对于内嵌的视频资源尤为重要。
    • Decompression Fallback: 也建议勾选,它会在浏览器不支持某种解压方式时提供备选方案,增强兼容性。

4.2 构建后的文件结构

点击Build后,Unity会生成一个包含以下关键文件的文件夹:

YourWebGLBuild/ ├── index.html // 主入口页面 ├── Build/ // 包含 .unityweb 等核心资源文件 ├── StreamingAssets/ // 你的视频文件就在这里! │ └── Videos/ │ └── sample.mp4 └── TemplateData/ // 加载界面等模板资源

重要!你需要将整个文件夹上传到你的Web服务器。不能只上传Build文件夹。因为index.htmlStreamingAssets文件夹与Build文件夹是平级的,这是Unity WebGL加载资源的既定目录结构。

4.3 本地测试与服务器测试

  • 本地测试:直接双击index.html在浏览器中打开,大概率会失败。因为浏览器的本地文件协议 (file://) 有严格的跨域限制,VideoPlayer可能无法加载视频。正确的本地测试方法是使用一个本地HTTP服务器。
    • 简单方法:如果你安装了Python,在构建目录下打开命令行,运行python -m http.server 8000,然后在浏览器访问http://localhost:8000
    • 其他工具:Node.js的http-server、或者一些编辑器插件(如VSCode的Live Server)都可以。
  • 服务器测试:上传到服务器后,通过公网URL访问。这是最终的验收环境。

5. 深度问题排查与性能优化指南

即使按照上述步骤操作,你可能还是会遇到问题。下面是我总结的“排错三板斧”和优化技巧。

5.1 常见问题与解决方案速查表

问题现象可能原因排查步骤与解决方案
黑屏,无画面无声音1. 视频路径错误。
2. 视频格式/编码不支持。
3.VideoPlayer未正确配置音频输出。
1. 打开浏览器开发者工具(F12)的Network标签页,刷新页面,查看是否有对MP4文件的请求。如果请求失败(404),检查ConstructVideoURL方法生成的路径,并确认文件是否在StreamingAssets文件夹。
2. 用浏览器直接打开服务器上的MP4文件链接(如http://yourserver.com/YourWebGLBuild/StreamingAssets/Videos/sample.mp4),看能否播放。不能播放则一定是格式问题,用FFmpeg重新转码。
3. 检查AudioSource组件是否被静音,或VideoPlayeraudioOutputMode是否设置正确。
有声音,无画面1.RenderTexture创建失败或未赋值给RawImage
2.RawImage的材质/Shader问题。
1. 在OnVideoPrepared回调中,检查videoPlayer.targetTexture是否不为null,以及targetRawImage.texture是否已赋值。
2. 尝试将RawImage的材质暂时改为默认的UI/Default
画面卡顿、掉帧1. 视频分辨率/码率过高。
2. 浏览器硬件加速未开启或性能不足。
3. Unity主线程阻塞。
1. 针对WebGL优化视频:降低分辨率(如1080p)、使用更低的CRF值(如25-28)转码。
2. 在浏览器设置中确保硬件加速已开启。提醒用户关闭其他耗资源的网页。
3. 避免在Update中执行繁重逻辑。确保视频播放期间GC(垃圾回收)压力小。
音频播放不同步1. 浏览器性能问题导致解码延迟。
2.VideoPlayer.skipOnDrop设置不当。
1. 同卡顿优化,降低视频负载。
2. 尝试将videoPlayer.skipOnDrop设置为true(允许丢帧保音频),这通常是更优选择。
移动端无法播放1. 移动浏览器自动播放策略限制。
2. 移动设备性能限制。
1.这是最大的坑!iOS和许多安卓浏览器禁止非用户交互触发的音频自动播放。解决方案:视频设置为静音 (videoPlayer.audioOutputMode = VideoAudioOutputMode.None或设置audioSource.mute = true),或者将Play()的调用绑定到一个按钮的点击事件上,由用户手势触发。
2. 为移动端准备更低码率(如720p)的视频版本,并通过脚本检测设备类型来切换视频源。
构建后视频路径404StreamingAssets文件夹未正确包含在构建中,或服务器配置问题。检查构建输出目录,确认StreamingAssets文件夹及其内容存在。如果使用某些服务器(如Nginx),可能需要额外配置MIME类型,确保.mp4文件以video/mp4类型提供。

5.2 性能优化与进阶技巧

  1. 视频预加载与分块加载:对于较长的视频,可以在场景加载初期就调用videoPlayer.Prepare(),让浏览器提前开始缓冲。对于超长视频,考虑将视频切片,分段加载和播放。
  2. 多视频源适配:编写一个简单的设备检测脚本,根据用户的网络带宽(通过Application.internetReachability粗略判断)或屏幕分辨率,动态切换不同码率的视频文件路径。
  3. 内存管理:视频纹理和音频数据占用内存不小。当视频播放完毕且不再需要时,主动调用videoPlayer.targetTexture.Release()并设置videoPlayer.targetTexture = null,以及videoPlayer.Stop()videoPlayer.url = null,帮助Unity和浏览器回收内存。
  4. 使用RenderTexture的注意事项RenderTexture的分辨率不一定要和视频原分辨率一致。你可以设置一个较小的RenderTexture(如960x540)来渲染一个1080p的视频,这能降低GPU的填充压力,提升性能,代价是画面被缩放到RenderTexture的大小。你需要根据RawImage的显示尺寸来权衡。
  5. 监听加载进度VideoPlayer本身不提供加载进度,但我们可以通过一个协程,在Prepare()之后,定期检查videoPlayer.framevideoPlayer.frameCount来估算。虽然不精确,但足以做一个简单的进度条。
// 在VideoPlayerManager中添加进度估算 public System.Action<float> OnLoadProgressUpdated; // 进度回调 (0.0 - 1.0) private IEnumerator EstimateLoadingProgress() { while (videoPlayer != null && videoPlayer.isPrepared == false) { // 在准备阶段,frameCount可能为0,这是一个粗略估计 if (videoPlayer.frameCount > 0) { float progress = (float)videoPlayer.frame / videoPlayer.frameCount; OnLoadProgressUpdated?.Invoke(progress); } else { // 可以基于时间或其他启发式方法估算 OnLoadProgressUpdated?.Invoke(0.5f); // 示例值 } yield return new WaitForSeconds(0.1f); } OnLoadProgressUpdated?.Invoke(1.0f); } // 在调用 videoPlayer.Prepare() 后,启动这个协程 StartCoroutine(EstimateLoadingProgress());

5.3 关于“Use Existing Build”模式下的资源丢失

你提供的热词中提到了“Use existing build模式下材质、mesh都丢失了”。这通常与Unity的资源打包和寻址系统(如Addressables或AssetBundles)有关,但与我们内嵌在StreamingAssets中的视频资源是两套机制。StreamingAssets是原始文件复制,不经过Unity的序列化打包。因此,只要你路径正确,在“Use Existing Build”模式下,视频文件本身不会丢失。如果出现其他材质丢失,那可能是场景中其他资源引用了未包含在构建中的AssetBundle或Addressables资源包,需要检查那些资源的打包和加载逻辑,与视频播放方案无关。

6. 完整示例与脚本整合

为了让这个攻略更具参考价值,我提供一个整合了基础播放、UI控制和简单进度显示的完整场景示例脚本。你可以创建一个新的VideoPlayerUIController脚本,挂载在Canvas上。

using UnityEngine; using UnityEngine.UI; public class VideoPlayerUIController : MonoBehaviour { public VideoPlayerManager videoManager; // 拖拽赋值 public Slider progressSlider; public Text timeText; public Button playButton; public Button pauseButton; public Text statusText; private bool isSeeking = false; void Start() { if (videoManager == null) videoManager = FindObjectOfType<VideoPlayerManager>(); // 绑定按钮事件 playButton.onClick.AddListener(() => videoManager.Play()); pauseButton.onClick.AddListener(() => videoManager.Pause()); // 进度条交互 progressSlider.onValueChanged.AddListener(OnSliderValueChanged); progressSlider.onPointerDown.AddListener(() => isSeeking = true); progressSlider.onPointerUp.AddListener(() => { isSeeking = false; videoManager.Seek((float)progressSlider.value * videoManager.GetVideoLength()); }); // 初始状态 UpdateButtonStates(false); statusText.text = "准备中..."; } void Update() { if (videoManager == null || !videoManager.IsPrepared) return; // 更新进度条(如果用户没有在拖动) if (!isSeeking && videoManager.IsPlaying) { float currentTime = videoManager.GetCurrentTime(); float totalTime = videoManager.GetVideoLength(); if (totalTime > 0) { progressSlider.value = currentTime / totalTime; timeText.text = $"{FormatTime(currentTime)} / {FormatTime(totalTime)}"; } } // 更新按钮状态 UpdateButtonStates(videoManager.IsPlaying); statusText.text = videoManager.IsPlaying ? "播放中" : "已暂停"; } private void UpdateButtonStates(bool isPlaying) { playButton.interactable = !isPlaying; pauseButton.interactable = isPlaying; } private void OnSliderValueChanged(float value) { if (isSeeking) { float totalTime = videoManager.GetVideoLength(); timeText.text = $"{FormatTime(value * totalTime)} / {FormatTime(totalTime)}"; } } private string FormatTime(float seconds) { int minutes = Mathf.FloorToInt(seconds / 60); int secs = Mathf.FloorToInt(seconds % 60); return $"{minutes:00}:{secs:00}"; } // 需要在VideoPlayerManager中暴露这些属性 // public bool IsPrepared => isPrepared; // public bool IsPlaying => videoPlayer != null && videoPlayer.isPlaying; // public float GetCurrentTime() => videoPlayer != null ? (float)videoPlayer.time : 0; // public float GetVideoLength() => videoPlayer != null ? (float)videoPlayer.length : 0; }

这个UI控制器提供了播放/暂停按钮、一个进度条(可拖动跳转)和时间显示。你需要相应地在VideoPlayerManager脚本中添加那几个公开的属性或方法。

最后,我想分享一个我自己的深刻体会:WebGL开发,尤其是多媒体部分,“在目标环境(浏览器)中尽早且频繁地测试”是最高原则。编辑器里的顺利运行只是一个美好的假象。从你完成第一个可播放的版本开始,就立刻把它扔到本地HTTP服务器上,然后扔到你的测试服务器上,用不同的电脑、不同的浏览器(Chrome, Firefox, Safari, Edge)、不同的设备(PC, 手机)去跑。你会发现的问题,90%都能通过本文提到的路径、格式、编码和自动播放策略来解决。剩下的10%,就需要你耐心地查看浏览器控制台的错误信息,一点点分析和调试。这个过程很磨人,但一旦跑通,这套稳定可靠的视频播放方案,将成为你未来所有WebGL项目的坚实基石。

返回列表