Unity WebGL播放海康RTSP:转HLS全流程方案与避坑指南
简介面向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 的补零海康协议里通道号固定两位channelNo1 时必须拼成 01拼成 1 直接取流失败。主码流和子码流的差别也在这里体现主码流清晰但带宽大适合单路全屏子码流分辨率低适合九宫格。实际项目可以做到点击放大时从 sub 切到 maindemo 里留 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 的第三个参数是 autoPlayStartWebGL 下打开即播第二个参数是流地址这里用 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 里这几个参数直接决定播放稳定性设置项推荐值说明Resolution1920 x 1080监控大屏基准按拼接屏实际分辨率调WebGL Memory Size256 MB 以上多路播放建议 512 MB花屏白屏先查这里Compression FormatBrotli包体最小服务器不支持时选 DisabledGraphics APIWebGL 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 页面跑在 :8080HLS 流如果由另一台媒体服务器分发浏览器 video 元素按同源策略会拒绝跨域拉取Console 报 Access-Control-Allow-Origin 缺失。把 HLS 和页面放在同一个 Nginx 下并注入头问题直接消失。m3u8 的 MIME 类型也必须写对写错时浏览器把它当普通文本拒绝解析。ts 片段的 video/mp2t 同理少了它部分内核在 HLS 解析阶段就罢工。add_header 后面带 always 是防止 404 等非 200 响应时头不生效排查跨域时最容易在这上面看走眼。4.3 ffmpeg 拉流切片的参数怎么给# 海康 RTSP 转 HLS2 秒切片、播放列表只保留最近 4 片 ffmpeg -rtsp_transport tcp \ -i rtsp://user:password192.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视频就该出画面。更快的验证在服务器侧用 curlcurl -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 headerNetwork 里 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 streamcodec_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 ContentConsole 里一行醒目的红色报错。 原因这是浏览器安全策略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.FromJsonSourceConfig(jsonText).channels;配置放 StreamingAssets 还是服务器下发取决于要不要远程改通道。大屏项目建议放服务器Unity 侧启动时拉一次加设备或换 IP 只改配置文件不动客户端。6.2 单播放器复用还是播放器池九路画面如果每格挂一个 MediaPlayerWebGL 下解码上下文数量会和纹理一起失控这也是 5.3 节白屏的来源。我一般维护 2 到 4 个 MediaPlayer 的池子按当前激活的格子数分配切到哪路就从池里取空闲的播放器超出上限的格子排队等待。这样九宫格能看解码上下文数量又被按住。6.3 上线前的验证清单每次改完流链路我会把这几项过一遍再交付Chrome 和 Edge 各跑一轮看 OpenSuccess 是否稳定触发Safari 上确认 m3u8 的 MIME 类型正常用 ffprobe 核对码流是 H.264切换通道后看内存曲线是否回落最后拔一次网线验证断流重连能救回来。从那以后我每次接监控大屏需求都先花半小时把编码格式、跨域头、并发取流三个雷排掉再写播放器代码。链路通了播放器只是最后一块拼图希望这篇笔记帮到你。本文还有配套的精品资源点击获取