ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity WebGL播放萤石云视频流:UMP替代方案与jslib实践

Unity WebGL播放萤石云视频流:UMP替代方案与jslib实践 简介浏览器作为WebAssembly的运行时受沙箱机制限制无法直接访问操作系统底层多媒体框架和原始Socket导致RTSP等协议在WebGL环境下寸步难行。技术原理上Web端视频播放需依赖浏览器原生的video标签及HLS、FLV等封装协议通过JavaScript API实现拉流与解码。理解这一原理后工程实践便可借助jslib桥接层让Unity3D调用浏览器自身的HLS播放能力从而绕开UMP插件在WebGL平台上的失效问题。这种方案尤其适合萤石云监控大屏项目可在不引入额外转码服务的前提下实现稳定流畅的直播画面嵌入。本文从流媒体协议选型、萤石云API对接、jslib播放器封装到UI坐标同步完整呈现一套可直接落地的Unity WebGL视频流播放方案。对于正在被UMP折磨的开发者这套思路或许能帮你少走两天弯路。 先说结论如果你的Unity3d项目要在WebGL平台播放萤石云监控视频流UMP这个插件基本可以放弃了。我在一个用C#做的Web监控端项目里试了两天UMPBuild到浏览器后视频控件黑屏、日志报错折腾一圈才搞明白不是UMP不努力是浏览器压根不给RTSP活路。换思路之后用jslib让Unity调用浏览器自己的播放器直接打开萤石云的HLS地址反而又快又稳。这篇文章适合正在做Unity WebGL监控大屏、或者是被UMP在WebGL下坑到怀疑人生的朋友。我会把从方案选型、萤石云API对接到jslib播放器封装、画面位置同步的完整工程代码都讲一遍所有代码都是我实际跑通过的。如果你只需要一个能用的Unity WebGL视频流播放方案照着抄就行。1. 为什么要替代UMPWebGL的流媒体困局1.1 UMP在WebGL下失效的底层原因UMP的全称是Unity Media Player在Windows和Android上拉RTSP流确实很强我自己之前好几个项目都用它。但问题在于UMP的核心是调用操作系统底层的多媒体框架和网络协议栈比如Windows上的DirectShow、Media FoundationAndroid上的MediaPlayer。这些在原生平台跑没问题可Unity WebGL构建出来的东西本质是WebAssembly跑在浏览器的沙箱里根本没有这些操作系统API。更致命的是RTSP协议本身。RTSP走的是TCP或UDP原始Socket而浏览器出于安全考虑压根没给网页暴露原始Socket的能力只有WebSocket可以用。也就是说哪怕你找到了一个支持WebGL的播放插件只要它试图直接连RTSP地址浏览器这关就过不了。我刚开始以为是UMP配置问题反复换版本、调参数后来在Unity官方文档里看到一行字WebGL平台不支持原生插件和Socket相关功能这才彻底死心。1.2 替代方案的选型对比HLS、HTTP-FLV、WebRTC与JSMPEG既然UMP不行那就得换播放协议和技术栈。做WebGL视频流本质上就是把播放这件事交给浏览器Unity只负责界面和逻辑。我列了几个主流方案的对比方案延迟浏览器兼容性服务端要求实现复杂度HLS hls.js3-10秒Chrome/Firefox/Edge需要hls.jsSafari原生支持服务器支持切片和m3u8索引低HTTP-FLV flv.js1-3秒Chrome/Firefox/Edge支持Safari需MSE支持服务器支持HTTP-FLV分发低WebSocket jsmpeg0.5-1秒所有浏览器需要自建ffmpeg转码推流服务高WebRTC0.5秒以内所有现代浏览器需要信令服务器和流媒体服务高选择标准很简单萤石云开放平台直接就能拿到HLS和FLV地址不需要自己搭转码服务所以WebRTC和jsmpeg方案虽然延迟低但工程成本高对大多数项目来说没必要。我最终选择了HLS为主、FLV为辅的路线主要原因是HLS在浏览器端的兼容性最好hls.js库也比较成熟出错率低。注意如果你们对延迟有硬性要求比如控制在1秒以内那就得走WebRTC或jsmpeg这时候需要在服务器端做RTSP转WebRTC转发这个以后有机会单独写一篇。2. 从萤石云拉取可播放的直播地址2.1 开放平台接入与accessToken获取要播放萤石云监控流第一步是去萤石云开放平台注册开发者账号创建应用后拿到AppKey和AppSecret。这一步没啥技术含量但有个坑设备必须先在萤石云上激活也就是用萤石云App扫码添加设备否则开放平台API是拿不到设备信息的。拿到AppKey和AppSecret之后用C#发起POST请求换取accessToken。萤石云的接口地址是https://open.ys7.com/api/lapp/token/get参数就是appKey和appSecret。我在Unity里用UnityWebRequest封装了一个方法using System.Collections; using UnityEngine; using UnityEngine.Networking; public static class YSCloudApi { public static IEnumerator GetAccessToken(string appKey, string appSecret, System.Actionstring onSuccess, System.Actionstring onError) { var form new WWWForm(); form.AddField(appKey, appKey); form.AddField(appSecret, appSecret); using (var request UnityWebRequest.Post(https://open.ys7.com/api/lapp/token/get, form)) { yield return request.SendWebRequest(); if (request.result UnityWebRequest.Result.Success) { var result JsonUtility.FromJsonYSResultYSTokenData(request.downloadHandler.text); if (result.code 200) onSuccess?.Invoke(result.data.accessToken); else onError?.Invoke(result.msg); } else { onError?.Invoke(request.error); } } } } [System.Serializable] public class YSResultT { public string code; public string msg; public T data; } [System.Serializable] public class YSTokenData { public string accessToken; public long expireTime; }这里有个细节萤石云的accessToken有效期一般是7天接口每次调用都会返回新的token。我在项目里做了缓存token过期前不重复请求。缓存文件用Path.Combine(Application.persistentDataPath, ys_token.json)存下来避免每次启动都白等一次网络请求。2.2 获取设备列表与直播地址拿到accessToken之后先查设备列表。接口是https://open.ys7.com/api/lapp/device/list参数除了accessToken还要有pageStart和pageSize。返回的数据里最关键的是deviceSerial和channelNo这两个参数是拿直播地址必需的。真正拿到可播放地址的接口是https://open.ys7.com/api/lapp/live/address/getPOST参数如下参数说明accessToken上一步获取的tokendeviceSerial设备序列号channelNo通道号一般单通道设备填1protocol播放协议2代表HLS3代表RTMP具体以官方最新文档为准quality清晰度1是高清2是标清public static IEnumerator GetLiveAddress(string accessToken, string deviceSerial, int channelNo, System.Actionstring onSuccess, System.Actionstring onError) { var form new WWWForm(); form.AddField(accessToken, accessToken); form.AddField(deviceSerial, deviceSerial); form.AddField(channelNo, channelNo.ToString()); form.AddField(protocol, 2); // 2 HLS form.AddField(quality, 1); using (var request UnityWebRequest.Post(https://open.ys7.com/api/lapp/live/address/get, form)) { yield return request.SendWebRequest(); if (request.result UnityWebRequest.Result.Success) { var result JsonUtility.FromJsonYSResultYSLiveAddressData(request.downloadHandler.text); if (result.code 200) onSuccess?.Invoke(result.data.hls); else onError?.Invoke(result.msg); } else { onError?.Invoke(request.error); } } } [System.Serializable] public class YSLiveAddressData { public string id; public string deviceSerial; public int channelNo; public string hls; public string rtmp; public string hlsHd; }注意几个容易踩的坑返回的hls地址是带签名参数的临时地址有效期大概几个小时过期后必须重新调用接口获取不能用旧地址一直播。有些设备没开通云存储或转码服务可能拿不到hls地址这种情况需要去萤石云控制台确认设备状态。开放平台接口对调用频率有限制测试时别在循环里疯狂请求会被限流。2.3 地址格式与协议选型的对应关系萤石云返回的hls地址长这样https://xxx.ys7.com/xxx.../playback.m3u8?signxxxexpirexxx。我一开始以为拿到m3u8直接塞给Unity的VideoPlayer就能播试了之后发现根本不工作。查了Unity WebGL的文档才知道VideoPlayer在WebGL平台只支持有限的格式HLS直播流不在支持列表里。这就是为什么必须换一种思路让Unity的UI只画一个“占位框”真正播放视频的是浏览器层面的一个video元素。这个video元素的位置和大小由Unity每帧计算好通过jslib告诉浏览器去摆放。从用户视角看画面就是嵌在Unity界面里的但本质上它是个悬浮在Unity画布上方的普通网页元素。3. 工程落地jslib C# 双端联动3.1 工程目录与核心文件划分这个方案的核心是两部分C#负责业务逻辑和UI位置计算jslib负责操作浏览器video元素。我的工程目录是这样的Assets/ Plugins/WebGL/WebStreamPlayer.jslib Scripts/Runtime/WebStreamPlayer.cs Scripts/YSCloud/YSCloudApi.cs Scripts/YSCloud/YSData.cs Scenes/Demo.unityjslib文件必须放在Assets/Plugins/WebGL目录下Unity构建WebGL时会自动把它编译进HTML页面里。C#脚本里通过[DllImport(__Internal)]声明外部函数WebGL构建时Unity会把这两者绑定起来。3.2 jslib播放器插件的实现细节jslib的语法是Unity自定义的一套JavaScript包装核心用mergeInto(LibraryManager.library, { ... })导出函数。我封装了初始化、播放、停止、设置位置、设置屏幕尺寸五个函数mergeInto(LibraryManager.library, { YS_Init: function () { if (window.__ysVideo) return; var video document.createElement(video); video.id ys-video; video.style.position fixed; video.style.zIndex 9999; video.style.objectFit contain; video.style.display none; video.style.pointerEvents none; video.muted true; video.autoplay true; video.playsInline true; document.body.appendChild(video); window.__ysVideo video; window.__ysHls null; window.__ysFlv null; }, YS_PlayUrl: function (url) { var video window.__ysVideo; if (!video || !url) return; YS_Stop(); video.style.display block; video.muted true; if (url.indexOf(.m3u8) 0) { if (window.Hls Hls.isSupported()) { var hls new Hls({ enableWorker: true, lowLatencyMode: true }); hls.loadSource(url); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, function () { video.play().catch(function () {}); }); window.__ysHls hls; } else if (video.canPlayType(application/vnd.apple.mpegurl)) { video.src url; video.play(); } } else if (url.indexOf(.flv) 0) { if (window.flvjs flvjs.isSupported()) { var flvPlayer flvjs.createPlayer({ type: flv, url: url, isLive: true, hasAudio: false }); flvPlayer.attachMediaElement(video); flvPlayer.load(); flvPlayer.play(); window.__ysFlv flvPlayer; } } else { video.src url; video.play(); } }, YS_Stop: function () { var video window.__ysVideo; if (!video) return; if (window.__ysHls) { window.__ysHls.destroy(); window.__ysHls null; } if (window.__ysFlv) { window.__ysFlv.destroy(); window.__ysFlv null; } video.pause(); video.removeAttribute(src); video.load(); video.style.display none; }, YS_SetVideoRect: function (x, y, w, h) { var video window.__ysVideo; if (!video) return; var canvas document.querySelector(#canvas); if (!canvas) return; var canvasRect canvas.getBoundingClientRect(); var scaleX canvas.clientWidth / window.__ysScreenWidth; var scaleY canvas.clientHeight / window.__ysScreenHeight; var left canvasRect.left x * scaleX; var top canvasRect.top y * scaleY; var width w * scaleX; var height h * scaleY; video.style.left left px; video.style.top top px; video.style.width width px; video.style.height height px; }, YS_SetScreenSize: function (width, height) { window.__ysScreenWidth width; window.__ysScreenHeight height; } });这里有几个关键点必须说清楚第一YS_PlayUrl里我直接接收了C#传来的字符串参数没有用UTF8ToString。这是Unity新版jslib的推荐用法字符串参数会被自动转换成JavaScript字符串。如果你在Unity 2019或者更老的版本上遇到中文乱码或者地址截断再考虑手动做内存拷贝和UTF8解码。第二video元素设置了pointerEvents: none这个非常重要。否则video悬浮在Unity画布上方时会拦截所有鼠标事件导致Unity里的按钮点不了。第三hls.js和flv.js这两个库必须在Unity构建后的index.html里引入。我是在项目的index.html模板里加了两行CDN引用script srchttps://cdn.jsdelivr.net/npm/hls.jslatest/script script srchttps://cdn.jsdelivr.net/npm/flv.jslatest/script注意如果你们的项目必须离线部署这俩库需要下载到本地服务器但不能用Unity默认的打包方式塞进去要手动改构建后的index.html。3.3 C#侧的调用封装与坐标换算C#侧的封装很简单核心就是一个外部函数声明类using System.Runtime.InteropServices; public static class WebStreamPlayer { #if UNITY_WEBGL !UNITY_EDITOR [DllImport(__Internal)] public static extern void YS_Init(); [DllImport(__Internal)] public static extern void YS_PlayUrl(string url); [DllImport(__Internal)] public static extern void YS_Stop(); [DllImport(__Internal)] public static extern void YS_SetVideoRect(float x, float y, float w, float h); [DllImport(__Internal)] public static extern void YS_SetScreenSize(int width, int height); #else public static void YS_Init() { Debug.Log([WebStreamPlayer] Editor mode, no jslib.); } public static void YS_PlayUrl(string url) { Debug.Log([WebStreamPlayer] Editor play url: url); } public static void YS_Stop() { Debug.Log([WebStreamPlayer] Editor stop.); } public static void YS_SetVideoRect(float x, float y, float w, float h) { } public static void YS_SetScreenSize(int width, int height) { } #endif }注意UNITY_WEBGL !UNITY_EDITOR这个条件编译不能少。因为DllImport(__Internal)在Editor里运行会直接报错所以Editor下必须给一个空实现。这样同一个脚本在Editor里调试不会崩Build到WebGL后又能走真实逻辑。在UI显示这一块我用的方案是场景里放一个RawImage作为视频画面的占位框然后每帧把RawImage的四个顶点转换成屏幕坐标传给jslib。核心代码using UnityEngine; public class CameraViewBinder : MonoBehaviour { public RectTransform videoRect; public Camera uiCamera; private void Start() { WebStreamPlayer.YS_Init(); WebStreamPlayer.YS_SetScreenSize(Screen.width, Screen.height); } private void LateUpdate() { if (videoRect null) return; var corners new Vector3[4]; videoRect.GetWorldCorners(corners); var minScreen RectTransformUtility.WorldToScreenPoint(uiCamera, corners[0]); var maxScreen RectTransformUtility.WorldToScreenPoint(uiCamera, corners[2]); float left minScreen.x; float bottom minScreen.y; float width maxScreen.x - minScreen.x; float height maxScreen.y - minScreen.y; float cssTop Screen.height - bottom - height; WebStreamPlayer.YS_SetVideoRect(left, cssTop, width, height); } }这里必须解释一下坐标转换Unity的屏幕坐标系原点在左下角而浏览器的CSS坐标系原点在左上角所以y方向要翻转。也就是说Unity里计算出的bottom要变成CSS的top需要Screen.height - bottom - height。我一开始没做翻转结果视频画面垂直方向完全反了排查了半天才发现是坐标系问题。还有个隐藏坑如果Unity Canvas的缩放模式影响了实际渲染分辨率Screen.width和Screen.height是Unity的逻辑分辨率而canvas的CSS尺寸可能跟它不一样。所以jslib里我用canvas.clientWidth / __ysScreenWidth计算缩放比例就是为了兼容Unity画布被CSS拉伸的情况。如果画面位置有偏差优先检查这一步的缩放对不对。3.4 如何做到“界面贴合”RawImage占位与HTML overlay同步实现“视频画面嵌入Unity UI”的关键就是上面那段坐标同步逻辑。但这里有一个视觉层面的取舍video元素是浮在Unity画布上面的如果RawImage本身有背景色或者边框那video挂在RawImage上面是完全没问题的。但如果你在RawImage前面还有别的UI元素比如文字、图标盖在视频上面这些UI元素会被video挡住因为video的zIndex是9999在Unity画布之上。解决这个问题有两条路第一把需要盖在视频上方的UI元素也用html overlay来做这显然太麻烦了不推荐。第二调整层级关系让Unity画布整体放在video上层同时让Unity画布背景透明。具体做法是在Unity的Player Settings里把WebGL Template的渲染canvas背景设为transparent: true。这样Unity画布是半透明的video在下面Unity的UI和3D物体都能正常盖在视频上面。这个方案对监控大屏这种场景很实用因为大屏界面通常有各种信息卡片、报警弹窗压在视频画面上面。我实际项目用的是第二种方案效果比较理想。但要注意Unity WebGL的透明背景对性能有一定损耗而且在一些显卡驱动上会引发渲染异常最好在目标浏览器上多测几遍。如果你只是简单地把视频放在页面里不做复杂叠加用第一种方案反而更省事。顺带提一句如果你要在UI里动态切换多个摄像头画面可以多创建几个video元素每个对应一个jslib实例然后每个RawImage绑定一个CameraViewBinder。我在项目里最多同时显示4路视频流Chrome多路video并行播放没什么压力但要注意每路流的地址都要重新从萤石云API获取不能共用同一个带签名的临时地址。4. 浏览器兼容性与常见坑排查4.1 Mixed Content 与 CORS 的拦路虎这是我在项目里卡得最久的一个问题。萤石云返回的hls地址是http://开头的而我的WebGL页面部署在https://下浏览器直接把请求拦了。控制台报错是Mixed Content: The page at https://... was loaded over HTTPS, but requested an insecure resource http://...。解决方案有三种如果整个系统都部署在内网直接用http访问页面不走https最省事。如果必须走https把hls地址也转成https。可以给萤石云的存储域名单独配一张SSL证书或者用Nginx反代一层把http流转成https。如果只是临时测试可以在浏览器里临时允许不安全内容Chrome地址栏右侧有个盾牌图标点开后选择“仍要加载”。这个只适合开发环境生产环境千万别用。另一个跟跨域类似的问题是CORS。hls.js是用XHR去拉取m3u8和ts切片的如果服务器响应头里没有Access-Control-Allow-Origin请求会被浏览器拦截。排查方法很简单F12打开Network面板看m3u8请求的状态如果显示blocked by CORS policy那就是这个原因。萤石云的流媒体服务器大多数时候是允许跨域的但如果你接的是客户自建的流媒体服务大概率要自己处理CORS头。4.2 WebGL上下文创建失败与硬件加速这个坑不是视频流的问题但Unity WebGL项目部署到用户机器上时经常遇到。典型报错是a webgl context could not be created或者this browser supports WebGL 2, but it is disabled or unavailable。翻译成人话就是浏览器检测不到显卡硬件加速WebGL上下文创建失败Unity画面直接白屏。排查步骤在Chrome地址栏输入chrome://gpu看WebGL那一行的状态如果是Hardware accelerated就正常如果是Software only说明显卡驱动或浏览器设置有问题。检查chrome://settings/system里的“使用硬件加速模式”是否开启开启后重启浏览器。更新显卡驱动尤其是笔记本双显卡环境确认浏览器用的是独立显卡而不是核显。如果用户用的是国产安全浏览器或者内网管控浏览器WebGL经常被安全策略禁用这种情况只能让用户换Chrome或者调整浏览器策略。说实话WebGL上下文创建失败这个问题在视频播放之前就会暴露。如果用户在浏览器里能正常打开Unity页面只是视频黑屏那问题大概率不在WebGL而在前面的Mixed Content或CORS。4.3 延迟、卡顿与稳定性优化HLS方案的延迟实测大概在3到10秒具体取决于网络和切片长度。萤石云的hls切片一般是6秒一个所以延迟不会太低。如果客户觉得延迟高我建议切换到HTTP-FLV方案即protocol参数改成获取flv地址然后用flv.js播放。我实测FLV延迟能压到1到3秒代码我已经在上面的jslib里写好了直接调用FLV地址就能自动切换播放器。卡顿方面几个常见原因没有开启硬件加速视频解码全部走CPU尤其是多路同时播放时容易掉帧。hls.js的worker线程被浏览器限制可以在创建Hls实例时设置enableWorker: false有时候反而更稳。多路视频同时播放时浏览器对同一域名的并发连接数有限制。如果多路流域名相同建议做域名分片或者错峰加载。稳定性方面记得监听hls.js的ERROR事件遇到网络抖动能自动重连。我在jslib里加了简单的fatal错误处理如果是网络错误延迟2秒重新加载同一个地址如果是manifest解析失败直接销毁播放器等待C#层重新拉取新的直播地址。4.4 常见问题速查表现象可能原因快速处理Unity页面白屏控制台报WebGL context错误浏览器硬件加速未开启或驱动异常检查chrome://gpu开启硬件加速更新驱动视频区域黑屏控制台报Mixed Content页面是https视频地址是http换成https地址或整个页面走http视频请求被拦截报CORS错误流媒体服务器未返回跨域头后端代理加CORS或反向代理中转视频画面位置不对偏移或翻转Unity坐标和CSS坐标没对齐检查缩放比例和y轴翻转逻辑视频能播但一直加载中地址过期或者网络差重新调用萤石云API获取新地址多路视频同时播画面卡顿CPU解码压力大开硬件加速减少清晰度或降低路数点击Unity按钮没反应video元素拦截了鼠标事件确认video设置了pointerEvents: none手机上点播放没反应浏览器自动播放策略限制video必须设置muted和playsInline4.5 代码层面的几个易错点这里再补充几个我在写代码时踩过的坑都比较隐蔽。第一个是jslib函数命名不能和浏览器全局对象冲突。我第一次写的时候给初始化函数起了个名字叫Init()结果和Unity内部js代码冲突了构建后一直报Init is not defined。后来统一加了YS_前缀才解决。各位自己写jslib的时候函数名务必加项目前缀避免和Unity生成的js代码、第三方库的全局变量撞车。第二个是字符串参数的内存问题。如果你用老式写法在jslib里调用UTF8ToString(ptr)记得在C#侧传入的是IntPtr而不是string。我在网上抄了一段代码混用了两种写法导致地址在传递过程中被截断只传过去前几个字符。如果新版jslib直接接收string参数就不会有这个问题但要看清楚你到底在写老版本还是新版本的语法。第三个是LateUpdate里做坐标同步的时机。如果RawImage的尺寸在运行时会被动态修改必须保证先更新布局、再同步坐标。Unity的LateUpdate执行时机在所有Update之后所以在这里同步是安全的。但如果用了LayoutGroup自动布局RawImage的位置要到下一帧才生效这种情况可以用协程延后一帧再同步或者直接放在UI刷新后的回调里。第四个是销毁逻辑。页面退出时如果忘记销毁hls.js实例会有内存泄漏和视频连接不释放的问题。我在jslib的YS_Stop里已经做了destroy但还要注意如果Unity场景切换时没有调用YS_Stop旧页面里的video会一直挂在页面上新场景里重新创建video实例时就会出bug。所以场景卸载时一定要回调WebStreamPlayer.YS_Stop()。5. 进阶思路从overlay走向纹理上传如果你对overlay方案不满意比如视频上面要放Unity的粒子特效、视频要作为3D场景里的贴图那就得把视频帧上传到Unity纹理里。这个思路是在jslib里创建一个不可见的canvas把video的每一帧画到canvas上然后用texImage2D上传到WebGL纹理最后把纹理绑定到Unity的Material上。反过来Unity侧通过Texture2D的UpdateExternalTexture方法来接收这个原生纹理。这个方案的技术复杂度比overlay高一个量级主要难点在于纹理的生命周期管理、跨域纹理的浏览器限制、以及Unity WebGL的Texture2D外部纹理接口在不同Unity版本上的不稳定性。我调研过Unity 2021以后UpdateExternalTexture的稳定性有所改善但项目排期紧我没敢在生产环境用。如果你们有硬性需求做3D贴图视频流建议先用小demo验证纹理上传的性能和兼容性再推全。我个人在实际项目里的体会是除非监控画面必须出现在3D场景里否则overlay方案就是成本最低、效果最稳的选择。做WebGL项目思路要转过来不要总想着用Unity去实现一切浏览器这个舞台本来就提供了很多现成的能力借力打力才是最聪明的做法。如果有条件把视频播放相关的代码本文还有配套的精品资源点击获取
返回列表