ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Unity WebGL集成海康摄像头:AVProVideo与跨域代理实战

Unity WebGL集成海康摄像头:AVProVideo与跨域代理实战

1. 项目概述与核心挑战

最近在做一个智慧工厂的数字孪生项目,客户要求在WebGL平台上,将厂区里几十个海康威视摄像头的实时监控画面,无缝集成到Unity构建的3D场景中。这个需求听起来很直接,但真正动手做,才发现从摄像头拉流到Unity WebGL页面稳定播放,中间隔着一道道“天堑”。最核心的难题有两个:一是Unity WebGL环境对网络请求有严格的跨域限制,直接请求摄像头的RTSP或HTTP流会被浏览器安全策略拦截;二是WebGL平台本身没有原生的视频解码能力,需要借助插件来处理视频流。

经过一番技术选型和踩坑,最终我们确定了以AVProVideo插件为核心,结合自定义HTTP代理服务来解决跨域问题的技术方案。这篇文章,我就来详细拆解这个方案的完整实现路径,包括AVProVideo的配置、海康摄像头流的获取与转换、跨域代理服务的搭建,以及最终在WebGL平台上的部署与优化。无论你是正在做类似的数字孪生、智慧园区项目,还是单纯需要在WebGL中播放网络视频流,相信这篇实战记录都能给你提供直接的参考。

2. 技术方案选型与核心思路拆解

2.1 为什么选择AVProVideo?

在Unity中播放视频,常见的方案有Unity自带的VideoPlayer、第三方插件如AVPro Video、Unity WebRTC等。针对WebGL平台播放海康摄像头流这个特定场景,我们逐一分析:

  • Unity原生VideoPlayer:在桌面端和移动端表现尚可,但在WebGL平台,它严重依赖浏览器的HTML5<video>标签。问题在于,海康摄像头提供的流(无论是RTSP over HTTP,还是HLS/m3u8)通常需要特定的HTTP请求头(如Authorization)或处理跨域预检请求,而浏览器的<video>标签对这些自定义HTTP行为的控制能力极弱,几乎无法应对需要鉴权的摄像头流。因此,首先排除。

  • Unity WebRTC:这是一个更“原生”的实时流媒体方案,理论上延迟更低。但它的复杂度呈指数级上升。你需要搭建一个信令服务器(Signaling Server)和至少一个媒体服务器(如Janus, Mediasoup)来转发WebRTC流。海康摄像头的RTSP流需要先通过类似rtsp-simple-serverGStreamer的工具转码成WebRTC可接受的格式(如VP8/VP9/H.264 over RTP),再通过媒体服务器分发给WebGL客户端。这套架构对于几十个摄像头的项目来说,在服务器资源、网络带宽和维护成本上都过高,属于“杀鸡用牛刀”。

  • AVPro Video:这是我们的最终选择。AVPro Video是一个功能强大的商业插件,它的核心优势在于其灵活的网络请求处理机制。它允许开发者通过实现IHttpRequest接口,完全自定义发送给视频源的HTTP请求。这意味着我们可以拦截AVProVideo发起的每一次视频请求,手动为其添加海康摄像头所需的认证头(如Authorization: Basic ...),或者更关键地,将请求重定向到我们自己的、解决了跨域问题的代理服务器。在WebGL平台,AVPro Video会使用浏览器的Media Source Extensions技术进行软解码,性能足以应对多路720P/1080P的直播流。其“以插件处理网络,以浏览器解码视频”的架构,在功能、性能和开发复杂度上取得了最佳平衡。

注意:AVPro Video是付费插件,但其提供的强大控制和WebGL兼容性,对于企业级项目来说是值得的投资。社区也有一些开源替代方案,但通常功能完整性和官方支持度不及AVPro。

2.2 整体技术架构设计

