
简介本资源是一套面向Unity3D开发者尤其聚焦WebGL部署场景的监控视频流快速播放解决方案工程源码专为解决Universal Media Player插件在萤石云ezopen协议下启动慢数十秒、WebGL平台无法播放等生产级痛点而设计。方案采用网页嵌入式架构通过3D WebView等浏览器插件在Unity中加载H5页面播放视频流实测启动速度提升2–3倍兼顾兼容性与响应效率。压缩包含1079个文件主体为138个C#脚本核心逻辑与协议对接、125个PNG/UI资源、55个JS前端播放控制脚本、56个pak资源包及22个Unity场景资产总大小115.38MBProjectSettings等基础配置文件完整开箱即用于Unity 2020.3.28f1c1环境。已有1108人学习下载提供从appKey鉴权、ezopen URL生成、WebGL跨域适配到嵌入式浏览器调用的全链路实现含可直接运行的演示场景与结构化工程目录适合中高级Unity开发者快速集成安防监控类交互功能。 接了那么多Unity项目我基本承认一个事实只要目标是WebGL视频播放就没有省心的路。尤其当视频源是萤石云这类监控摄像头RTSP、RTMP、HLS三套协议摆在那里UMPUniversal Media Player虽然能打但每次构建完浏览器一开就是白屏或者黑屏调半天又发现是解码器的问题这种折腾次数多了自然就想找个能替代它的稳定方案。这篇文章把我在一个实际项目里跑通的方案完整写出来C#侧通过萤石云OpenAPI拿AccessToken和播放地址WebGL侧用iframe嵌入H5播放器承载页整个链路不依赖UMP不需要自己写解码器构建出来的包体也更小。内容适合正被WebGL监控视频流折磨的Unity开发者也适合刚接触萤石云接入、想弄明白浏览器视频播放约束的朋友。1. 为什么UMP在WebGL上撑不住先谈谈这几条硬约束1.1 UMP在WebGL平台的实际表现UMP全称Universal Media Player是Unity商店里很常用的视频播放方案桌面端表现确实不错RTSP、RTMP、本地文件、网络流都能接。但到了WebGL平台事情就变得微妙了。先说结论UMP在WebGL上不是完全不能用而是性价比太低。我实测的情况是同样的HLS流在Windows桌面构建下能正常播放换成WebGL构建后Chrome里偶尔能出画面但经常是在画面出来之前黑屏几十秒Safari里更是直接报解码错误。更麻烦的是UMP的WebGL版本依赖WebAssembly解码器构建出来的包体会膨胀几MB加载时间明显变长。如果你是个人开发者或者小团队还要面对许可证成本的问题。UMP是商业插件不管项目是内部工具还是对外交付授权费用都是实打实的开销。替代UMP最直接的好处就是省掉这一块同时把视频解码的包袱从Unity里扔出去。1.2 浏览器给WebGL视频播放加的三道锁理解了WebGL为什么难做视频播放就能明白替代UMP不是换一个插件那么简单。浏览器在安全模型上给视频播放加了三道硬锁第一道锁浏览器不能直接播放RTSP和RTMP。RTSP走的是TCP 554端口RTMP走的是专门的流媒体协议这两种协议都不在HTTP(S)的范畴里浏览器原生不支持。如果你想在WebGL里播放RTSP流必须先通过后端转成HLS或者WebRTC再喂给浏览器。第二道锁HLSm3u8不是所有浏览器都能原生播放。Safari对HLS的支持是原生的Chrome、Edge、Firefox则需要借助hls.js这类库走MediaSource Extensions接口把HLS重新封装成MP4片段。这个差异直接决定了很多Unity视频插件在WebGL上的体验分裂同一个视频Safari能放Chrome放不了用户一换浏览器就来找你。第三道锁跨域和HTTPS限制。Unity WebGL构建出来的页面如果是HTTPS那么页面里引用的所有视频、接口、iframe资源都必须是HTTPS否则浏览器会直接拦截。视频流地址如果带了跨域问题CORS策略又会把画面卡死。这三道锁叠加在一起导致任何Unity视频插件在WebGL上都注定是“带着镣铐跳舞”。真正稳的方案不是去解决解码问题而是绕开它。1.3 萤石云视频流本身的特殊性萤石云和普通视频网站还不一样它对外提供的流地址不是静态的。萤石云开放平台支持RTMP、HLS、RTSP三种取流协议其中RTMP和RTSP在WebGL上直接不可用只剩HLS一条路。但HLS地址又不能自己拼需要通过OpenAPI动态获取并且返回的地址是带token的有时效性过期后必须重新请求。这一点很关键很多人代码写完了跑了几小时突然黑屏排查半天才发现是地址过期了。另外萤石云设备本身有序列号deviceSerial、通道号channelNo、验证码这些概念。比如你用的是DS-7808N-F1这类NVR设备序列号是设备机身上的标签不是型号名称。通道号对应的是接在NVR上的第几路摄像头。这些参数在调用OpenAPI时一个都不能错。2. 拿萤石云播放地址前先把这三件事准备好2.1 开放平台创建应用拿到AppKey和AppSecret萤石云的接入流程分两步账号体系和设备体系。先在萤石云开放平台注册开发者账号然后在控制台创建一个应用。创建成功后你会得到一对AppKey和AppSecret这两个字符串相当于你在萤石云生态里的身份凭证后续所有OpenAPI调用都靠它们来换取AccessToken。如果你是个人测试设备管理里需要先把你的监控设备绑定到账号下。NVR设备和摄像头一般用序列号加验证码的方式添加验证码就是设备底部标签上的六位大写字母。添加成功后在设备列表里能看到对应的deviceSerial和channelNo。这一步最容易踩的坑是同一个设备可能同时绑定在萤石云App的普通账号和开放平台的开发者账号下而开放平台只认它自己账号体系里的设备。如果设备没有在开放平台控制台的“设备管理”里看到就算AppKey和AppSecret正确后面所有接口都会报“设备不存在”。2.2 OpenAPI换取AccessToken的请求细节萤石云的OpenAPI采用POST表单请求返回JSON。换取AccessToken的接口路径是/api/lapp/token/get请求体带两个字段appKey和appSecret。我建议直接在Unity里用UnityWebRequest封装这样打包到WebGL时不需要额外引入网络库。代码逻辑就三步拼表单、发请求、解析结果。using System; using System.Collections; using UnityEngine; using UnityEngine.Networking; [Serializable] public class YsTokenData { public string accessToken; public long expireTime; } [Serializable] public class YsTokenResponse { public string code; public string msg; public YsTokenData data; } public class Ys7TokenManager { public string AccessToken { get; private set; } public long ExpireTime { get; private set; } public IEnumerator RequestToken(string appKey, string appSecret) { WWWForm form new WWWForm(); form.AddField(appKey, appKey); form.AddField(appSecret, appSecret); using (UnityWebRequest request UnityWebRequest.Post(https://open.ys7.com/api/lapp/token/get, form)) { request.timeout 10; yield return request.SendWebRequest(); if (request.result ! UnityWebRequest.Result.Success) { Debug.LogError(Token请求失败: request.error); yield break; } YsTokenResponse response JsonUtility.FromJsonYsTokenResponse(request.downloadHandler.text); if (response.code ! 200) { Debug.LogError(Token接口返回错误: response.msg); yield break; } AccessToken response.data.accessToken; ExpireTime response.data.expireTime; Debug.Log(AccessToken获取成功 expireTime ExpireTime); } } }这里有个小细节萤石云返回的code是字符串200而不是数字200用JsonUtility解析时要注意字段类型。另外AccessToken的有效期通常是7天建议首次获取后保存到本地比如PlayerPrefs下次启动先判断是否过期过期才重新请求避免每次打开页面都白等一次网络请求。2.3 获取HLS直播地址参数、返回值与有效期拿到AccessToken之后下一步就是获取具体的播放地址。直播地址的接口路径是/api/lapp/live/address/get请求字段有四个accessToken上一步拿到的令牌deviceSerial设备序列号channelNo通道号从1开始protocol2表示HLS这是WebGL平台可选的协议quality清晰度1流畅、2标清、3高清按需传using System; using System.Collections; using UnityEngine; using UnityEngine.Networking; [Serializable] public class YsLiveAddressData { public string id; public string url; public string hls; public string hlsHd; public string rtmp; public string rtmpHd; public string rtsp; } [Serializable] public class YsLiveAddressResponse { public string code; public string msg; public YsLiveAddressData data; } public class Ys7LiveAddressManager { public string AccessToken; public string DeviceSerial; public int ChannelNo 1; public IEnumerator RequestHlsAddress(Actionstring onSuccess, Actionstring onError) { WWWForm form new WWWForm(); form.AddField(accessToken, AccessToken); form.AddField(deviceSerial, DeviceSerial); form.AddField(channelNo, ChannelNo); form.AddField(protocol, 2); form.AddField(quality, 3); using (UnityWebRequest request UnityWebRequest.Post(https://open.ys7.com/api/lapp/live/address/get, form)) { request.timeout 10; yield return request.SendWebRequest(); if (request.result ! UnityWebRequest.Result.Success) { onError?.Invoke(request.error); yield break; } YsLiveAddressResponse response JsonUtility.FromJsonYsLiveAddressResponse(request.downloadHandler.text); if (response.code ! 200) { onError?.Invoke(response.msg); yield break; } string hlsUrl !string.IsNullOrEmpty(response.data.hlsHd) ? response.data.hlsHd : response.data.hls; Debug.Log(HLS地址: hlsUrl); onSuccess?.Invoke(hlsUrl); } } }返回的data字段里hls是标清HLS地址hlsHd是高清HLS地址rtmp/rtsp就是当前WebGL平台上用不到的协议。这里我建议优先取hlsHd画面的清晰度会好一些但也要看设备的码流上限4G网络下反而可能更卡。HLS地址的有效期一般是1小时左右过期之后播放器会报错或者黑屏。所以不要把地址缓存起来长期使用每次打开监控画面时都重新请求一次才保险。3. 方案对比iframe嵌入、hls.js纹理映射与VideoPlayer直连我为什么选iframe3.1 方案Aiframe嵌入萤石H5播放器承载页iframe方案的思路很直接自己写一个HTML页面里面用萤石云官方提供的H5播放器SDKEZUIKit播放视频然后把Unity WebGL页面里再嵌一个iframesrc指向这个页面。这个方案的优点是彻底绕开了Unity的视频解码问题。浏览器原生能播的视频iframe就能显示浏览器不能播的SDK内部会自己做降级处理和你Unity工程没有关系。构建出来的包体也不会因为解码器变大。缺点也一样明显iframe是“浮”在Unity画面上层的不是Unity场景里的3D纹理。如果你的项目只需要在UI面板或者全屏区域显示监控画面这个缺点无所谓但如果你非要把画面贴到3D大屏模型上这个方案就不成立了。3.2 方案Bhls.js jslib实时解码再映射纹理这个方案在技术圈里讨论得比较多思路是在浏览器环境里用hls.js把HLS流实时解码渲染到一个canvas或video标签上然后通过Unity WebGL的纹理接口把画面同步到3D物体的材质上。听起来很“Unity”但工程复杂度极高。hls.js的解码能力依赖浏览器本身的MediaSource能力Chrome和Safari的表现不一致画面上传到Unity纹理的过程又涉及WebGL纹理绑定和跨域污染问题实测720p的画面已经明显掉帧1080p基本卡成PPT。我自己的经验是除非产品经理硬性要求“画面必须显示在3D模型表面”否则不要碰这个方案。为了一个边角需求投入的人力成本足够把整个项目其他模块做完。3.3 方案CUnity VideoPlayer直连m3u8这个方案听上去最省事VideoPlayer的url直接填萤石云返回的m3u8地址什么都不用改。但实测下来VideoPlayer在WebGL平台对m3u8的支持非常有限。Unity WebGL的VideoPlayer底层是浏览器video标签而Chrome、Edge这类浏览器默认不原生支持HLS。也就是说你在Editor编辑器里测试时一切正常发布到浏览器就黑屏。如果你不想搭承载页、不想写jslib短时间内可以用这个方案做快速验证但别把它当正式交付方案。3.4 三个方案的取舍参照表方案实现难度包体影响支持3D贴图稳定性适用场景iframe承载页低无影响不支持高UI面板展示、全屏监控hls.js纹理映射极高增加少量JS库支持低3D大屏、模型贴图VideoPlayer直连m3u8低无影响支持低临时验证、Safari环境最终我选了iframe承载页。理由就一条稳定优先。视频监控的核心是“关键时刻能看到画面”为了这个目标Unity层做一点妥协完全值得。4. 工程落地从C#取流到WebGL构建的完整代码4.1 工程结构与依赖准备整个工程需要四个文件协作Assets/Plugins/WebGL/Ys7WebGL.jslibWebGL平台的JS互操作层Assets/Scripts/Ys7ApiClient.cs萤石云OpenAPI的C#封装Assets/Scripts/Ys7PlayerBridge.csUnity侧调用jslib的平台判断封装WebGLBuild/play.html萤石云H5播放器承载页构建后放在和index.html同一目录这里我说明一下jslib文件是Unity WebGL平台特有的JS互操作机制放在Assets/Plugins/WebGL目录下Unity会自动把它编译进构建产物。C#侧通过DllImport声明对应的函数名就能直接调用JS代码不需要额外的SDK。4.2 C#封装AccessToken与播放地址请求网络请求部分前面已经给出核心代码。在实际工程里我一般会把“获取Token”和“获取播放地址”串成一个协程按钮点击后依次执行避免用户等待时出现空状态。using System.Collections; using UnityEngine; using UnityEngine.UI; public class Ys7Demo : MonoBehaviour { public string appKey 你的AppKey; public string appSecret 你的AppSecret; public string deviceSerial 设备序列号; public int channelNo 1; public Button openButton; private Ys7TokenManager tokenManager new Ys7TokenManager(); private Ys7LiveAddressManager addressManager new Ys7LiveAddressManager(); private void Start() { openButton.onClick.AddListener(OnOpenClicked); } public void OnOpenClicked() { StartCoroutine(OpenVideoFlow()); } private IEnumerator OpenVideoFlow() { string token PlayerPrefs.GetString(ys7_access_token, ); if (string.IsNullOrEmpty(token)) { yield return tokenManager.RequestToken(appKey, appSecret); token tokenManager.AccessToken; if (string.IsNullOrEmpty(token)) yield break; PlayerPrefs.SetString(ys7_access_token, token); PlayerPrefs.Save(); } addressManager.AccessToken token; addressManager.DeviceSerial deviceSerial; addressManager.ChannelNo channelNo; string hlsUrl null; yield return addressManager.RequestHlsAddress( url { hlsUrl url; }, error { Debug.LogError(error); } ); if (!string.IsNullOrEmpty(hlsUrl)) { Ys7PlayerBridge.OpenPlayer(hlsUrl, deviceSerial, 0, 0, Screen.width, Screen.height); } } private void OnDisable() { Ys7PlayerBridge.ClosePlayer(); } }有一个地方要特别提醒Unity WebGL环境下协程里的网络请求是异步进行的不会卡主线程。但你在编辑器里调试时萤石云的接口是走外网的第一次请求可能需要几秒不要急着点按钮。4.3 承载页HTML的写法关键承载页是iframe方案的核心它的作用是代替Unity去和萤石云播放器SDK打交道。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title萤石云视频承载页/title link relstylesheet hrefhttps://open.ys7.com/sdk/js/1.2/ezuikit.css style html, body { margin: 0; padding: 0; width: 100%; height: 100%; background: #000; overflow: hidden; } #player { width: 100%; height: 100%; } .mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #000; color: #fff; font-size: 18px; cursor: pointer; z-index: 10; } /style /head body div idplayer/div div classmask idmask点击开启监控/div script typetext/javascript srchttps://open.ys7.com/sdk/js/1.2/ezuikit.js/script script var query new URLSearchParams(window.location.search); var accessToken query.get(accessToken); var deviceSerial query.get(deviceSerial); var channelNo query.get(channelNo); var mask document.getElementById(mask); var player null; function createPlayer() { if (!accessToken || !deviceSerial) { mask.textContent 参数不完整请检查链接; return; } mask.style.display none; player new EZUIKit.Player({ id: player, accessToken: accessToken, url: ezopen://open.ys7.com/ deviceSerial / channelNo .live, autoplay: true, audio: 1 }); } mask.addEventListener(click, function () { createPlayer(); }); // 如果浏览器允许自动播放则直接创建播放器 if (navigator.userActivation navigator.userActivation.isActive) { createPlayer(); } /script /body /html这个页面的设计有两个细节第一播放器不是页面一加载就创建而是等用户点击后才创建。原因后面第5章会详细讲这是因为浏览器的自动播放策略限制了video元素在无用户交互时不能带声音播放。加一个“点击开启监控”遮罩是万无一失的做法。第二URL参数用的是URLSearchParams解析这是浏览器原生API兼容性没问题。承载页部署时和Unity构建产物放在同一个静态目录下这样iframe的src就可以写相对路径避免跨域。4.4 jslib互操作层动态打开和关闭视频jslib文件是Unity WebGL调用原生JS的唯一通道。我这里实现两个函数打开播放器、关闭播放器。mergeInto(LibraryManager.library, { Ys7OpenPlayer: function (urlPtr, deviceNamePtr, left, top, width, height) { var url UTF8ToString(urlPtr); var deviceName UTF8ToString(deviceNamePtr); var containerId ys7_video_container; var oldContainer document.getElementById(containerId); if (oldContainer) { oldContainer.parentNode.removeChild(oldContainer); } var container document.createElement(div); container.id containerId; container.style.position fixed; container.style.left left px; container.style.top top px; container.style.width width px; container.style.height height px; container.style.zIndex 9999; container.style.backgroundColor #000; container.style.overflow hidden; var iframe document.createElement(iframe); iframe.src url; iframe.style.width 100%; iframe.style.height 100%; iframe.style.border none; iframe.setAttribute(allow, autoplay; encrypted-media); container.appendChild(iframe); document.body.appendChild(container); }, Ys7ClosePlayer: function () { var container document.getElementById(ys7_video_container); if (container) { container.parentNode.removeChild(container); } } });C#侧的封装长这样using System.Runtime.InteropServices; using UnityEngine; public static class Ys7PlayerBridge { #if UNITY_WEBGL !UNITY_EDITOR [DllImport(__Internal)] private static extern void Ys7OpenPlayer(string url, string deviceName, int left, int top, int width, int height); [DllImport(__Internal)] private static extern void Ys7ClosePlayer(); #endif public static void OpenPlayer(string url, string deviceName, int left, int top, int width, int height) { #if UNITY_WEBGL !UNITY_EDITOR Ys7OpenPlayer(url, deviceName, left, top, width, height); #else Debug.Log([WebGL] 编辑器模式模拟打开播放器: url); #endif } public static void ClosePlayer() { #if UNITY_WEBGL !UNITY_EDITOR Ys7ClosePlayer(); #else Debug.Log([WebGL] 编辑器模式模拟关闭播放器); #endif } }这里最关键的是#if UNITY_WEBGL !UNITY_EDITOR这个判断。在编辑器里直接调用DllImport声明的方法会报错因为编辑器环境没有浏览器执行环境。加上这个条件编译之后编辑器里只会打日志方便调试。4.5 构建选项与浏览器兼容性检查WebGL构建时的Player Settings有几个重要选项配置不对会影响后面运行。Compression Format建议选Brotli压缩率比Gzip高但要注意服务器需要支持Content-Encoding: br响应头否则浏览器会解密失败。建议勾选Decompression Fallback兼容性更好。如果项目里有中文UI记得在Player Settings里把Active Input Handling改成Input System包取决于你的项目实际否则WebGL输入会有问题。部署方面Unity WebGL构建出来的页面必须通过HTTP(S)协议访问不能直接用file://协议打开否则会碰到跨域和WebAssembly加载问题。生产环境建议用Nginx做静态文件服务器或者用云平台的对象存储托管本文还有配套的精品资源点击获取