Unity WebGL实现本地视频播放:基于Blob URL与HTML5 File API的完整方案

📅 2026/7/23 6:37:20 👁️ 阅读次数 📝 编程学习
Unity WebGL实现本地视频播放:基于Blob URL与HTML5 File API的完整方案

1. 项目概述与核心挑战

最近在做一个Unity WebGL的互动展示项目,客户提了个挺实际的需求:希望用户能在网页里直接选择自己电脑或手机里的视频文件,然后无缝地在3D场景里播放出来。乍一听,这需求在桌面端或移动端原生应用里不算难事,一个文件选择对话框加上本地路径读取就搞定了。但一旦放到WebGL这个环境里,事情就变得微妙起来。

WebGL的本质是在浏览器沙箱里运行的一个“安全孤岛”。浏览器出于安全考虑,严格限制了网页脚本对用户本地文件系统的直接访问。你不能像在Windows或Android应用里那样,通过System.IO.File去读取一个C:\Users\...\video.mp4这样的路径。这直接堵死了传统Unity里用VideoPlayersource = VideoSource.Url并传入一个本地文件路径(file://协议)这条路。所以,这个需求的核心,其实是在WebGL的安全沙箱模型下,找到一种合规的、用户主动授权的方式,把本地视频文件“搬”到Unity应用里来。

为什么不用网络视频?因为很多展示场景需要用户上传自己的素材,比如房产VR看房里的户型介绍视频、教育培训里的个人作业视频、或者电商产品里的自定义展示内容。让用户自己选本地视频,体验更直接,也避免了先上传到服务器的繁琐步骤和隐私顾虑。

这个功能的技术核心,就是打通浏览器级别的文件选择APIUnity WebGL的VideoPlayer组件之间的桥梁。整个过程可以拆解为三步:第一步,通过HTML的<input type="file">元素让用户选择文件;第二步,将选中的文件对象转换为一个Unity能访问的临时URL(通常是blob:objectURL);第三步,将这个URL交给Unity的VideoPlayer进行播放。听起来流程清晰,但每一步都有不少坑等着你,从文件类型过滤、内存管理到跨语言交互,都需要仔细处理。

2. 技术方案选型与原理剖析

面对这个需求,我们有几个技术路径可以选,但最适合Unity WebGL的,目前来看只有一条主流且可靠的路。

2.1 为什么不能直接用本地文件路径?

这是首先要搞清楚的根本限制。在桌面平台(如Windows Standalone),你可以这样写:

videoPlayer.source = VideoSource.Url; videoPlayer.url = @"file:///C:/Users/MyVideo.mp4"; videoPlayer.Play();

这段代码在构建为.exe运行时是可行的。但在WebGL中,浏览器会阻止这种直接的文件系统访问。即使你通过某种方式获取到了完整的本地路径字符串,VideoPlayer在WebGL后端(通常是基于HTML5<video>标签)也无法识别和加载file://协议的内容,因为这会违反同源策略和安全模型。所以,直接传路径这个方案从一开始就被否决了。

2.2 备选方案评估

  1. Unity WebGL的本地文件系统API(Experimental):Unity提供了一个UnityEngine.Experimental.Networking命名空间下的DownloadHandlerFile类,以及一些文件系统访问的尝试,但这些功能在WebGL中要么不完整,要么仍是实验性的,并且主要面向的是“预加载到内存”或“缓存”的文件,而非用户动态选择的本地文件。可靠性不足,不推荐用于生产环境。

  2. 使用第三方JavaScript插件:Asset Store上确实有一些插件声称能处理WebGL下的本地文件。这些插件本质上也是封装了我们将要讨论的HTML5 File API和URL.createObjectURL方法。使用插件的好处是可能封装好了跨平台兼容性,但坏处是增加了项目依赖、可能带来额外的学习成本和潜在的更新维护问题。对于这个相对标准的功能,自己实现一次更能理解底层原理,也更有掌控力。

  3. 通过JavaScript桥接调用HTML5 File API(推荐方案):这是最直接、最符合Web标准、也是社区实践最广的方案。其核心原理是利用Unity WebGL与JavaScript互操作的能力(通过jslib插件或Application.ExternalCall/ExternalEval,但在较新版本中更推荐使用[DllImport("__Internal")]的方式)。让C#脚本调用一个我们自定义的JavaScript函数,这个函数在浏览器环境中创建一个隐藏的文件输入元素,监听其变化事件,获取用户选择的File对象,并生成一个指向该文件内存数据的临时URL(Blob URL),最后将这个URL回传给Unity。

方案选定理由:我们选择第三种方案。因为它:

  • 标准且未来兼容:基于W3C标准的HTML5 File API和URL API,浏览器支持度极高。
  • 无需额外依赖:不引入第三方插件,项目更纯净。
  • 理解底层机制:自己实现能透彻理解WebGL与浏览器交互的细节,便于后续排查问题和功能扩展。
  • 性能可控:生成的Blob URL直接由浏览器内核管理,VideoPlayer通过<video>标签加载,效率较高。

2.3 核心交互流程与数据流转

整个方案的执行流程,本质上是一次从浏览器到Unity,再回到浏览器的数据“旅行”:

  1. 用户触发:用户在Unity WebGL的画布内点击一个UI按钮(例如“选择视频”)。
  2. C#调用JS:该按钮绑定的C#脚本,通过[DllImport("__Internal")]调用一个我们注入的JavaScript函数,例如OpenFileDialog
  3. JS创建文件对话框:JavaScript函数动态创建一个<input type="file" accept="video/*">元素,并触发它的点击事件。这时,浏览器原生的文件选择窗口会弹出。
  4. 用户选择与JS处理:用户选择一个或多个视频文件后,JS监听onchange事件,获取到FileList对象。我们通常只取第一个文件(files[0])。
  5. 生成Blob URL:JS使用URL.createObjectURL(file),为这个File对象生成一个唯一的、临时的URL,格式如blob:https://yourdomain.com/550e8400-e29b-41d4-a716-446655440000。这个URL指向浏览器内存或临时存储空间中的文件数据。
  6. URL回传Unity:JS通过unityInstance.SendMessage方法,将这个Blob URL字符串发送回Unity场景中指定的GameObject和其上的C#脚本方法。
  7. Unity播放视频:C#脚本在接收到URL后,将其赋值给VideoPlayer组件的url属性,然后调用Play()。VideoPlayer的WebGL实现会利用这个Blob URL去设置HTML5<video>元素的src属性,从而开始加载和播放。

关键点理解URL.createObjectURL()创建的是一个指向原始FileBlob对象的引用URL。它比FileReader.readAsDataURL()(生成Base64字符串)更高效,因为Base64编码会使数据体积膨胀约33%,对于视频文件来说内存开销巨大。Blob URL是播放本地大文件的正确选择。

3. 完整实现步骤与代码详解

理论清楚了,我们开始动手实现。我会按照从JavaScript层到C#层的顺序,把每个文件、每段代码的作用和细节讲透。

3.1 第一步:创建JavaScript插件文件(.jslib)

在Unity项目的Assets文件夹下(通常是在Plugins子文件夹内,没有就新建一个),创建一个名为WebGLFileUploader.jslib的文件。这个文件将被Unity在构建WebGL时自动识别并包含。

// WebGLFileUploader.jslib mergeInto(LibraryManager.library, { // 打开文件选择对话框的函数 OpenFileDialog: function (gameObjectName, callbackFunctionName) { // 将Unity传过来的字符串指针转换为JS字符串 var goName = Pointer_stringify(gameObjectName); var cbName = Pointer_stringify(callbackFunctionName); // 1. 创建隐藏的file input元素 var fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'video/*'; // 限制只选择视频文件 fileInput.style.display = 'none'; // 隐藏不显示 // 2. 添加到body中(有些浏览器需要元素在DOM中才能触发点击) document.body.appendChild(fileInput); // 3. 监听文件选择变化事件 fileInput.addEventListener('change', function (event) { if (this.files && this.files.length > 0) { var selectedFile = this.files[0]; // 4. 检查文件类型(二次保险) if (!selectedFile.type.startsWith('video/')) { console.warn('Selected file is not a video:', selectedFile.type); // 可以回传一个错误信息给Unity unityInstance.SendMessage(goName, cbName, 'error:not_a_video'); cleanup(); return; } // 5. 为选中的文件创建Blob URL var blobUrl = URL.createObjectURL(selectedFile); console.log('Blob URL created:', blobUrl); // 6. 将Blob URL发送回Unity unityInstance.SendMessage(goName, cbName, blobUrl); // 7. 重要:清理DOM中的input元素 // 注意:不要在这里revokeObjectURL,因为VideoPlayer还在使用这个URL } else { // 用户取消了选择 unityInstance.SendMessage(goName, cbName, 'canceled'); } cleanup(); }); // 触发文件选择对话框 fileInput.click(); // 清理函数:移除DOM中的input元素 function cleanup() { if (fileInput.parentNode) { document.body.removeChild(fileInput); } } }, // 可选的:释放Blob URL的函数,用于手动管理内存 RevokeBlobUrl: function (blobUrlPtr) { var blobUrl = Pointer_stringify(blobUrlPtr); if (blobUrl && blobUrl.startsWith('blob:')) { URL.revokeObjectURL(blobUrl); console.log('Blob URL revoked:', blobUrl); } } });

代码关键点解析:

  • mergeInto(LibraryManager.library, {...}): 这是Unity WebGL插件的固定格式,用于将我们的函数注入到Unity生成的JavaScript库中。
  • Pointer_stringify(): 用于将C#传过来的字符串指针(IntPtrstring在交互时会自动转换)转换为JavaScript字符串。
  • accept = 'video/*': 这是一个用户体验优化。虽然不能100%防止用户选择非视频文件(因为文件类型判断基于扩展名),但可以让系统文件对话框默认过滤显示视频文件。
  • URL.createObjectURL(selectedFile): 核心步骤,生成临时URL。
  • unityInstance.SendMessage(): 这是从JavaScript环境回调Unity的标准方法。第一个参数是场景中GameObject的名字,第二个是该GameObject上挂载脚本的某个方法名,第三个是传递给该方法的字符串参数。
  • 内存管理提示:在事件回调中,我们移除了fileInput元素(cleanup函数),但没有立即调用URL.revokeObjectURL(blobUrl)。这是因为VideoPlayer正在使用这个URL进行播放。立即撤销会导致视频加载失败。正确的做法是在Unity端,当确定视频不再需要时(如播放完毕、切换视频、对象销毁时),再主动调用RevokeBlobUrl函数来释放内存。

3.2 第二步:编写C#脚本控制逻辑

接下来,在Unity中创建一个C#脚本,例如LocalVideoPlayer.cs,并将其挂载到一个空的GameObject上(比如就叫VideoManager)。

using UnityEngine; using UnityEngine.UI; // 如果要用UI Button using UnityEngine.Video; public class LocalVideoPlayer : MonoBehaviour { // 对外暴露的VideoPlayer组件引用,可以在Inspector中拖拽赋值 public VideoPlayer videoPlayer; // 可选:用于显示状态的UI Text public Text statusText; // 存储当前正在使用的Blob URL,用于后续清理 private string _currentBlobUrl = null; void Start() { // 安全检查 if (videoPlayer == null) { videoPlayer = GetComponent<VideoPlayer>(); if (videoPlayer == null) { Debug.LogError("VideoPlayer component not found! Please assign one."); return; } } // 确保VideoPlayer的source类型是Url videoPlayer.source = VideoSource.Url; UpdateStatus("Ready. Click to select a video."); } // 这个方法将被UI Button调用 public void OnSelectVideoButtonClicked() { #if UNITY_WEBGL && !UNITY_EDITOR // 只在WebGL平台下调用JS插件 OpenFileDialog(gameObject.name, "OnVideoFileSelected"); #else // 在编辑器或其他平台,可以使用不同的逻辑(例如打开系统文件对话框) // 这里简单提示 Debug.LogWarning("File selection via JS is only available in WebGL build."); UpdateStatus("Please run in WebGL build to select local files."); #endif } // 声明外部JS函数 [System.Runtime.InteropServices.DllImport("__Internal")] private static extern void OpenFileDialog(string gameObjectName, string callbackFuncName); [System.Runtime.InteropServices.DllImport("__Internal")] private static extern void RevokeBlobUrl(string blobUrl); // 这是从JavaScript回调的方法 public void OnVideoFileSelected(string urlOrMessage) { Debug.Log("Received from JS: " + urlOrMessage); if (urlOrMessage == "canceled") { UpdateStatus("Selection canceled."); return; } if (urlOrMessage.StartsWith("error:")) { UpdateStatus("Error: " + urlOrMessage); return; } // 如果之前有视频正在播放,先停止并释放旧的Blob URL if (videoPlayer.isPlaying) { videoPlayer.Stop(); } if (!string.IsNullOrEmpty(_currentBlobUrl)) { ReleaseBlobUrl(_currentBlobUrl); _currentBlobUrl = null; } // 新的URL应该是一个以"blob:"开头的有效URL if (!string.IsNullOrEmpty(urlOrMessage) && urlOrMessage.StartsWith("blob:")) { _currentBlobUrl = urlOrMessage; videoPlayer.url = _currentBlobUrl; UpdateStatus("Loading video..."); // 监听准备完成事件 videoPlayer.prepareCompleted += OnVideoPrepared; videoPlayer.errorReceived += OnVideoError; // 开始准备视频 videoPlayer.Prepare(); } else { UpdateStatus("Invalid URL received."); } } private void OnVideoPrepared(VideoPlayer source) { Debug.Log("Video prepared. Duration: " + source.length + " seconds."); UpdateStatus("Video loaded. Playing..."); source.prepareCompleted -= OnVideoPrepared; // 移除监听,避免重复 source.Play(); } private void OnVideoError(VideoPlayer source, string message) { Debug.LogError("VideoPlayer Error: " + message); UpdateStatus("Playback Error: " + message); source.errorReceived -= OnVideoError; // 发生错误时也释放URL if (!string.IsNullOrEmpty(_currentBlobUrl)) { ReleaseBlobUrl(_currentBlobUrl); _currentBlobUrl = null; } } private void UpdateStatus(string message) { if (statusText != null) statusText.text = "[Status] " + message; Debug.Log("[LocalVideoPlayer] " + message); } // 释放Blob URL关联的内存 private void ReleaseBlobUrl(string url) { #if UNITY_WEBGL && !UNITY_EDITOR RevokeBlobUrl(url); #else // 非WebGL平台无需处理 #endif } // 在对象销毁或视频不再需要时,确保清理资源 void OnDestroy() { if (videoPlayer != null) { videoPlayer.prepareCompleted -= OnVideoPrepared; videoPlayer.errorReceived -= OnVideoError; if (videoPlayer.isPlaying) videoPlayer.Stop(); } if (!string.IsNullOrEmpty(_currentBlobUrl)) { ReleaseBlobUrl(_currentBlobUrl); } } // 提供一个公共方法,用于在切换视频或UI操作时手动释放 public void CleanupCurrentVideo() { if (videoPlayer.isPlaying) videoPlayer.Stop(); if (!string.IsNullOrEmpty(_currentBlobUrl)) { ReleaseBlobUrl(_currentBlobUrl); _currentBlobUrl = null; videoPlayer.url = null; UpdateStatus("Video cleaned up."); } } }

C#脚本核心逻辑拆解:

  1. 平台编译指令#if UNITY_WEBGL && !UNITY_EDITOR至关重要。它确保调用JS插件的代码只在发布为WebGL时生效。在Unity Editor中运行时,这些代码会被跳过,避免编辑器环境下因找不到__Internal库而报错。我们可以在#else部分为编辑器编写模拟逻辑(比如使用UnityEditor.EditorUtility.OpenFilePanel),方便测试。

  2. DllImport声明[DllImport("__Internal")]是Unity用于调用自注入JavaScript函数的标记。声明的函数签名(名称、参数)必须与.jslib文件中的完全一致。

  3. 回调方法OnVideoFileSelected方法必须为public,因为它是通过SendMessage由JavaScript按名称反射调用的。其参数是一个string,对应JS端SendMessage的第三个参数。

  4. VideoPlayer事件监听:直接给videoPlayer.url赋值后立即调用Play()在WebGL中可能失败,因为视频数据还未加载。最佳实践是先调用Prepare(),并在prepareCompleted事件回调中开始播放。同时监听errorReceived事件以处理加载或解码失败的情况。

  5. 内存管理_currentBlobUrl变量用于跟踪当前使用的Blob URL。在加载新视频、发生错误或对象销毁时,调用ReleaseBlobUrl方法(内部调用JS的RevokeBlobUrl)来通知浏览器释放该URL占用的内存。这是一个好习惯,能避免内存泄漏,尤其是在用户频繁切换视频的场景下。

3.3 第三步:设置Unity场景与UI

  1. 在场景中创建一个GameObject,命名为VideoManager,将LocalVideoPlayer脚本挂载上去。
  2. 在同一个GameObject上或另一个合适的对象上,添加一个VideoPlayer组件。在LocalVideoPlayer脚本的Inspector面板中,将Video Player字段拖拽赋值。
  3. 创建一个UIButton,将其On Click()事件绑定到VideoManagerGameObject的LocalVideoPlayer.OnSelectVideoButtonClicked方法。
  4. (可选)创建一个UIText元素用于显示状态,并将其赋值给脚本的statusText字段。

3.4 第四步:构建与部署测试

  1. File -> Build Settings中,将平台切换到WebGL
  2. 点击Player Settings...,在Resolution and Presentation下,建议将Default Canvas Width/Height设置为你的目标分辨率。
  3. 关键设置:在Publishing Settings下,确保Compression FormatDisabledGzip。如果使用Brotli,部分老版本浏览器可能支持不佳。对于测试,Disabled最简单。
  4. 点击Build,选择一个输出文件夹。
  5. 构建完成后,你会得到一个包含.html.js.data等文件的文件夹。你不能直接双击.html文件来测试,因为file://协议下某些API(如URL.createObjectURL在某些上下文)可能受限。你需要通过一个HTTP服务器来运行。
    • 简单方法:如果你安装了Python,在构建输出文件夹下打开命令行,运行python -m http.server 8000(Python 3)或python -m SimpleHTTPServer 8000(Python 2),然后在浏览器访问http://localhost:8000
    • 其他工具:也可以用Node.js的http-server、或者VSCode的Live Server插件等。
  6. 在浏览器中打开页面,点击按钮,选择本地视频文件,观察视频是否能在Unity的VideoPlayer组件设定的Render Texture或目标Renderer上正常播放。

4. 进阶优化与实战避坑指南

基础功能跑通后,我们来看看如何让它更健壮、体验更好,以及我踩过的一些坑。

4.1 支持更多文件格式与格式兼容性

accept="video/*"是一个宽泛的过滤器。但不同浏览器对视频格式的支持差异巨大。VideoPlayer在WebGL后端依赖浏览器的HTML5<video>解码能力。常见的支持情况是:MP4(H.264编码 + AAC音频)几乎通用,WebM(VP8/VP9)在Chrome、Firefox中支持好,OGG/Theora则较少。

优化建议:

  1. 明确提示:在UI上提示用户推荐上传MP4格式(H.264)的视频。
  2. JS端细化过滤:可以修改accept属性,例如accept=".mp4,.webm,.ogg,video/mp4,video/webm,video/ogg",同时指定扩展名和MIME类型,提高过滤准确性。
  3. C#端格式检测:在OnVideoFileSelected中,可以根据文件URL或通过JS回传的文件名、类型信息,进行简单的格式检查并给出友好提示。
// 在JS的change事件中,可以回传更多信息 var fileInfo = { name: selectedFile.name, type: selectedFile.type, size: selectedFile.size, blobUrl: blobUrl }; // 需要将对象序列化为字符串回传,例如用JSON unityInstance.SendMessage(goName, cbName, JSON.stringify(fileInfo));
// C#端解析 [System.Serializable] // 需要这个属性来使用JsonUtility public class FileInfo { public string name; public string type; public long size; public string blobUrl; } public void OnVideoFileSelected(string fileInfoJson) { var fileInfo = JsonUtility.FromJson<FileInfo>(fileInfoJson); if (!fileInfo.type.Contains("mp4") && !fileInfo.type.Contains("webm")) { UpdateStatus($"Warning: {fileInfo.type} format may not be supported by all browsers. MP4 is recommended."); } // ... 使用fileInfo.blobUrl }

4.2 大文件处理与加载反馈

视频文件可能很大(几十MB甚至上百MB)。URL.createObjectURL是瞬间完成的,它只是创建了一个引用。但后续VideoPlayer在Prepare和播放时,浏览器才会开始读取文件数据并解码。

  • 加载进度:原生的VideoPlayer在WebGL下没有提供直接的加载进度事件。一个变通方法是,你可以通过JS监听<video>元素的progress事件,然后通过SendMessage周期性回传加载的字节范围,在Unity端模拟一个进度条。但这需要更复杂的JS-C#交互,将JS端的<video>元素与Unity的VideoPlayer实例更紧密地绑定,实现成本较高。对于大多数情况,显示一个“加载中...”的旋转图标或提示语就足够了。
  • 内存与性能:Blob URL引用的文件数据通常存储在内存中。播放一个非常大的视频会占用大量内存。要提醒用户,或者考虑在服务器支持下进行分片上传和流式播放(这就超出本地播放范畴了)。

4.3 移动端适配与用户体验

在手机浏览器上,这个方案同样有效,但体验略有不同:

  • 文件选择:点击<input type="file">会触发移动系统的文件选择器,用户可以从相册、文件管理App中选择。
  • 播放控制:移动端浏览器通常对视频播放有更严格的策略,比如自动播放限制。在iOS Safari上,视频必须有muted属性或者用户有手势交互(如点击)后才能播放声音。你需要确保VideoPlayer的audioOutputMode设置正确,并且播放指令(Play())是在一个用户触发的回调(如按钮点击事件)中发出的。
// 可以在准备完成后,在一个用户触发的回调(如另一个“播放”按钮)中开始播放 // 或者,确保VideoPlayer的Play()是在OnSelectVideoButtonClicked这类直接由用户点击触发的事件链中调用。 private void OnVideoPrepared(VideoPlayer source) { // 不在这里自动Play,而是等待用户点击一个“播放”按钮 UpdateStatus("Video loaded. Tap Play button."); _isPrepared = true; } // 单独的播放按钮事件 public void OnUserRequestedPlay() { if (_isPrepared && videoPlayer != null) { videoPlayer.Play(); } }

4.4 常见问题与排查清单

  1. 点击按钮没反应,浏览器控制台没有错误

    • 检查:确保构建平台是WebGL,并且脚本中的#if UNITY_WEBGL && !UNITY_EDITOR编译指令正确。在编辑器里运行时,按钮点击会执行#else部分的代码。
    • 检查.jslib文件是否放在了Assets/Plugins(或其子目录)下?构建后可以查看生成的.html文件,搜索OpenFileDialog函数名,看是否被包含。
  2. 能弹出文件对话框,但选择视频后Unity没反应,JS控制台有Blob URL日志

    • 检查:C#回调函数OnVideoFileSelected的名字是否与JS中SendMessage调用的第二个参数完全一致(大小写敏感)。
    • 检查:接收回调的GameObject名称是否与JS中SendMessage的第一个参数一致。建议在C#中直接传递gameObject.name
    • 检查:浏览器控制台是否有CORS(跨域)错误?本地文件通过HTTP服务器(如localhost)访问,Blob URL是同源的,不应该有CORS问题。如果是从file://协议直接打开页面,则可能失败。
  3. 视频能加载,但一直黑屏或卡在第一帧

    • 检查:VideoPlayer的Render Mode设置。如果是Render Texture,确保你指定了一个有效的Render Texture,并且有一个RawImage或Material使用了它。如果是Camera Near PlaneCamera Far Plane,确保目标Camera正确。
    • 检查:视频编码。尝试一个标准的MP4(H.264 + AAC)文件。用FFmpeg等工具检查或转换视频编码:ffmpeg -i input.mp4 -c:v libx264 -profile:v high -level 4.2 -c:a aac output.mp4
    • 检查:浏览器控制台(Network标签)查看视频请求是否成功(状态码206或200)。查看Console标签是否有解码错误。
  4. 有画面没声音

    • 检查:VideoPlayer的Audio Output Mode。对于WebGL,通常使用Audio Source模式,并需要将一个AudioSource组件拖拽到VideoPlayer的Audio Source属性上。确保这个AudioSource的Mute未勾选,音量合适。
    • 检查:移动端自动播放策略。确保第一次播放是由用户手势触发的。
  5. 切换视频或重复选择时,内存持续增长

    • 检查:是否在加载新视频前或对象销毁时,正确调用了ReleaseBlobUrl(内部调用JS的RevokeBlobUrl)来释放旧的Blob URL。每个未被释放的Blob URL都会占用内存。
  6. 在iOS Safari上无法播放

    • 这是最常见也最棘手的问题之一。Safari对视频格式和编码要求极其严格。
    • 确保视频编码是H.264 Baseline/Main/High Profile,级别不超过4.0或4.2。过高的级别(如5.1)可能不被支持。
    • 确保MP4文件是“快速启动”(Fast Start)的。这意味着元数据(moov atom)位于文件开头,而不是结尾。使用FFmpeg修复:ffmpeg -i input.mp4 -movflags faststart -c copy output.mp4
    • 音频编码必须是AAC。MP3音频在Safari的<video>标签中可能无法播放。
    • 测试,测试,再测试。准备一个专门针对Safari转码过的视频文件进行测试。

4.5 性能与内存管理心得

  • 及时释放URL.revokeObjectURL是你的好朋友。只要确定VideoPlayer不再需要某个Blob URL(播放结束、切换视频、页面关闭),就立刻调用它。可以将释放逻辑放在VideoPlayer.loopPointReached事件、或你自己的停止/切换函数中。
  • 限制并发:不要同时创建多个文件选择对话框或加载多个大视频。管理好状态,确保同一时间只有一个加载流程。
  • Fallback方案:如果你的应用必须支持所有格式,考虑集成一个纯JavaScript的播放器(如video.js)作为备选,当检测到Unity VideoPlayer无法播放时,隐藏Unity画布,显示一个HTML的<video>元素来播放。这需要更复杂的架构,但能提升兼容性。

5. 总结与扩展思路

通过上述步骤,我们成功地在Unity WebGL项目中实现了用户自主选择并播放本地视频的功能。这套方案的核心在于理解并尊重浏览器的安全模型,利用<input type="file">获取用户授权,通过URL.createObjectURL搭建起本地文件数据与WebGL中VideoPlayer组件之间的桥梁。

我个人在实际项目中的体会是,这个功能的稳定性90%取决于视频文件本身。在开发阶段,务必准备一个“黄金样本”视频——用标准编码器生成的、符合所有浏览器要求的MP4文件(H.264 High Profile @ Level 4.2, AAC音频,Fast Start)。用它来测试功能流程。一旦流程通了,大部分用户问题都可以归结为视频格式问题,这时你就可以给出一份清晰的“视频格式要求说明”给用户或内容制作人员。

扩展一下,这个模式不仅可以用于视频,稍加修改就能用于播放本地音频(AudioSource配合accept="audio/*"),甚至读取本地图片文件(使用FileReader.readAsDataURL得到Base64字符串,然后赋值给Texture2D.LoadImage)或文本文件(FileReader.readAsText)。其JS-C#交互的框架是通用的。

最后,记得在真机上,尤其是iOS和Android的各种主流浏览器上进行充分测试。WebGL应用的魅力在于跨平台,但麻烦也在于此,每个平台的浏览器都有自己小小的“个性”,提前发现并适配这些差异,是让项目顺利上线的关键。