我们的目标是在Unity WebGL应用中稳定播放http://[摄像头IP]:[端口]/rtsp_over_http/...这类URL的视频流。整体架构分为三部分:

  1. 前端(Unity WebGL应用)

    • 使用AVPro Video的MediaPlayer组件播放视频。
    • 通过自定义的HttpRequest实现,将所有指向摄像头IP的视频请求,重定向到我们部署的同源代理服务器
  2. 代理服务器(关键中间层)

    • 这是一个运行在与WebGL应用同域名、同端口下的后端服务(例如,你的WebGL应用部署在https://your-app.com,代理服务就运行在https://your-app.com/api/proxy/)。
    • 它的职责是接收来自Unity WebGL应用的视频请求,然后以服务器身份(无跨域限制)去请求真实的海康摄像头流,获取流数据后再原样返回给前端。
  3. 数据源(海康威视摄像头)

    • 摄像头配置好网络,开启RTSP或HTTP流服务。通常需要获取其RTSP URL(如rtsp://admin:password@192.168.1.100:554/h264/ch1/main/av_stream)或经过封装后的HTTP流地址。

工作流程: Unity WebGL -> (发起视频请求) -> 自定义HttpRequest -> (将请求URL改写为代理服务器地址) -> 代理服务器 -> (携带认证信息请求真实摄像头) -> 海康摄像头 -> (返回视频流) -> 代理服务器 -> (返回流数据给前端) -> AVPro Video解码播放。

这个架构的核心价值在于,通过一个简单的同源代理,完美规避了浏览器的跨域安全策略,同时将复杂的摄像头认证逻辑从不可控的浏览器环境转移到了完全可控的服务器端。

3. 核心组件配置与实操要点

3.1 海康摄像头端配置要点

在对接前,确保摄像头网络可达且流地址正确。以下是通过海康摄像头Web管理界面获取流信息的典型步骤:

  1. 登录与网络配置:通过浏览器访问摄像头IP,使用管理员账号登录。在【网络】->【基本配置】中,确认IP地址、子网掩码、网关设置正确,确保从你的代理服务器所在网络能够ping通该IP。
  2. 获取RTSP流地址:进入【配置】->【视音频】->【视频流】,查看主码流参数。海康摄像头的RTSP URL通常有固定格式:
    • 主码流rtsp://[username]:[password]@[ip]:[port]/h264/ch1/main/av_stream
    • 子码流rtsp://[username]:[password]@[ip]:[port]/h264/ch1/sub/av_stream
    • 其中ch1代表通道1,多通道设备可能是ch2,ch3等。554是RTSP默认端口。
  3. 启用并测试HTTP流:部分项目更倾向于使用HTTP流,因为它更易于在Web环境中处理。海康摄像头可能支持通过特定路径提供RTSP over HTTP的服务,例如http://[ip]:[port]/rtsp_over_http/...http://[ip]:[port]/ISAPI/Streaming/channels/101。这需要在摄像头管理界面查找“HTTP服务”或“ISAPI”相关设置,并启用。一个非常重要的测试步骤:在服务器端(或一个能与摄像头通信的机器上),使用curlffplay命令测试这个HTTP流地址是否能直接拉通,并检查返回的头信息(如Content-Type: video/mp4)。
# 示例:使用curl测试HTTP流,并只获取头信息 curl -I -u admin:your_password "http://192.168.1.100:80/ISAPI/Streaming/channels/101" # 期望看到 HTTP/1.1 200 OK 以及 Content-Type: video/mp4 之类的信息
  1. 安全与认证:建议为视频流访问创建独立的、权限最低的用户,避免使用最高权限的admin账号。同时,如果摄像头支持,可以设置IP地址过滤,只允许代理服务器的IP访问流地址。

3.2 AVPro Video插件基础配置

在Unity中导入AVPro Video插件后,按照以下步骤进行基础配置:

  1. 创建MediaPlayer与Display:在场景中创建一个空物体,为其添加MediaPlayer组件。然后,创建一个UI Image或RawImage对象,为其添加ApplyToMaterialDisplayUGUI组件(取决于你是在3D物体表面还是UI上显示视频),并将该组件的Media Player字段拖拽指向刚才创建的MediaPlayer组件。
  2. MediaPlayer关键设置
    • Media Source: 选择PathURL。初期测试可以用一个公开的HTTP MP4视频URL。但我们的重点在于通过代码动态设置。
    • Auto Start: 取消勾选。我们将通过代码在准备好后控制播放。
    • Video API: 在Windows/Mac/Linux标签下,选择MediaFoundationDirectShow以获得更好的本地播放性能。但对于WebGL,这个设置无效,因为WebGL平台使用浏览器自己的解码路径。
    • Audio Output: 根据需求设置,监控流通常无声,可以关闭。
  3. 编写播放控制脚本:创建一个脚本,挂载到MediaPlayer对象上,用于动态加载和播放视频流。
using UnityEngine; using RenderHeads.Media.AVProVideo; public class HikvisionStreamPlayer : MonoBehaviour { public MediaPlayer mediaPlayer; public string streamUrl; // 例如:http://your-proxy-server/proxy?targetUrl=encoded_camera_url void Start() { if (mediaPlayer == null) mediaPlayer = GetComponent<MediaPlayer>(); if (!string.IsNullOrEmpty(streamUrl)) { // 设置媒体源 mediaPlayer.OpenMedia(MediaPathType.AbsolutePathOrURL, streamUrl, true); } } // 可以在UI按钮事件中调用 public void ChangeStream(string newUrl) { if (mediaPlayer != null) { mediaPlayer.CloseMedia(); mediaPlayer.OpenMedia(MediaPathType.AbsolutePathOrURL, newUrl, true); } } }

至此,一个基础的AVPro Video播放器就配置好了。但直接填入海康摄像头的HTTP流地址,在WebGL中必然会因跨域或401未授权而失败。接下来就是最关键的环节:实现自定义的HTTP请求处理器。

4. 跨域代理服务的实现与集成

4.1 实现自定义的IHttpRequest(Unity端)

AVPro Video允许我们通过MediaPlayer.EventMediaPlayerCreateHttpRequest事件来注入自定义的HTTP请求逻辑。我们需要创建一个实现IHttpRequest接口的类。

using System; using System.Collections.Generic; using RenderHeads.Media.AVProVideo; public class CustomHttpRequest : IHttpRequest { private string _url; private Dictionary<string, string> _headers; // 代理服务器的基地址 private static readonly string ProxyBaseUrl = "https://your-webgl-domain.com/api/proxy?url="; public CustomHttpRequest(string url) { // 将原始的视频URL进行编码,并拼接到代理服务器地址后面 _url = ProxyBaseUrl + Uri.EscapeDataString(url); _headers = new Dictionary<string, string>(); } public void SetHeader(string name, string value) { // 这里可以设置一些通用的头,但针对摄像头的认证头,我们将在代理服务器添加 _headers[name] = value; } public string GetUrl() { return _url; } public Dictionary<string, string> GetHeaders() { return _headers; } public void Send() { /* AVPro内部处理 */ } public void Abort() { /* AVPro内部处理 */ } // ... 需要实现接口的其他方法,如GetError(), GetResponseCode(), GetBytes()等,但通常AVPro会处理。 } // 在MediaPlayer初始化时注册 void SetupMediaPlayer() { if (mediaPlayer != null) { mediaPlayer.Events.AddListener(OnMediaPlayerEvent); } } void OnMediaPlayerEvent(MediaPlayer mp, MediaPlayerEvent.EventType et, ErrorCode errorCode) { if (et == MediaPlayerEvent.EventType.MediaPlayerCreateHttpRequest) { // 当AVPro需要创建HTTP请求时,提供我们的自定义实现 mp.CreateHttpRequest = (string url) => new CustomHttpRequest(url); } }

这段代码的核心是CustomHttpRequest的构造函数。它把原本指向海康摄像头的URL(如http://192.168.1.100/...)转换成了指向我们同源代理服务器的URL,并将原始URL作为查询参数传递。这样,浏览器发起的请求就变成了向自己域下的一个接口请求,完美避开了跨域问题。

4.2 搭建Node.js代理服务器(服务器端)

代理服务器可以用任何后端语言实现,这里以Node.js + Express为例,因为它轻量且部署简单。

  1. 初始化项目并安装依赖

    mkdir video-proxy-server && cd video-proxy-server npm init -y npm install express http-proxy-middleware cors basic-auth
  2. 创建代理服务器脚本(server.js)

    const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const auth = require('basic-auth'); const app = express(); const PORT = process.env.PORT || 3000; // 可配置的海康摄像头认证信息(应从环境变量或配置库读取,切勿硬编码) const CAMERA_CREDENTIALS = { '192.168.1.100': { username: 'admin', password: 'camera_password_1' }, '192.168.1.101': { username: 'operator', password: 'camera_password_2' }, // ... 更多摄像头 }; // 中间件:解析前端传递的目标URL,并添加海康摄像头认证头 const proxyMiddleware = createProxyMiddleware({ target: 'http://dummy', // 占位符,实际目标由下面router动态设置 changeOrigin: true, followRedirects: true, selfHandleResponse: false, on: { proxyReq: (proxyReq, req, res) => { // 1. 从查询参数中获取真实的目标摄像头URL const targetUrl = req.query.url; if (!targetUrl) { return res.status(400).send('Missing target URL'); } let parsedUrl; try { parsedUrl = new URL(targetUrl); } catch (e) { return res.status(400).send('Invalid target URL'); } // 2. 根据摄像头IP,获取对应的认证信息 const cameraHost = parsedUrl.hostname; const creds = CAMERA_CREDENTIALS[cameraHost]; if (creds) { // 构造Basic Auth头 const authString = Buffer.from(`${creds.username}:${creds.password}`).toString('base64'); proxyReq.setHeader('Authorization', `Basic ${authString}`); } // 3. 可选:添加或覆盖其他必要的请求头 // 例如,海康某些接口可能需要 `User-Agent` 或 `Accept` proxyReq.setHeader('User-Agent', 'Video-Proxy-Server/1.0'); proxyReq.removeHeader('origin'); // 移除可能引起问题的origin头 // 4. 非常重要:设置代理请求的目标URL proxyReq.path = parsedUrl.pathname + parsedUrl.search; proxyReq.host = parsedUrl.hostname; proxyReq.port = parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80); }, proxyRes: (proxyRes, req, res) => { // 5. 关键:设置响应头,允许WebGL前端跨域访问(因为现在是同源,但习惯性加上) res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS'); // 确保内容类型正确传递 if (proxyRes.headers['content-type']) { res.setHeader('Content-Type', proxyRes.headers['content-type']); } // 处理分块传输(对于直播流很重要) if (proxyRes.headers['transfer-encoding'] === 'chunked') { res.setHeader('Transfer-Encoding', 'chunked'); } }, error: (err, req, res) => { console.error('Proxy error:', err); res.status(500).send('Proxy error occurred.'); } } }); // 路由:所有到 /api/proxy 的请求都走代理中间件 app.use('/api/proxy', proxyMiddleware); // 健康检查端点 app.get('/health', (req, res) => { res.status(200).send('OK'); }); app.listen(PORT, () => { console.log(`Video proxy server running on port ${PORT}`); });
  3. 部署与运行

    • 将上述代码部署到你的WebGL应用所在的服务器或后端服务集群中。确保该服务可以通过https://your-webgl-domain.com/api/proxy访问。
    • 使用pm2systemd等工具守护进程。
    • 在服务器防火墙或安全组中,确保该服务器能访问内网中的所有海康摄像头IP和端口。

4.3 安全加固与优化建议

  1. 认证信息管理:绝对不要将摄像头密码硬编码在代码中。上述示例仅为说明。应使用环境变量、密钥管理服务(如AWS Secrets Manager, HashiCorp Vault)或安全的配置文件。
  2. 请求验证:代理服务器应验证前端传入的targetUrl参数,限制只能代理到预设的白名单IP或域名,防止服务器被滥用为开放代理。
  3. 速率限制与缓存:对于多用户访问同一路摄像头的情况,可以在代理层实现简单的流缓存,避免对摄像头发起重复请求。同时,对客户端IP进行速率限制,防止恶意请求。
  4. HTTPS:确保代理服务器使用HTTPS,以保证视频流数据在传输过程中的安全。
  5. 超时与重试:在代理配置中设置合理的超时时间(如proxyTimeout: 30000),并考虑实现简单的重试逻辑,以应对网络波动。

5. Unity WebGL打包与部署实战

5.1 WebGL播放器设置

在Unity Editor中,切换到WebGL平台后,需要进行关键设置:

  1. Player Settings:
    • Resolution and Presentation: 取消勾选Run In Background,对于监控看板,通常需要一直运行。
    • Publishing Settings:
      • Compression Format: 选择Brotli以获得更小的构建包,但需要服务器支持。Gzip兼容性更好。
      • Data Caching: 勾选,可以缓存资源,提升重复访问体验。
      • Enable Exceptions: 建议选择Full Without StacktraceFull,以便在浏览器控制台看到更详细的错误信息,方便调试。
  2. AVPro Video for WebGL:
    • 导入AVPro Video后,确保其WebGL模块已正确安装。检查AVProVideo/Runtime/Scripts/Platforms/WebGL下的文件是否存在。
    • MediaPlayer组件的Platform Options中,找到WebGL部分,确保Use Texture等选项配置正确。通常默认即可。

5.2 构建、部署与Nginx配置

  1. 构建:执行Build,生成包含index.html,.js,.data,.wasm等文件的Build文件夹。
  2. 部署:将整个Build文件夹的内容上传到你的Web服务器(如Nginx, Apache)的静态文件目录下。
  3. Nginx配置示例:以下配置展示了如何同时提供Unity WebGL静态文件和代理API服务。
server { listen 443 ssl http2; server_name your-webgl-domain.com; ssl_certificate /path/to/your/cert.pem; ssl_certificate_key /path/to/your/key.pem; # 1. 静态文件服务 (Unity WebGL构建产物) location / { root /var/www/unity-webgl-build; # 你的构建文件目录 index index.html index.htm; try_files $uri $uri/ /index.html; # 对于WebGL的.data和.wasm文件,需要正确的MIME类型 include /etc/nginx/mime.types; types { application/wasm wasm; application/octet-stream data; } # 设置缓存和CORS(虽然同源,但好习惯) add_header Cache-Control 'no-cache, no-store, must-revalidate'; add_header Access-Control-Allow-Origin '*'; } # 2. 视频流代理API (非常重要!) location /api/proxy { # 将请求转发到我们刚才搭建的Node.js代理服务 proxy_pass http://localhost:3000; # Node.js服务监听的地址 proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 以下头信息对于视频流传输至关重要 proxy_set_header Connection ''; proxy_buffering off; proxy_cache off; proxy_read_timeout 3600s; # 长超时,适应直播流 proxy_send_timeout 3600s; # 禁用Nginx对后端响应体的处理,直接透传 chunked_transfer_encoding on; proxy_set_header Accept-Encoding ''; proxy_pass_request_headers on; proxy_pass_request_body on; } # 可选:健康检查 location /health { proxy_pass http://localhost:3000/health; } }

这个配置的关键在于/api/proxy这个 location 块。proxy_buffering off;chunked_transfer_encoding on;这两条指令对于直播流的实时性至关重要,它们确保了视频数据能够以“流”的形式从摄像头经过代理服务器,再实时地传递到浏览器,而不是被Nginx缓冲成完整的文件再发送。

6. 常见问题排查与性能优化实录

6.1 问题排查清单

在开发和部署过程中,你几乎一定会遇到以下问题。这里提供一套排查思路:

问题现象可能原因排查步骤
Unity WebGL页面黑屏,无视频1. 代理服务器未工作或地址错误。
2. 摄像头认证失败。
3. AVPro Video自定义请求未生效。
4. 视频流格式AVPro不支持。
1. 打开浏览器开发者工具(F12)的Network标签页。刷新页面,查看是否有向/api/proxy?url=...发起的请求。如果没有,检查Unity脚本中ProxyBaseUrl配置。如果有,查看请求状态码。
2. 如果状态码是401,说明代理服务器添加Basic Auth失败。检查代理服务器日志,确认CAMERA_CREDENTIALS配置和摄像头IP匹配,密码正确。
3. 如果状态码是404,说明代理服务器未能正确转发到摄像头。在代理服务器上用curl命令手动测试拼接后的目标URL。
4. 如果状态码是200,但视频仍黑屏,查看响应头Content-Type。如果是video/mp4video/x-flv等,AVPro通常支持。尝试在MediaPlayer的Events中监听Error事件,查看具体错误信息。
视频播放卡顿、延迟高1. 网络带宽不足。
2. 摄像头码流过高。
3. 代理服务器或浏览器解码性能瓶颈。
4. Nginx开启了缓冲。
1. 在摄像头管理后台,将子码流分辨率码率调低(如720P, 1Mbps)。在Unity中播放子码流地址。
2. 确保Nginx配置中proxy_buffering off;
3. 在浏览器任务管理器中,检查该标签页的CPU和网络占用。如果CPU持续很高,可能是浏览器软解码多路高清流压力大,考虑减少同屏播放的摄像头数量,或使用画中画、分页加载。
4. 检查代理服务器所在机器的CPU和网络IO。
视频播放几秒后自动停止1. 摄像头流本身是图片轮询,非真正流。
2. 代理服务器或网络连接超时中断。
1. 用VLC播放器直接打开摄像头流地址,看是否也是播放几秒就停。如果是,可能是摄像头配置成了“快照”模式,需要改为“连续流”模式。
2. 增加代理服务器的超时设置(如上述Nginx配置中的proxy_read_timeout)。
WebGL构建后,视频功能失效1. 自定义IHttpRequest代码在Release构建中被优化掉。
2. 代理服务器地址在构建后是硬编码,与部署环境不符。
1. 确保注册CreateHttpRequest事件的代码在AwakeStart中执行,且相关对象在场景中不会被意外销毁。
2.强烈建议将代理服务器的基地址(ProxyBaseUrl)做成可配置项,例如通过Application.absoluteURL解析或从外部JSON配置文件加载,避免重新打包。

6.2 性能优化与进阶技巧

  1. 流媒体协议优选:如果海康摄像头支持HLS (m3u8)DASH输出,优先使用它们。这两种协议是专为自适应码流和网络传输设计的,在Web环境中兼容性更好。AVPro Video对HLS有很好的支持。只需将代理目标URL指向摄像头的.m3u8播放列表地址即可。
  2. 多路流管理与性能
    • 按需加载:不要一次性加载所有摄像头流。可以根据3D场景中摄像机的视锥体(Frustum)进行裁剪,只加载在视野内的摄像头流。或者实现一个简单的“分页”或“楼层切换”逻辑。
    • 降低分辨率:在数字孪生中,监控视频往往作为画中画或小窗口显示,不需要1080P全高清。坚持使用摄像头的子码流,能极大减轻网络和解码压力。
    • 控制播放状态:当视频窗口最小化或移出屏幕时,调用mediaPlayer.Pause()mediaPlayer.Stop()来释放资源。
  3. AVPro Video高级设置
    • Buffer:调整MediaPlayerBuffer相关设置(如Buffer SizeBuffer Duration)。对于直播,较小的缓冲区可以减少延迟,但可能增加卡顿风险。需要根据网络状况权衡。
    • Render Thread:在非WebGL平台,可以尝试开启Use Render Thread以获得更好的性能,但在WebGL中此选项无效。
  4. 代理服务器负载均衡:如果摄像头数量非常多(上百路),单个代理服务器可能成为瓶颈。可以考虑使用Nginx作为负载均衡器,将视频代理请求分发到多个后端的Node.js代理实例上。

这个方案我们已经在一个拥有超过50个摄像头的智慧园区项目中稳定运行了半年。核心体会是,清晰的分层架构(前端播放、代理中转、源流)是稳定性的基石。将复杂的网络和认证问题放到服务器端解决,让前端专注于渲染和交互,是应对WebGL平台各种限制的最有效策略。希望这份详细的实战指南,能帮你顺利打通Unity数字孪生与海康摄像头WebGL直播的任督二脉。

返回列表