ARTICLE DETAIL

资讯详情

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

Unity3d WebGL 替代 UMP:萤石云 HTTP-FLV 视频流接入实战

Unity3d WebGL 替代 UMP:萤石云 HTTP-FLV 视频流接入实战 简介这份Unity3d工程源码面向需要在Unity中快速播放萤石云监控视频流的开发者尤其适合受困于Universal Media Player打开慢、WebGL端无法播放等问题的中高级Unity程序员。项目以萤石云ezopen协议为例通过3D WebView打开网页播放视频流的嫁接思路实测打开速度比UMP快2至3倍并解决了WebGL平台视频播放的兼容难题。压缩包共1079个文件约115.38MB包含138个cs脚本、576个meta、125个png贴图、56个pak、55个js、17个dll及多个unity场景、prefab预制体、shader与wasm等覆盖工程配置、网页交互与视频流播放的完整模块。已有1131人学习下载。读者可获得可直接运行的Unity 2020.3.28f1c1工程源码理解网页播放视频流的实现路径并借鉴其排错与优化思路快速迁移到自己的监控播放项目中。1. 萤石云视频流在 Unity3d 里的真实处境为什么 WebGL 是分水岭很多做 Unity3d 可视化项目的同行都遇到过这个需求在三维场景里嵌入一路或多路萤石云监控视频流让用户点一下摄像头图标就能看到实时画面。PC 端用 UMP 插件跑 RTSP 或 RTMP 拉流本地测试一切正常打包 WebGL 就翻车——浏览器不支持原生 RTSPUMP 的底层依赖在 WebGL 平台直接失效。这不是配置问题是平台能力边界问题。萤石云官方提供的取流协议里RTMP、HLS、FLV 都能在浏览器环境找到对应方案唯独 RTSP 在 WebGL 里没有原生出路。所以标题里说的“替代 UMP”本质上是换一条 WebGL 能走通的取流链路而不是把 UMP 的代码改改就能用。这篇文章面向的是已经踩过 UMP WebGL 坑、或者正准备选型的 Unity3d C# 开发者我会把萤石云取流协议选型、WebGL 端播放器接入、C# 与 JavaScript 互调、以及实际部署中会遇到的坑按可复现的步骤讲清楚。2. 萤石云取流协议选型WebGL 能走哪条路2.1 萤石云三种取流地址的浏览器兼容性对比萤石云设备接入后平台会提供几种取流地址格式。常见的有 RTSP、RTMP、HLS 和 HTTP-FLV。在 Unity3d WebGL 环境下这四种协议的可用性差异很大选错了后面全是白费功夫。协议地址特征WebGL 原生支持延迟表现推荐度RTSPrtsp:// 开头不支持低不推荐RTMPrtmp:// 开头需 Flash已淘汰低不推荐HLS.m3u8 结尾部分浏览器支持高5-20秒应急可用HTTP-FLV.flv 结尾需 flv.js 等库低1-3秒推荐从表格能看出来HTTP-FLV 是 WebGL 场景下延迟和兼容性平衡得最好的选择。HLS 虽然 iOS Safari 原生支持但延迟太高监控场景里用户点开画面等十几秒才出图体验说不过去。RTMP 依赖 Flash2021 年后主流浏览器全部移除支持这条路已经堵死。RTSP 更不用提浏览器内核根本不认这个协议。所以替代 UMP 的核心思路就一句话把萤石云取流地址从 RTSP 换成 HTTP-FLV然后在 WebGL 端用 flv.js 这类 JavaScript 库完成解码播放Unity3d 负责把视频画面渲染到 UI 或三维物体上。2.2 获取萤石云 HTTP-FLV 取流地址的两种方式萤石云开放平台提供 API 接口来获取取流地址。常见做法是调用/api/lapp/live/address/get接口传入设备序列号和通道号返回结果里包含多个协议的地址。我一般会在 C# 后端服务里封装这个请求而不是直接在 Unity 里调原因有两个一是 accessToken 有有效期后端统一管理方便刷新二是避免把 appKey 和 appSecret 暴露在客户端。// C# 后端获取萤石云取流地址的封装方法 using System; using System.Net.Http; using System.Threading.Tasks; using Newtonsoft.Json.Linq; public class EzvizStreamService { private static readonly HttpClient client new HttpClient(); private string appKey 你的appKey; private string appSecret 你的appSecret; private string accessToken; private DateTime tokenExpireTime; // 获取 accessToken缓存有效期内不重复请求 public async Taskstring GetAccessTokenAsync() { if (!string.IsNullOrEmpty(accessToken) DateTime.Now tokenExpireTime) return accessToken; string url $https://open.ys7.com/api/lapp/token/get?appKey{appKey}appSecret{appSecret}; var response await client.PostAsync(url, null); var json JObject.Parse(await response.Content.ReadAsStringAsync()); if (json[code]?.ToString() 200) { accessToken json[data][accessToken].ToString(); // 萤石云 token 默认 7 天有效这里提前 1 小时过期 tokenExpireTime DateTime.Now.AddSeconds( double.Parse(json[data][expireTime].ToString()) - 3600); return accessToken; } throw new Exception($获取 token 失败: {json[msg]}); } // 获取 HTTP-FLV 取流地址 public async Taskstring GetFlvUrlAsync(string deviceSerial, int channelNo) { string token await GetAccessTokenAsync(); string url $https://open.ys7.com/api/lapp/live/address/get? $accessToken{token}deviceSerial{deviceSerial}channelNo{channelNo}protocol3; var response await client.PostAsync(url, null); var json JObject.Parse(await response.Content.ReadAsStringAsync()); if (json[code]?.ToString() 200) { // protocol3 对应 HTTP-FLV返回的 url 字段即为取流地址 return json[data][url].ToString(); } throw new Exception($获取取流地址失败: {json[msg]}); } }这段代码里有两个关键参数需要说明。protocol参数控制返回的地址协议类型萤石云文档里 1 是 RTMP、2 是 HLS、3 是 HTTP-FLV传 3 才能拿到 WebGL 可用的地址。expireTime是 token 过期时间戳单位是秒我习惯提前一小时刷新避免边界情况。另外注意deviceSerial是设备序列号不是设备名称在萤石云控制台设备详情页可以找到。2.3 为什么不在 Unity C# 里直接请求萤石云 API有些同行图省事直接在 Unity 的 C# 脚本里用UnityWebRequest调萤石云接口。这样做在编辑器里能跑通但打包 WebGL 后会遇到跨域问题。萤石云 API 域名不会给你的 WebGL 部署域名配置 CORS 头浏览器直接拦截请求。而且 appSecret 写在客户端代码里反编译后直接泄露设备安全没有保障。我一般会搭一个轻量后端服务用 C# ASP.NET Core 或者 Node.js 都行专门负责 token 管理和取流地址下发。Unity 端只请求自己的后端后端再转发到萤石云。这样既绕开了跨域又保护了密钥。后端部署在内网服务器上Unity WebGL 页面和它在同一域名下跨域问题自然消失。3. WebGL 端播放器接入flv.js 与 Unity 的桥接3.1 在 Unity WebGL 模板里引入 flv.jsUnity WebGL 打包后会生成一个 HTML 模板文件默认路径在Assets/WebGLTemplates下。你需要在这个模板的head里引入 flv.js 的脚本文件。flv.js 是 B 站开源的 JavaScript 库专门用来在浏览器里解码播放 HTTP-FLV 流不需要 Flash 插件。!-- Unity WebGL 模板 index.html 中引入 flv.js -- script srchttps://cdn.jsdelivr.net/npm/flv.js1.6.2/dist/flv.min.js/script script // 全局变量供 Unity 调用 var flvPlayer null; var videoElement null; // Unity 调用的初始化函数 function InitFlvPlayer(videoId, streamUrl) { videoElement document.getElementById(videoId); if (!videoElement) { console.error(找不到 video 元素: videoId); return false; } if (flvjs.isSupported()) { flvPlayer flvjs.createPlayer({ type: flv, url: streamUrl, isLive: true, hasAudio: false // 监控场景通常不需要音频 }); flvPlayer.attachMediaElement(videoElement); flvPlayer.load(); flvPlayer.play(); return true; } console.error(当前浏览器不支持 flv.js); return false; } // 停止播放并释放资源 function DestroyFlvPlayer() { if (flvPlayer) { flvPlayer.pause(); flvPlayer.unload(); flvPlayer.detachMediaElement(); flvPlayer.destroy(); flvPlayer null; } } /script这段 JavaScript 代码定义了两个全局函数InitFlvPlayer和DestroyFlvPlayer它们会被 Unity 的 C# 脚本通过Application.ExternalCall或jslib方式调用。videoId是页面上video元素的 idstreamUrl是从后端拿到的 HTTP-FLV 地址。isLive: true告诉 flv.js 这是直播流它会自动处理缓冲和追帧。hasAudio: false是因为监控视频通常不需要声音关掉可以减少带宽和 CPU 占用。3.2 C# 调用 JavaScript 的两种方式与选择Unity WebGL 平台下C# 调用 JavaScript 有两种常见方式。一种是用Application.ExternalCall直接在 C# 里写 JavaScript 函数名和参数。另一种是写.jslib文件在 Unity 项目里声明外部函数然后 C# 用DllImport调用。Application.ExternalCall的优点是简单直接不需要额外文件缺点是参数类型受限只能传基本类型而且字符串拼接容易出错。.jslib方式更规范支持回调适合复杂交互。我一般会在简单场景用ExternalCall需要传结构化数据或处理回调时用.jslib。// C# 端调用 JavaScript 初始化播放器 using UnityEngine; using System.Runtime.InteropServices; public class FlvStreamPlayer : MonoBehaviour { // 声明 jslib 中的外部函数 [DllImport(__Internal)] private static extern bool InitFlvPlayer(string videoId, string streamUrl); [DllImport(__Internal)] private static extern void DestroyFlvPlayer(); private string currentStreamUrl; // 供外部调用的播放方法 public void PlayStream(string videoId, string flvUrl) { currentStreamUrl flvUrl; #if UNITY_WEBGL !UNITY_EDITOR bool success InitFlvPlayer(videoId, flvUrl); if (!success) { Debug.LogError(FLV 播放器初始化失败); } #else Debug.Log(非 WebGL 平台跳过 FLV 播放器初始化); #endif } // 停止播放 public void StopStream() { #if UNITY_WEBGL !UNITY_EDITOR DestroyFlvPlayer(); #endif currentStreamUrl null; } void OnDestroy() { StopStream(); } }这里用#if UNITY_WEBGL !UNITY_EDITOR做平台判断很重要。在编辑器里调试时DllImport(__Internal)会报错因为编辑器环境没有 JavaScript 运行时。加上这个条件编译编辑器里走日志分支打包 WebGL 后才走真实调用。OnDestroy里调用StopStream是为了防止场景切换时播放器没释放导致内存泄漏或后台继续拉流。3.3 把视频画面渲染到 Unity 场景的两种方案flv.js 解码后的画面是渲染在 HTML 的video元素上的Unity WebGL 的画布和这个 video 元素是两层。要让视频出现在 Unity 场景里有两种做法。第一种是 CSS 叠加。把video元素用绝对定位盖在 Unity 画布上方通过 JavaScript 控制它的位置和大小。这种方案延迟最低因为视频画面不经过 Unity 渲染管线直接由浏览器合成。缺点是视频始终在 Unity 内容上层无法被三维物体遮挡也无法做后期特效。第二种是 RenderTexture 方案。用 JavaScript 把video的画面逐帧绘制到canvas再通过 Unity 的WebGLTexture接口传给 C# 端的Texture2D。这种方案视频可以作为材质贴在三维物体上能被遮挡、能做特效但每帧要跨语言传数据延迟和 CPU 占用都会增加。我一般会先问清楚需求如果只是做个监控大屏视频固定在某个区域显示CSS 叠加就够了简单稳定。如果要把监控画面贴到三维场景的电视墙或设备模型上那就得用 RenderTexture 方案接受一定的性能开销。4. 避坑与排查WebGL 视频流最容易翻车的五个点4.1 现象打包后视频黑屏控制台报 CORS 错误原因萤石云返回的 HTTP-FLV 地址域名和你 WebGL 页面的域名不一致浏览器拦截了跨域请求。萤石云的 FLV 地址通常带 CDN 域名不会给你的部署域名配置 CORS 头。解决在后端服务里做一层代理把 FLV 流通过自己的域名转发。C# 后端可以用HttpClient拉取萤石云流再以流式响应写给前端。这样 flv.js 请求的是同域地址跨域问题消失。注意代理层要设置Response.BufferOutput false否则会等整个流缓冲完才返回直播就变成点播了。4.2 现象视频能播放但延迟越来越大几分钟后落后十几秒原因flv.js 默认的缓冲策略偏保守网络波动时会累积延迟。直播流如果一直追不上最新帧延迟会持续增长。解决在创建 flv.js 播放器时配置enableStashBuffer: false关闭缓冲池。同时监听statistics_info事件当延迟超过阈值时调用flvPlayer.currentTime flvPlayer.buffered.end(0)强制跳到最新位置。另外liveBufferLatencyChasing: true这个配置项也能帮助追帧但要注意它可能导致画面卡顿需要根据网络质量权衡。4.3 现象编辑器里正常WebGL 打包后提示DllImport找不到入口点原因.jslib文件没有放在正确的目录或者 Unity 打包时没有把它合并进最终产物。.jslib必须放在Assets/Plugins/WebGL目录下文件名和函数名要匹配。解决检查.jslib文件路径是否为Assets/Plugins/WebGL/你的文件名.jslib。文件内容里函数名要和 C# 里DllImport声明的名称完全一致大小写敏感。另外.jslib文件里函数要用mergeInto(LibraryManager.library, { ... })包裹不能直接写function声明。4.4 现象多路视频同时播放时页面卡死CPU 占用飙升原因flv.js 每路流都会启动独立的解码线程浏览器对同时解码的视频路数有限制。一般超过 4 路 1080p 流低配机器就会扛不住。解决做路数控制同时播放不超过 4 路。超过的部分用截图或低分辨率子码流替代。萤石云支持主子码流主码流用于单画面全屏子码流用于多画面宫格。在取流地址请求时通过streamType参数区分子码流分辨率低解码压力小很多。4.5 现象页面切换或关闭后后台仍在拉流流量持续消耗原因Unity 场景切换时没有调用DestroyFlvPlayerflv.js 实例还在后台运行。或者浏览器页面关闭时JavaScript 的beforeunload事件没有触发清理逻辑。解决在 Unity 的OnDestroy和OnApplicationQuit里都调用停止播放的方法。同时在 HTML 模板里监听window.onbeforeunload页面关闭前强制销毁所有 flv 实例。另外可以在后端代理层做连接数限制同一设备序列号的流超过一定时间没有心跳就自动断开防止客户端异常退出后流一直挂着。5. 进阶技巧用子码流和按需拉流把并发做到 16 路5.1 主子码流切换的 C# 封装萤石云设备通常支持主码流和子码流两路输出。主码流分辨率高、码率大适合单画面全屏查看。子码流分辨率低、码率小适合多画面宫格。在取流地址请求时通过streamType参数控制0 是主码流1 是子码流。// 获取指定码流类型的取流地址 public async Taskstring GetFlvUrlAsync(string deviceSerial, int channelNo, int streamType 1) { string token await GetAccessTokenAsync(); // streamType: 0-主码流 1-子码流 string url $https://open.ys7.com/api/lapp/live/address/get? $accessToken{token}deviceSerial{deviceSerial} $channelNo{channelNo}protocol3streamType{streamType}; var response await client.PostAsync(url, null); var json JObject.Parse(await response.Content.ReadAsStringAsync()); if (json[code]?.ToString() 200) { return json[data][url].ToString(); } throw new Exception($获取取流地址失败: {json[msg]}); }这个封装里streamType默认给 1也就是子码流。多画面场景下全部用子码流单画面放大时再切换成主码流重新拉流。切换时记得先销毁旧的 flv 实例再创建新的否则两路流同时拉会浪费带宽。5.2 按需拉流只播放可见区域的视频16 路视频如果同时拉流即使子码流也扛不住。实际监控大屏通常一屏显示 4x4 宫格但用户不会同时盯着 16 路看。我一般会做按需拉流只对当前可见的宫格拉流滚动出可视区域的宫格自动停止。实现方式是在 Unity 端维护一个可见性列表每个宫格对应一个视频播放组件。当宫格进入可视区域时调用PlayStream离开时调用StopStream。后端代理层配合做连接数控制同一时间最多维持 8 路活跃流超出的排队等待。// 宫格可见性管理控制按需拉流 using System.Collections.Generic; using UnityEngine; public class StreamVisibilityManager : MonoBehaviour { // 当前活跃的流数量上限 private const int MaxActiveStreams 8; private Dictionarystring, FlvStreamPlayer gridPlayers new Dictionarystring, FlvStreamPlayer(); private Liststring activeStreams new Liststring(); // 宫格进入可视区域时调用 public void OnGridVisible(string gridId, string flvUrl) { if (activeStreams.Contains(gridId)) return; if (activeStreams.Count MaxActiveStreams) { // 超出上限移除最早激活的流 string oldest activeStreams[0]; activeStreams.RemoveAt(0); if (gridPlayers.ContainsKey(oldest)) gridPlayers[oldest].StopStream(); } if (!gridPlayers.ContainsKey(gridId)) { // 动态创建播放器组件实际项目中建议用对象池 var go new GameObject($Player_{gridId}); gridPlayers[gridId] go.AddComponentFlvStreamPlayer(); } gridPlayers[gridId].PlayStream($video_{gridId}, flvUrl); activeStreams.Add(gridId); } // 宫格离开可视区域时调用 public void OnGridInvisible(string gridId) { if (gridPlayers.ContainsKey(gridId)) { gridPlayers[gridId].StopStream(); } activeStreams.Remove(gridId); } }这段代码里MaxActiveStreams设为 8是实测下来中端配置电脑能稳定解码的上限。activeStreams列表按激活顺序排列超出时移除最早的。实际项目里建议用对象池管理FlvStreamPlayer组件避免频繁创建销毁 GameObject 带来的 GC 压力。5.3 一个容易被忽略的细节video 元素的 z-index 管理CSS 叠加方案里video元素的z-index需要仔细管理。Unity WebGL 画布本身是一个canvas默认z-index是 0。如果把video的z-index设得太高它会盖住所有 Unity UI包括按钮和弹窗。设得太低又会被画布遮住。我一般会把video的z-index设为 1Unity 画布设为 0然后在 Unity 里需要显示在视频上方的 UI 元素通过 JavaScript 动态调整z-index到 2。切换场景或打开弹窗时记得把视频的z-index临时降下去否则弹窗会被视频盖住。这个细节在开发阶段很容易忽略上线后用户反馈“弹窗被视频挡住了”才回头查算是一个血泪经验。另外video元素的pointer-events建议设为none让鼠标事件穿透到 Unity 画布。否则用户点击视频区域时事件被 video 元素捕获Unity 收不到点击交互就断了。需要视频区域响应点击时在 Unity 端用射线检测处理而不是依赖 HTML 元素的事件。这套方案我从去年开始用在几个智慧园区和工厂监控项目里WebGL 端最多跑过 16 路子码流宫格单画面切换主码流延迟在 1.5 秒左右稳定性满足日常监控需求。替代 UMP 的关键不是找另一个插件而是把取流协议从 RTSP 换成 HTTP-FLV再用 flv.js 在浏览器端解码。C# 负责业务逻辑和 Unity 渲染JavaScript 负责视频解码两边通过 jslib 桥接。踩过的坑主要集中在跨域、延迟累积和资源释放上按第 4 章的排查清单逐条过一遍基本能覆盖 90% 的问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表