简介:面向Unity/C#开发者的WebGL视频监控方案demo,核心是基于AVProVideo插件拉取并播放海康摄像头实时画面,解决WebGL平台下的视频解码与渲染问题。作者重写了Http请求模块,并加入无水印AVProVideo的发布配置指导文件;使用者只需修改请求配表中的IP、端口等字段即可接入自有设备,本地配合nginx部署也有对应思路可循。压缩包约80MB,共701个文件,主要包含cs脚本99个、asset配置30个、shader25个,以及exoplayer系列aar、so、dll等跨平台解码库,另有mp4测试视频、png图标等辅助文件,目录结构便于按模块查阅。对于需要将海康监控集成到Unity并发布WebGL的开发者,这套demo展示了从请求鉴权、拉流解码、画面渲染到打包发布的一整条链路,并能避免AVProVideo授权和水印带来的麻烦。已有1214人学习下载,适合有一定Unity基础、希望快速获得可运行方案的开发者。
1. WebGL 播放海康实时画面:这道坎卡在哪
之前接了一个监控大屏项目,需求是在浏览器里同时弹 9 路海康摄像头实时画面。我第一版直接用 Unity VideoPlayer 喂 RTSP 地址,打包出来全是黑屏,折腾了两天才查明白:浏览器根本不认 RTSP 协议,必须转成 HLS 才能被页面里的 video 元素消费。这份 demo 源码走的正是这条绕行链路:Unity WebGL + AVProVideo 插件 + 海康 ISAPI 取流,把设备 RTSP 推给边缘媒体服务转 HLS,再在 WebGL 中用 AVProVideo 播放。适合正在写监控大屏、Unity 网页嵌入实时视频的开发者直接拿来当脚手架,省掉从零踩协议坑的时间。
2. 架构与技术选型:RTSP 绕行成 HLS 的完整链路
demo 的看点不在 C# 脚本有多复杂,而在它把「浏览器不能播 RTSP」这条硬限制绕得很干净。整个项目按数据流向分成设备端、边缘转换、WebGL 播放三段,每一段各管一摊,代码结构上也按这个边界做了分层。
2.1 技术栈全景与数据链路
项目的技术栈组合很常规,但组合起来正好是监控大屏的标配形态:
- 客户端:Unity LTS 版本 + WebGL 构建目标,C# 写业务逻辑,AVProVideo 负责视频解码与纹理回传;
- 设备端:海康 IPC/NVR,开放 554 端口的 RTSP 拉流,REST 接口走 ISAPI 协议;
- 边缘服务:ffmpeg 从设备拉 RTSP,切成 HLS 的 m3u8/ts 文件,Nginx 做静态分发和跨域头注入。
数据链路实际跑起来长这样:海康摄像机 → RTSP → ffmpeg 拉流转 HLS → Nginx 分发 m3u8/ts → Unity WebGL 里的 AVProVideo 请求播放 → 解码帧经 WebGL 纹理 bridge 上传 GPU → C# 侧绑定 RawImage 显示。
为什么必须绕这一圈?浏览器里的 video 元素只认 HTTP(S) 协议族里的媒体格式,HLS、DASH、MP4 都没问题,但 RTSP 是二进制控制协议,没有浏览器会在原生层实现它。想在 WebGL 里播 RTSP,唯一体面的路线就是让后端把它转成 HLS。这不是 demo 的过度设计,是所有 WebGL 监控项目绕不开的必经之路,搞清楚这条链路,后面所有代码都是在为它填细节。
提示:如果是纯局域网单机演示,也可以让海康设备直接输出 HLS,但不少固件默认不开这条路,用 ffmpeg 中转兼容性最稳,换设备也不用改播放端。
2.2 项目目录与脚本职责
拿到源码先别急着打开场景,把目录职责认一遍,后面改起来才不迷路。demo 的 Assets 下大致按下面这张表分布:
| 路径 | 职责 |
|---|---|
| Assets/Plugins/AVProVideo/ | 插件本体,包含 WebGL 平台专用的 js 桥接与解码封装 |
| Assets/Scripts/Core/PlaybackManager.cs | 播放生命周期管理:打开、停止、释放、断流重连 |
| Assets/Scripts/Core/StreamSource.cs | 通道数据模型:设备 IP、通道号、码流类型、播放地址 |
| Assets/Scripts/Device/HikvisionApi.cs | 海康 ISAPI 封装:校验登录、拉设备信息、拼流地址 |
| Assets/Scripts/UI/ChannelGrid.cs | 动态生成网格,把通道数据绑定到每格画面 |
| Assets/StreamingAssets/demo_sources.json | 通道配置表,demo 启动时加载 |
PlaybackManager 是核心,所有对 AVProVideo 的调用都收口在它里面;HikvisionApi 只跟设备打交道,不碰任何显示逻辑;UI 层只消费 StreamSource 数据。三层分离的好处是,后面把海康换成别的品牌,或者把 HLS 换成 WebRTC 网关,都只改对应一层,不用动播放器。
如果目录里没有 StreamingAssets 或 json 不存在,demo 一般会走内置默认通道,这点从 StreamSource 的构造函数能看出来。实际项目我会把这份配置挪到服务器下发,浏览器端只认一份带签名的流地址列表,改通道不用重新打包。
2.3 为什么不直接用 hls.js 或 Unity VideoPlayer
拆这个项目之前我也想过,浏览器里单拎出来很多方案都能播,为什么非要用 AVProVideo。答案是:Unity WebGL 里「能播」和「画面能进场景」是两回事。
hls.js 确实能在网页里播 HLS,但它跑在 HTML 的 video 元素里,Unity 拿不到那一帧的画面。要让监控画面嵌进 Unity 场景,必须把这些帧上传到 WebGL 纹理再贴到 RawImage 上,自研这一套要处理跨域、纹理生命周期、iOS Safari 兼容,成本很高,属于典型的黑匣子。
Unity 自带的 VideoPlayer 在 WebGL 平台支持度很差,官方文档都不建议,我第一版就是在这翻的车。AVProVideo 的价值在于它把浏览器解码、纹理回传、事件回调都封装好了,C# 侧只关心播放地址和生命周期,而且编辑器里直接能预览,调试效率高出不少。demo 选它,本质上是把最容易出幺蛾子的部分交给成熟封装。
3. 核心代码拆解:从通道点击到画面弹出的完整链路
这一章把 demo 里最关键的几个脚本按调用顺序走一遍。逻辑不复杂,但每段代码背后都有浏览器平台特有的约束,注释里标出来的地方都是实际踩过的位置。
3.1 数据模型:StreamSource 先定清楚「播什么」
// StreamSource.cs —— 每路画面只存“来源信息”和“能播的地址” using System; [Serializable] public class StreamSource { public string channelName; // 通道名,UI 上显示用 public string deviceIp; // 海康设备 IP public int channelNo; // 通道号,从 1 开始 public string streamType = "main";// main 主码流 / sub 子码流 public string hlsUrl; // WebGL 实际播放的 HLS 地址 // 海康 RTSP 取流 URL,注意这不是给浏览器用的 public string BuildRtspUrl(string user, string pass) { // 主码流通道后缀 01,子码流 02,通道号补零到两位 string typeCode = streamType == "sub" ? "02" : "01"; return $"rtsp://{user}:{pass}@{deviceIp}:554/Streaming/Channels/{channelNo:D2}{typeCode}"; } }这个模型把「设备侧地址」和「播放侧地址」分开存放。BuildRtspUrl 生成的地址是给 ffmpeg 或后端拉流用的,浏览器端一律走 hlsUrl。最容易忽略的是 channelNo:D2 的补零:海康协议里通道号固定两位,channelNo=1 时必须拼成 01,拼成 1 直接取流失败。
主码流和子码流的差别也在这里体现:主码流清晰但带宽大,适合单路全屏;子码流分辨率低,适合九宫格。实际项目可以做到点击放大时从 sub 切到 main,demo 里留 streamType 字段就是为这个做准备的。hlsUrl 在 demo 里是预先配好的,真实部署时一般由后端根据 RTSP 地址动态生成并返回,客户端不感知转码细节。
3.2 播放核心:PlaybackManager 的生命周期设计
// PlaybackManager.cs —— 所有对 AVProVideo 的调用都收口在这一层 public class PlaybackManager : MonoBehaviour { [SerializeField] private MediaPlayer _player; [SerializeField] private RawImage _display; private string _currentUrl; private void Start() { _player.Events.AddListener(OnVideoEvent); } public void Play(StreamSource source) { // 切流前必须把上一路彻底停掉,否则 WebGL 会残留解码上下文 _player.Stop(); _currentUrl = source.hlsUrl; _player.OpenVideoFromFile( MediaPlayer.FileLocation.AbsolutePathOrURL, _currentUrl, true // autoPlayStart:打开成功后自动播放 ); } private void OnVideoEvent(MediaPlayer mp, MediaPlayerEvent.EventType et, ErrorCode err) { if (et == MediaPlayerEvent.EventType.Error) { Debug.LogError($"[Playback] 拉流失败:{_currentUrl},错误码 {err}"); StartCoroutine(AutoReconnect()); } else if (et == MediaPlayerEvent.EventType.OpenSuccess) { // m_ResolveTexture 是 AVProVideo 回传解码帧的桥接纹理 _display.texture = mp.m_ResolveTexture; } } }OpenVideoFromFile 的第三个参数是 autoPlayStart,WebGL 下打开即播;第二个参数是流地址,这里用 AbsolutePathOrURL 表示直接播网络 URL,不依赖 StreamingAssets 目录,这样 hlsUrl 指向哪台服务器都能播。事件回调里主要处理两个分支:OpenSuccess 时把桥接纹理 m_ResolveTexture 赋给 RawImage,画面立刻出现;Error 时记录日志并触发重连。
WebGL 下 Stop() 和 CloseMedia() 的区别必须分清:Stop 只是暂停,CloseMedia 才会释放解码资源。切换通道时先 Stop 再 Open 就够了,只有错误需要彻底重建才调 CloseMedia。如果切流前不先 Stop,连续切几次浏览器会同时挂着多个解码上下文,内存随之涨上去,这是后文避坑章里白屏问题的主要来源。
3.3 断流重连:直播场景下的自动恢复
// 直播流不像点播有结束,播不动基本就是链路断了,需要自动重连 private IEnumerator AutoReconnect() { yield return new WaitForSeconds(3f); if (string.IsNullOrEmpty(_currentUrl)) yield break; Debug.LogWarning($"[Playback] 3 秒后重连:{_currentUrl}"); // 重连前先让播放器内部状态归零,避免 Error 状态下直接 Open 卡缓冲 _player.Stop(); _player.OpenVideoFromFile( MediaPlayer.FileLocation.AbsolutePathOrURL, _currentUrl, true ); }重连间隔设 3 秒是经验值:太快会让 ffmpeg 和摄像头反复握手,把设备连接数打满;太慢则大屏上黑屏时间太长。如果连续失败超过三次,我会把策略改成指数退避,间隔按 3、6、12 秒递增,避免无效请求把设备端口占死。海康设备对并发取流数有限制,这一层控制在真实部署里比播放器代码本身更关键,也更容易被忽略。
注意:重连逻辑必须串行化,不能让两个协程同时去 Open 同一个 MediaPlayer。我一般会加一个 _retryCount 字段,每次 Open 前先判断当前是否已经在重连流程里。
4. WebGL 构建与部署:PlayerSetting、Nginx 和跨域三件事
跑通 demo 分两步:先在 Unity 里把 WebGL 包打出来,再把它和海康的 HLS 流放到同一个可访问的服务器下。这一章按操作顺序给参数,照着配基本不会卡。
4.1 Unity WebGL 构建参数
Build Settings 里切到 WebGL 平台后,Player Settings 里这几个参数直接决定播放稳定性:
| 设置项 | 推荐值 | 说明 |
|---|---|---|
| Resolution | 1920 x 1080 | 监控大屏基准,按拼接屏实际分辨率调 |
| WebGL Memory Size | 256 MB 以上 | 多路播放建议 512 MB,花屏白屏先查这里 |
| Compression Format | Brotli | 包体最小;服务器不支持时选 Disabled |
| Graphics API | WebGL 2.0 | 纹理上传和抗锯齿表现更稳 |
| Data caching | 勾选 | 二次打开时缓存打包数据,加载更快 |
Compression Format 有个常见坑:选了 Brotli,服务器必须能正确响应 .br 文件的 content-encoding,用的 Nginx 没装 brotli 模块时,浏览器下载完不会自动解压,页面卡在加载进度条。新手第一次打包遇白屏,八成是压缩格式和服务器不匹配,不是脚本问题。
AVProVideo 导入时注意勾选 WebGL 平台支持,插件目录下会多出 .jslib 桥接文件。Build 完成后产出 index.html 加 Build 目录结构,整个目录原样拷到服务器即可。另外建议直接 Build 到文件夹而不是点 Build And Run,后者起的是本地临时服务器,访问不到内网的海康流地址,部署阶段容易误导排查方向。
4.2 Nginx 托管 Unity 产物与 HLS 切片
demo 的部署拓扑很简单:一台 Nginx 同时托管 Unity 页面和 HLS 切片目录。下面这份配置可以直接抄:
server { listen 8080; root /data/www/stream-demo; # 托管 Unity WebGL 静态产物 location / { try_files $uri $uri/ /index.html; } # HLS 分发:m3u8 和 ts 必须带正确的 MIME 与跨域头 location /hls/ { alias /data/hls/; add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods "GET, OPTIONS" always; add_header Cache-Control no-cache always; types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; } if ($request_method = OPTIONS) { return 204; } } }跨域头这一行是整套部署里最容易漏的。Unity 页面跑在 :8080,HLS 流如果由另一台媒体服务器分发,浏览器 video 元素按同源策略会拒绝跨域拉取,Console 报 Access-Control-Allow-Origin 缺失。把 HLS 和页面放在同一个 Nginx 下并注入头,问题直接消失。
m3u8 的 MIME 类型也必须写对,写错时浏览器把它当普通文本拒绝解析。ts 片段的 video/mp2t 同理,少了它部分内核在 HLS 解析阶段就罢工。add_header 后面带 always 是防止 404 等非 200 响应时头不生效,排查跨域时最容易在这上面看走眼。
4.3 ffmpeg 拉流切片的参数怎么给
# 海康 RTSP 转 HLS,2 秒切片、播放列表只保留最近 4 片 ffmpeg -rtsp_transport tcp \ -i "rtsp://user:password@192.168.1.64:554/Streaming/Channels/101" \ -c:v copy -c:a aac \ -f hls -hls_time 2 -hls_list_size 4 -hls_flags delete_segments \ /data/hls/channel_101.m3u8参数逐个说:-rtsp_transport tcp 强制走 TCP 拉流,UDP 跨网段丢包严重,画面会频繁花屏;-c:v copy 表示视频流不重编码,前提是设备编码格式为 H.264,如果设备开的是 H.265,浏览器解不了,必须去掉 copy 改为 -c:v libx264 -profile:v baseline 强制转码;-hls_time 2 是每个切片 2 秒,切片越大延迟越高;-hls_list_size 4 是播放列表只保留最近 4 片,直播不需要历史回看。
这里还有一个实际部署里常碰到的延迟细节:如果设备端设置了较长的 I 帧间隔(比如 4 秒一个关键帧),-c:v copy 模式下首屏会明显变慢,因为播放器要等下一个关键帧到达才开始出画面。解决方式要么调设备编码参数,要么在转码时通过 x264 参数强制关键帧间隔。
另外,如果设备密码里带了 @ 或 : 符号,RTSP URL 解析会错位,ffmpeg 直接 401。最稳的办法是先用海康 ISAPI 拿到临时取流凭证,或者对特殊字符做 URL 编码,具体展开在避坑章。
4.4 部署后怎么确认链路通
浏览器 F12 打开 Network 面板,过滤 m3u8:能看到请求返回 200,且响应头里有 access-control-allow-origin,说明跨域这关过了;再点开 ts 片段确认是 200,视频就该出画面。更快的验证在服务器侧用 curl:
curl -v -H "Origin: http://localhost:8080" \ http://localhost:8080/hls/channel_101.m3u8看响应头是否带 Access-Control-Allow-Origin: *,以及 body 里的 ts 路径是否和 Nginx 的 alias 目录对得上。路径对不上是典型的自伤问题:m3u8 里写死了绝对路径,换服务器后全部 404。这类问题我在现场遇到过好几次,每次排查链路前先 curl 一下,能省掉大量在浏览器里瞎猜的时间。
5. 避坑手册:海康流进浏览器的五个典型翻车现场
链路跑通只是开始,真实环境里每个环节都有坑。这一章是调类似项目时反复踩过的五类问题,按现象、原因、解决的顺序写,方便直接对号排查。
5.1 黑屏加 Console 报跨域
现象:Unity 画面黑屏,浏览器 Console 报 No Access-Control-Allow-Origin header,Network 里 m3u8 请求被标记 blocked。 原因:Unity 页面和 HLS 流不在同一个源,浏览器 video 元素默认不允许跨域拉取视频,连带纹理上传也被掐断。 解决:在 Nginx 的 /hls/ 配置里加上 Access-Control-Allow-Origin 头,并确保 add_header 带 always 关键字,让 200 和 404 响应都注入头。然后用 4.4 节的 curl 命令验证响应头,别在浏览器里反复刷新猜,两步就能定位到是 Nginx 没生效还是请求根本没到 Nginx。
5.2 HLS 文件返回 200 但画面一直 Loading
现象:m3u8 和 ts 都能拉到,视频却一直转圈,OpenSuccess 事件迟迟不触发。 原因:多半是编码格式问题。海康新设备默认主码流可能是 H.265,而 Chrome、Edge 在 MSE 里对 H.265 支持很差,解出来全是灰屏或卡 loading。另一个常见原因是 m3u8 里的 ts 路径写成了绝对地址,指向换环境后访问不到的内网 IP。 解决:先用 ffprobe 确认实际编码格式:
ffprobe -v error -show_streams -select_streams v \ -show_entries stream=codec_name,width,height \ -of json http://localhost:8080/hls/channel_101.m3u8看到 codec_name 是 hevc 就是踩中 H.265,把 ffmpeg 的 -c:v copy 改成 -c:v libx264 -profile:v baseline -level 3.0 强制转 H.264。ts 路径问题则要在 ffmpeg 里配 hls_base_url 指向正确的域名前缀,保证切片的相对路径能在浏览器里解析。
5.3 连续切流后白屏、内存只涨不跌
现象:切到第 5、6 路以后,新画面出不来,旧画面还停在屏幕上,任务管理器里浏览器内存肉眼可见地上涨。 原因:切流时只调了 OpenVideoFromFile,没有先 Stop 再 CloseMedia。AVProVideo 在 WebGL 下会保留上一路的解码上下文和纹理引用,Unity 侧 GC 又拿不到浏览器内存,累积起来就是白屏。 解决:切流顺序固定为:先把 RawImage.texture 置空,再 Stop,最后 Open。彻底不用的通道直接 CloseMedia,并加一层引用计数防止 m_ResolveTexture 被上一路拽着不放手:
_display.texture = null; _player.Stop(); _player.CloseMedia();这个顺序在 Demo 里看着多余,但多路切换场景下缺一步都会出问题。我给 RawImage 置空是为了主动断掉 Texture 引用,让后续纹理可以被回收,而不是等 GC 碰运气。
5.4 海康拉流 401 / 连接数超限
现象:ffmpeg 或设备日志里出现 401 Unauthorized,或者网关报连接数已达上限,部分通道黑屏。 原因:两个来源。一是密码里有特殊字符,RTSP URL 解析错位导致认证失败;二是设备默认并发取流数有限,常见在 6 路上下,九宫格超过上限后设备开始拒绝新连接。 解决:密码里的特殊字符统一做 URL 编码,或者在海康 ISAPI 里单独建一个只读取流账号,避免业务改密影响所有通道。并发控制由播放端来做:九宫格只拉 sub 子码流,点击放大才临时切 main;同时及时关闭空闲连接,防止设备侧会话一直挂着不释放。
5.5 HTTPS 页面拉 HTTP 流被浏览器拦截
现象:Unity 页面部署在 HTTPS 域名下,HLS 流地址是 http:// 开头,浏览器直接拦 Mixed Content,Console 里一行醒目的红色报错。 原因:这是浏览器安全策略,HTTPS 页面里拉 HTTP 资源属于混合内容,视频被划为高风险资源,默认直接屏蔽。 解决:最省事的做法是让 HLS 也走 HTTPS,由 Nginx 统一做 TLS 终结;纯内网演示不想上证书,就让 Unity 页面和 HLS 统一走 HTTP,保持协议一致。判断标准就一条:页面协议和流协议必须相同,一个 http 一个 https 必挂。
6. 二次开发:从单路 demo 到多路监控大屏
demo 跑通只是起点,上生产大概率要面对多路、多设备、可配置三个需求。这里给三条能直接落地的改造思路。
6.1 把通道表改成 JSON 驱动
demo 的通道数据是代码里写死的,改成外部配置后,加摄像头不用改代码重新打包:
[ { "name": "东门岗", "ip": "192.168.1.64", "channelNo": 1, "streamType": "sub" }, { "name": "西门岗", "ip": "192.168.1.64", "channelNo": 2, "streamType": "sub" }, { "name": "机房", "ip": "192.168.1.65", "channelNo": 1, "streamType": "main" } ]加载时注意 JsonUtility 不支持数组作为根节点,要包一层:
[Serializable] public class SourceConfig { public StreamSource[] channels; } StreamSource[] list = JsonUtility.FromJson<SourceConfig>(jsonText).channels;配置放 StreamingAssets 还是服务器下发,取决于要不要远程改通道。大屏项目建议放服务器,Unity 侧启动时拉一次,加设备或换 IP 只改配置文件,不动客户端。
6.2 单播放器复用还是播放器池
九路画面如果每格挂一个 MediaPlayer,WebGL 下解码上下文数量会和纹理一起失控,这也是 5.3 节白屏的来源。我一般维护 2 到 4 个 MediaPlayer 的池子,按当前激活的格子数分配,切到哪路就从池里取空闲的播放器,超出上限的格子排队等待。这样九宫格能看,解码上下文数量又被按住。
6.3 上线前的验证清单
每次改完流链路,我会把这几项过一遍再交付:Chrome 和 Edge 各跑一轮看 OpenSuccess 是否稳定触发;Safari 上确认 m3u8 的 MIME 类型正常;用 ffprobe 核对码流是 H.264;切换通道后看内存曲线是否回落;最后拔一次网线验证断流重连能救回来。从那以后,我每次接监控大屏需求,都先花半小时把编码格式、跨域头、并发取流三个雷排掉,再写播放器代码。链路通了,播放器只是最后一块拼图;希望这篇笔记帮到你。
本文还有配套的精品资源,点击获取