首页 / 资讯中心 / 文章详情

浏览器直连海康大华天地伟业摄像头:WebCodecs与WebSocket实战

浏览器直连海康大华天地伟业摄像头:WebCodecs与WebSocket实战 ★ FEATURED ARTICLE
1. 项目背景与核心需求拆解1.1 为什么要在浏览器里直接调取监控画面做过安防监控项目的人都有一个共同感受客户不想装客户端。不管是海康、大华还是天地伟业各家都有自己的PC端软件但实际交付的时候甲方往往一句“我打开浏览器就能看就行”把需求打回来。这个诉求听起来简单背后涉及的东西其实不少。传统方案是在服务器上装一套流媒体服务把RTSP转成HLS或者FLV再推给浏览器。这套方案成熟、稳定但部署成本高中小项目根本划不来。而网络摄像头本身大多支持HTTP协议下的CGI接口和私有SDKChrome浏览器又提供了WebSocket、WebRTC、WebCodecs这些能力理论上完全可以绕过中间服务器让浏览器直接跟摄像头对话。这个项目的核心目标就是在Chrome浏览器中通过Web技术直接完成对海康、大华、天地伟业三家主流网络摄像头的预览、抓图、云台控制等二次开发功能不依赖厂商客户端不依赖额外的流媒体服务器。适合有一定前端基础、正在做安防集成项目的开发者参考。1.2 三家厂商的协议差异到底在哪海康、大华、天地伟业虽然都做网络摄像头但各自的接口体系差别很大这是整个项目最头疼的地方。海康走的是ISAPI体系基于HTTP的RESTful风格接口认证方式有HTTP Digest和Bearer Token两种。它的预览流走RTSP但通过ISAPI可以拿到设备信息、通道状态、抓图等。较新的设备还支持WebSocket推送裸流。大华用的是CGI接口体系路径格式跟海康完全不同认证主要靠HTTP Digest。大华的RTSP地址格式、通道编号规则跟海康也不一样云台控制的参数命名更是各搞各的。天地伟业相对小众一些接口文档没有前两家那么完善部分型号走的是私有HTTP接口返回数据格式有时候是XML有时候是JSON需要抓包分析。注意三家厂商的设备型号跨度很大老设备和新设备的接口能力完全不同。动手之前一定要先确认手头设备的具体型号和固件版本否则很容易出现“文档上写支持但实际调不通”的情况。1.3 浏览器端的技术选型思路浏览器要显示视频无非几条路HLS、FLV、WebRTC、WebCodecs。HLS延迟太高监控场景基本不考虑。FLV需要服务端转封装违背了“不依赖中间服务器”的初衷。WebRTC延迟低但摄像头原生不支持WebRTC需要做信令转换。最终我选择的方案是通过WebSocket从摄像头获取裸流数据在浏览器端用WebCodecs解码再渲染到Canvas上。这个方案的好处是延迟可以控制在几百毫秒以内而且完全在浏览器内完成不需要任何服务端参与。缺点是WebCodecs对编码格式有要求必须是H.264或H.265而且Chrome版本要在94以上。对于不支持WebSocket裸流推送的老设备退而求其次的方案是通过HTTP接口定时抓取JPEG图片用类似MJPEG的方式做伪实时预览。虽然帧率低但兼容性最好。2. 核心细节解析与实操要点2.1 海康设备的接口调用细节海康的ISAPI接口认证是第一个坎。大部分设备默认开启HTTP Digest认证直接用fetch发请求会返回401。正确的做法是在fetch的options里带上credentials: include然后手动处理Digest认证的挑战响应。async function hikDigestAuth(url, username, password) { const resp await fetch(url, { method: GET }); if (resp.status 401) { const wwwAuth resp.headers.get(WWW-Authenticate); const realm wwwAuth.match(/realm(.?)/)[1]; const nonce wwwAuth.match(/nonce(.?)/)[1]; const qop wwwAuth.match(/qop(.?)/)?.[1] || ; const ha1 md5(${username}:${realm}:${password}); const ha2 md5(GET:${url}); const nc 00000001; const cnonce Math.random().toString(36).substring(2); const response md5(${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}); const authHeader Digest username${username}, realm${realm}, nonce${nonce}, uri${url}, qop${qop}, nc${nc}, cnonce${cnonce}, response${response}; return fetch(url, { headers: { Authorization: authHeader } }); } return resp; }这段代码的关键点在于MD5的计算顺序海康的Digest实现跟标准RFC 2617基本一致但有些固件版本对qop的处理有差异如果qop为空字符串response的计算公式要去掉qop和nc部分。抓图接口是/ISAPI/Streaming/channels/101/picture其中101代表通道1的主码流。云台控制的接口是/ISAPI/PTZCtrl/channels/1/continuous通过PUT请求发送XML格式的控制指令。2.2 大华设备的CGI接口调用大华的CGI接口路径格式是/cgi-bin/...认证方式同样是HTTP Digest但它的nonce过期时间比较短大概30秒左右所以不能缓存认证信息每次请求都要重新走一遍挑战流程。大华的抓图接口是/cgi-bin/snapshot.cgi?channel1云台控制是/cgi-bin/ptz.cgi?actionstartchannel1codeUparg10arg21arg30。注意大华的云台参数用的是code而不是具体的坐标值这个跟海康的连续移动模式不一样。大华的RTSP地址格式是rtsp://username:passwordip:554/cam/realmonitor?channel1subtype0subtype0是主码流subtype1是子码流。这个地址在浏览器里不能直接播放但可以用来做服务端转发的配置参考。2.3 天地伟业的接口适配天地伟业的设备接口文档相对难找我实际测试下来发现它的HTTP接口路径跟大华有些类似但参数命名不同。抓图接口是/cgi-bin/snapshot.cgi?channel1这个跟大华一样但云台控制的参数格式是/cgi-bin/ptz.cgi?actionstartchannel1commandupspeed5。天地伟业的部分新型号支持ONVIF协议如果设备开启了ONVIF可以通过标准的ONVIF接口来获取设备信息和云台控制这样就不用去适配它的私有接口了。ONVIF的SOAP请求在浏览器端可以通过fetch发送但要注意CORS问题需要在摄像头端开启跨域支持或者通过本地代理转发。2.4 浏览器端的CORS与认证难题浏览器直接访问摄像头IP最大的障碍是CORS。摄像头的HTTP服务默认不会返回Access-Control-Allow-Origin头Chrome会直接拦截响应。解决办法有两个一是通过本地代理服务器转发请求二是在摄像头端修改配置开启CORS支持。海康的部分设备支持在网络-高级配置-HTTP里开启CORS但大部分老设备没有这个选项。大华和天地伟业的情况类似。所以实际项目中我通常会在本地起一个轻量级的Node.js代理专门处理认证和CORS问题。const express require(express); const { createProxyMiddleware } require(http-proxy-middleware); const app express(); app.use(/camera, createProxyMiddleware({ target: http://192.168.1.64, changeOrigin: true, pathRewrite: { ^/camera: }, onProxyReq: (proxyReq, req) { // 在这里处理Digest认证 proxyReq.setHeader(Authorization, buildDigestHeader(req)); } })); app.listen(3000);这个代理方案的好处是前端代码不需要关心认证细节所有请求都走代理代理层统一处理Digest认证和CORS头。缺点是增加了一个中间层部署的时候需要多跑一个Node进程。实操心得如果项目允许尽量在摄像头端开启CORS和ONVIF这样前端代码可以大大简化。如果不行代理方案是最稳妥的选择。不要试图用no-cors模式绕过那个模式拿不到响应内容等于白搭。3. 实操过程与核心环节实现3.1 环境准备与设备配置动手之前需要准备的东西一台支持WebSocket或HTTP接口的网络摄像头海康DS-2CD系列、大华IPC-HFW系列、天地伟业TC-NC系列都可以一台运行Chrome 94以上版本的电脑Node.js环境用于跑本地代理以及摄像头的管理员账号密码。设备端的配置步骤首先登录摄像头Web管理界面在网络-高级配置-HTTP里确认HTTP服务已开启端口默认是80。然后在网络-高级配置-RTSP里确认RTSP服务已开启端口默认是554。如果设备支持ONVIF在网络-高级配置-集成协议里开启ONVIF并创建一个ONVIF用户。接下来在系统-安全管理-用户管理里确认管理员账号有远程访问权限。有些设备默认只允许本地访问需要手动开启远程访问。最后在网络-基本配置-TCP/IP里确认摄像头的IP地址跟电脑在同一网段。3.2 本地代理服务的搭建代理服务用Node.js写核心依赖是express和http-proxy-middleware。先初始化项目mkdir camera-proxy cd camera-proxy npm init -y npm install express http-proxy-middleware md5然后创建server.js实现Digest认证的自动处理const express require(express); const crypto require(crypto); const { createProxyMiddleware } require(http-proxy-middleware); const app express(); const CAMERA_IP 192.168.1.64; const USERNAME admin; const PASSWORD your_password; function md5(str) { return crypto.createHash(md5).update(str).digest(hex); } function buildDigestHeader(method, uri, wwwAuth) { const realm wwwAuth.match(/realm(.?)/)[1]; const nonce wwwAuth.match(/nonce(.?)/)[1]; const qop wwwAuth.match(/qop(.?)/)?.[1] || ; const ha1 md5(${USERNAME}:${realm}:${PASSWORD}); const ha2 md5(${method}:${uri}); const nc 00000001; const cnonce Math.random().toString(36).substring(2); let response; if (qop) { response md5(${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}); } else { response md5(${ha1}:${nonce}:${ha2}); } return Digest username${USERNAME}, realm${realm}, nonce${nonce}, uri${uri}, qop${qop}, nc${nc}, cnonce${cnonce}, response${response}; } app.use(/camera, async (req, res, next) { const targetUrl http://${CAMERA_IP}${req.url}; try { let resp await fetch(targetUrl, { method: req.method }); if (resp.status 401) { const wwwAuth resp.headers.get(WWW-Authenticate); const authHeader buildDigestHeader(req.method, req.url, wwwAuth); resp await fetch(targetUrl, { method: req.method, headers: { Authorization: authHeader } }); } res.set(Access-Control-Allow-Origin, *); res.set(Content-Type, resp.headers.get(Content-Type)); const buffer await resp.arrayBuffer(); res.send(Buffer.from(buffer)); } catch (err) { res.status(500).json({ error: err.message }); } }); app.listen(3000, () { console.log(Proxy server running on port 3000); });这个代理服务启动后前端只需要请求http://localhost:3000/camera/ISAPI/...就能拿到摄像头的数据认证和CORS问题都在代理层解决了。3.3 前端预览页面的实现前端页面用原生JavaScript写核心是Canvas渲染和WebSocket数据接收。先创建一个简单的HTML结构!DOCTYPE html html head title摄像头预览/title style #videoCanvas { width: 640px; height: 360px; background: #000; } #controls { margin-top: 10px; } button { margin: 5px; padding: 8px 16px; } /style /head body canvas idvideoCanvas width640 height360/canvas div idcontrols button onclickptzControl(up)上/button button onclickptzControl(down)下/button button onclickptzControl(left)左/button button onclickptzControl(right)右/button button onclickcaptureSnapshot()抓图/button /div script srcapp.js/script /body /htmlapp.js里实现核心逻辑。首先是抓图功能async function captureSnapshot() { const resp await fetch(/camera/ISAPI/Streaming/channels/101/picture); const blob await resp.blob(); const url URL.createObjectURL(blob); const img new Image(); img.onload () { const canvas document.getElementById(videoCanvas); const ctx canvas.getContext(2d); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); URL.revokeObjectURL(url); }; img.src url; }云台控制功能async function ptzControl(direction) { const ptzXml ?xml version1.0 encodingUTF-8? PTZData pan${direction left ? -1 : direction right ? 1 : 0}/pan tilt${direction up ? 1 : direction down ? -1 : 0}/tilt zoom0/zoom /PTZData; await fetch(/camera/ISAPI/PTZCtrl/channels/1/continuous, { method: PUT, headers: { Content-Type: application/xml }, body: ptzXml }); // 停止云台 setTimeout(async () { await fetch(/camera/ISAPI/PTZCtrl/channels/1/continuous, { method: PUT, headers: { Content-Type: application/xml }, body: ?xml version1.0 encodingUTF-8? PTZDatapan0/pantilt0/tiltzoom0/zoom/PTZData }); }, 500); }3.4 WebSocket裸流接收与WebCodecs解码对于支持WebSocket推送裸流的海康设备可以通过ws://ip:port/...接收H.264数据。海康的WebSocket推送地址通常是ws://ip:7681/...具体路径需要抓包确认。const ws new WebSocket(ws://192.168.1.64:7681/live); ws.onmessage async (event) { const data new Uint8Array(event.data); // 解析H.264 NAL单元 const nalUnits parseH264NALUnits(data); for (const nal of nalUnits) { if (nal.type 5) { // IDR帧 await decoder.configure({ codec: avc1.42E01E, width: 1920, height: 1080 }); } const chunk new EncodedVideoChunk({ type: nal.type 5 ? key : delta, timestamp: performance.now() * 1000, data: nal.data }); decoder.decode(chunk); } }; const decoder new VideoDecoder({ output: (frame) { const canvas document.getElementById(videoCanvas); const ctx canvas.getContext(2d); ctx.drawImage(frame, 0, 0, canvas.width, canvas.height); frame.close(); }, error: (e) console.error(Decoder error:, e) });这段代码的关键在于H.264 NAL单元的解析。海康的WebSocket推送数据通常是裸的H.264流需要按照起始码00 00 00 01来分割NAL单元然后根据NAL类型判断是关键帧还是普通帧。关键帧需要先配置解码器普通帧直接送入解码。注意WebCodecs的VideoDecoder在Chrome 94以上才支持而且不同版本的Chrome对H.264的支持有差异。如果遇到解码失败先检查Chrome版本再检查摄像头的编码格式是不是H.264 High Profile有些设备默认用Main Profile需要手动改成Baseline或者High。4. 常见问题与排查技巧实录4.1 认证失败与401错误排查认证问题是最高频的坑。表现是浏览器控制台一直返回401但用Postman测试同样的账号密码却能通过。原因通常是浏览器的fetch没有正确处理Digest认证的挑战响应。排查步骤首先在Chrome开发者工具的Network面板里查看请求的Response Headers确认WWW-Authenticate头的值。如果realm或nonce跟预期不符说明摄像头的认证配置有变化。然后检查代理服务里的MD5计算逻辑特别是qop为空的情况。最后确认账号密码里有没有特殊字符比如、#这些如果有需要在MD5计算前做URL编码。还有一个隐蔽的坑海康的部分设备在连续多次认证失败后会锁定IP一段时间通常是5分钟。如果调试过程中突然所有请求都返回403大概率是被锁了等5分钟再试。4.2 视频流卡顿与延迟优化WebSocket裸流方案在实际测试中可能会出现卡顿表现为画面一顿一顿的或者延迟越来越大。原因通常有三个网络带宽不足、解码器配置不当、Canvas渲染频率过高。带宽方面1080P的H.264主码流码率通常在4Mbps左右如果网络环境不好建议切换到子码流码率可以降到1Mbps以下。海康的子码流地址是/ISAPI/Streaming/channels/102/picture注意通道号从101变成102。解码器配置方面VideoDecoder.configure里的codec参数必须跟实际编码格式匹配。H.264 Baseline是avc1.42E01EMain是avc1.4D401EHigh是avc1.64001E。如果配错了解码器会直接报错。Canvas渲染方面不要每一帧都重绘可以用requestAnimationFrame做节流把渲染频率控制在30fps左右。如果解码出来的帧率高于30fps多余的帧直接丢弃。4.3 云台控制无响应或方向错乱云台控制的问题通常出在参数格式上。海康的PTZ XML里pan和tilt的取值范围是-1到1但有些设备实际支持的范围是-100到100。如果发送-1云台可能只动一点点看起来像没反应。解决办法是先发送一个较大的值测试比如pan100看云台是否正常转动。如果转动正常再根据实际需求调整速度值。大华的云台参数用的是code比如Up、Down、Left、Right注意大小写敏感up和Up可能效果不一样。天地伟业的云台参数里有一个speed字段取值范围通常是1到10默认是5。如果云台不动先检查speed是不是0。4.4 常见问题速查表问题现象可能原因排查方法解决方案401认证失败Digest计算错误检查WWW-Authenticate头修正MD5计算逻辑视频卡顿带宽不足查看码率切换子码流解码报错codec参数不匹配查看编码格式修正codec字符串云台不动参数范围错误测试大值调整参数范围CORS拦截摄像头未开启CORS查看控制台使用本地代理WebSocket连不上端口未开放telnet测试开启WebSocket服务4.5 独家避坑技巧第一个技巧海康的ISAPI接口在返回XML的时候有些设备会在XML前面加一个BOM头导致DOMParser解析失败。解决办法是在解析前先用replace(/^\uFEFF/, )去掉BOM。第二个技巧大华的CGI接口在并发请求超过5个的时候会返回503所以前端不要同时发太多请求最好做一个请求队列串行发送。第三个技巧天地伟业的设备在重启后HTTP服务可能需要30秒才能完全启动如果重启后立即请求会返回连接拒绝。建议在代码里加一个重试机制间隔2秒重试最多重试10次。第四个技巧Chrome的WebCodecs在解码H.265的时候需要额外的硬件支持如果电脑没有H.265硬解解码会非常卡。建议在代码里检测VideoDecoder.isConfigSupported如果不支持H.265自动切换到H.264子码流。const support await VideoDecoder.isConfigSupported({ codec: hvc1.1.6.L93.B0, width: 1920, height: 1080 }); if (!support.supported) { console.warn(H.265 not supported, switching to H.264); // 切换到H.264子码流 }这套方案我在几个中小型监控项目里实际跑过海康和大华的设备兼容性最好天地伟业需要多花点时间抓包分析。整体延迟可以控制在500毫秒以内抓图和云台控制的响应速度也够用。唯一需要注意的是浏览器的安全策略越来越严未来可能会对本地网络请求做更多限制所以代理方案可能是长期更稳妥的选择。
阅读完成 · 觉得有帮助?
咨询建站