简介:海康威视 WEB 无插件开发包 V3.2 面向视频监控 Web 集成与前端开发人员,重点解决高版本 Chrome、Firefox 浏览器中传统控件无法使用的问题,并给出基于 Websocket 取流和 nginx 代理的接入方案。压缩包共 75 个文件,约 9.42MB,包含 js 脚本、html 示例页、xml 与 conf 代理配置、pdf 开发文档、bat 启停脚本以及 exe 运行组件和 nginx 目录,示例、文档与运行组件分区明确,便于搭建本地演示环境和定位所需模块,对代理服务、前端页面和运行组件的组织方式形成直观认识。已有 6737 人学习或下载,适合需要把海康设备预览能力嵌入业务系统的开发者参考。读者可结合控件开发包编程指南、Demo 测试说明和依赖库说明,理解前后端接入流程、浏览器兼容要点及常见问题处理,掌握无插件预览方案的部署思路与调试方法,提升实际项目中的落地效率。
1. 从一张空白页面说起:无插件包到底要装在哪
上周又被问了一次:海康威视 WEB 无插件开发包 V3.2 下载下来之后放哪儿。对方的做法是把压缩包解到站点根目录,双击 demo 里的 index.html,看到一片空白,于是判定包是坏的。这套海康威视 web 无插件开发包本身没有任何服务端程序,就是 HTML、JS 加说明文档,必须挂在一个 Web 服务后面才能工作。装错位置的表现非常一致:页面能打开,登录框点不动,Network 面板里一串 404 和跨源报错。下面按我实际部署的顺序记录——先确认版本与环境,再起静态服务,然后配反代,最后接页面、验证、收尾。
2. 解压与本地静态服务:先把 demo 原样跑起来
拿到包的第一件事不是改代码,是让原始 demo 在浏览器里正常显示。这一步能排除掉八成“包是不是坏的”疑问,也能提前暴露浏览器版本和路径问题。很多人跳过这步,直接往自己的项目里抄 JS,最后分不清是 SDK 的问题还是自己工程的问题,排查成本翻倍。
2.1 解压后先看清楚三样东西
解开压缩包,目录里真正要关注的只有三处:入口页面、js 目录里的主脚本、doc 目录里的说明。入口页面通常叫 index.html 或 demo.html;js 目录下会有一个体积明显偏大的主脚本,那是 SDK 本体;doc 里的说明文档写着支持的浏览器版本和设备固件版本要求。第三条最容易被跳过,但它恰恰是最硬的约束——设备端固件不提供无插件取流能力,前端写破天也出不来画面。
# 建一个独立的部署目录,别直接解在站点根目录下 mkdir -p /data/web/hik-web-v3.2 unzip -o web_sdk_v3.2.zip -d /data/web/hik-web-v3.2 # 只看两层结构,快速确认包的骨架,避免一层层点进去 find /data/web/hik-web-v3.2 -maxdepth 2 -type d | sort-maxdepth 2是故意限制层级的,包的目录有时嵌套三四层,第一次看全量 find 输出会被刷屏。确认骨架后,用ls -lh看 js 目录里文件的大小排序,主脚本一般几百 KB,比其余文件大一个量级,名字通常就写在入口页面的 script 标签里,两边对一下即可。
# 从入口页面反查它到底引了哪些脚本,比猜文件名靠谱 grep -oE '(src|href)="[^"]+"' /data/web/hik-web-v3.2/demo/index.html | sort -u这条命令把所有静态引用抽出来,一眼能看出脚本是相对路径还是绝对路径。如果出现/js/xxx.js这种以斜杠开头的写法,说明它默认部署在站点根目录,后面挪到子目录时必然 404,得提前记下来。
提示:不要对包内文件做任何修改再测试。第一轮验证要保证"原包 + 原配置",改动留到确认能跑之后。
2.2 起一个本地静态服务,别用 file:// 打开
双击 HTML 文件走的是file://协议,这个协议下浏览器的同源策略、相对路径解析、跨域请求行为跟 HTTP 完全不是一回事。SDK 内部要发请求拿设备信息,file://下这些请求要么被拦,要么目标地址拼不出来。最常见的做法是用一条命令起个临时静态服务,够用且不用装东西。
# 在包目录下起一个只监听本机的静态服务,端口避开常用的 80/8080 cd /data/web/hik-web-v3.2 python3 -m http.server 8080 --bind 127.0.0.1--bind 127.0.0.1很重要,它保证这个临时服务只对本机开放,不会被同网段其他人扫到。端口方面,8080 被占是常事,换成 8081、9000 都行,只要和后面反代配置的端口不冲突。服务起来后访问http://127.0.0.1:8080/demo/index.html,如果页面结构和按钮都正常显示,说明包是完整的,问题只会出在设备连接环节。
临时服务只适合自测,正式环境还是走 Nginx。一个最小的静态站点配置是这样:
server { listen 8081; server_name _; # 内网测试用,不绑定具体域名 root /data/web/hik-web-v3.2; # 指向解压后的包目录 index index.html; location / { try_files $uri $uri/ =404; # 找不到就老实返回 404,别兜底到 index add_header Cache-Control "no-cache"; } }root指向包目录,index让访问目录时自动落到入口页。try_files这里故意写成=404而不是兜底到 index.html,因为这是静态资源站点不是单页应用,资源缺失时老老实实报 404 比返回一个 HTML 更容易定位问题。Cache-Control: no-cache是给调试期用的,SDK 升级后浏览器吃旧缓存导致"改了没生效"的情况太常见,调试阶段先关掉缓存,上线再按文件名加版本号。
2.3 子目录部署时最容易翻车的相对路径
真正上线时,这套东西很少独占一个域名,多半挂在主站的/sdk/或/web/下面。一旦换目录,入口页面里的相对引用层级就变了:原本./js/xxx.js在/sdk/demo/index.html下会解析成/sdk/demo/js/xxx.js,而文件实际在/sdk/js/xxx.js。表现就是页面白屏,控制台一片 404。
处理办法有两个方向。省事的做法是把整个包的内容平铺到子目录根下,让入口页面和 js 目录同级;规范一点的做法是保留原结构,把入口页面的引用改成以/sdk/开头的绝对路径。我一般选后者,因为升级 SDK 时直接覆盖目录即可,不用担心层级错位。
# 批量把入口页面里的相对引用改成带前缀的绝对路径,改前先备份 cp demo/index.html demo/index.html.bak sed -i 's#"\./js/#"/sdk/js/#g; s#"\./css/#"/sdk/css/#g' demo/index.html # 改完再抽一次引用,确认没有漏网的相对路径 grep -oE '(src|href)="[^"]+"' demo/index.html | sort -used的两条替换规则分别处理 js 和 css,#作分隔符是为了避免路径里的斜杠和 sed 冲突。改完必须重新跑一遍 grep,因为有些引用写在 JS 字符串里,正则匹配不到,得靠浏览器 Network 面板二次确认。这一步别嫌麻烦,路径问题在后面会伪装成"设备连不上",白白浪费半天。
3. Nginx 反代到设备:WebSocket 和三行超时配置
静态页面能打开,只完成了三成。真正的连接目标是设备本身,浏览器要直接向设备的 IP 发请求。设备不给你返回跨域头,预检请求直接被拒,登录框点了没反应。这时候需要在页面和设备之间加一层转发,让浏览器眼里只有一个源。
3.1 为什么直连设备一定失败
浏览器发起跨源请求前会先发一个 OPTIONS 预检,设备端的 Web 服务对这种请求基本是 405 或者干脆不回跨域头,请求走不下去。就算绕开预检,登录用的认证方式还会带来第二道坎:认证失败返回 401 并带WWW-Authenticate头,浏览器弹原生登录框,而 SDK 里用的是自己的凭据逻辑,两边对不上,表现就是无限循环 401。
把设备和页面放到同一个源下,这两个问题一起消失。做法是把/device/这个路径前缀转发到设备地址,页面里的所有请求都改成走这个前缀。这样浏览器认为自己在跟同源地址通信,预检和认证都在同一套规则下走,凭据也能正常带上。
3.2 一份能跑通的反代配置
下面这段是我在测试环境里反复用过的骨架,改改 IP 和路径就能用。
upstream hik_device { server 192.168.1.64:80; # 设备实际地址和端口,80 走 HTTP,443 走 HTTPS keepalive 16; # 保持长连接,减少反复握手的开销 } server { listen 443 ssl; server_name web.demo.local; ssl_certificate /etc/nginx/cert/demo.crt; ssl_certificate_key /etc/nginx/cert/demo.key; # 静态资源:SDK 包本体 location /sdk/ { alias /data/web/hik-web-v3.2/; index index.html; try_files $uri $uri/ =404; } # 转发到设备:页面里所有设备请求都走这个前缀 location /device/ { proxy_pass http://hik_device/; proxy_http_version 1.1; # WebSocket 必须 1.1 proxy_set_header Upgrade $http_upgrade; # 透传协议升级头 proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Authorization $http_authorization; # 认证头别丢 proxy_buffering off; # 码流不能被缓冲 proxy_read_timeout 3600s; # 长连接读超时 proxy_send_timeout 3600s; } }逐项说一下取舍。proxy_http_version 1.1是硬性条件,默认的 1.0 不支持协议升级,WebSocket 握手会一直失败,控制台只报一句 connection closed,很容易误判成设备问题。Upgrade和Connection两个头必须一起写,只写一个等于没写。proxy_pass末尾那个斜杠不能省,它决定了/device/ISAPI/xxx转发出去是/ISAPI/xxx还是/device/ISAPI/xxx,少一个斜杠就是全站 404,这是血泪经验。
proxy_buffering off针对的是视频流。开着缓冲时 Nginx 会把响应攒够一定大小再往下发,播放器那头表现就是画面卡住几秒然后猛跳一下。关掉之后数据到达即转发,代价是后端连接占用时间变长。proxy_read_timeout默认 60 秒,超过就断,而无插件方案里的长连接往往几分钟才有一帧心跳,所以必须拉长到小时级。
| 参数 | 默认值 | 建议值 | 不调会怎样 |
|---|---|---|---|
| proxy_http_version | 1.0 | 1.1 | WebSocket 握手失败 |
| proxy_read_timeout | 60s | 3600s | 播放几分钟后断开 |
| proxy_buffering | on | off | 画面卡顿、延迟累积 |
| keepalive | 无 | 16 | 频繁建连,设备端连接数打满 |
| proxy_pass 末尾斜杠 | — | 必带 | 路径整体错位,全站 404 |
3.3 HTTPS 与自签证书:一次配好省很多事
新版本浏览器对非安全上下文越来越严格,取流相关的接口在 HTTP 下可能直接被禁用。测试环境用自签证书就够,但要让浏览器信任,否则每次打开都要点一次"继续访问",有些人图省事点了忽略,结果后续请求被拦,排查方向全错。
# 生成一张带 SAN 的自签证书,有效期一年,仅用于内网测试 openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /etc/nginx/cert/demo.key \ -out /etc/nginx/cert/demo.crt \ -subj "/CN=web.demo.local" \ -addext "subjectAltName=DNS:web.demo.local,IP:192.168.1.64"-addext subjectAltName这一行不能省。现代浏览器不看 CN 了,只看 SAN 列表,没有 SAN 的证书即使手动信任也会报名称不匹配。把域名和实际访问用的 IP 都写进去,能同时覆盖两种访问方式。证书装好后,操作系统的受信任根证书里也要导入一份,否则页面里的fetch依然会因为证书链不完整被拒。
还要注意混合内容问题:页面是 HTTPS,里面如果残留 http:// 开头的设备地址,浏览器会直接拦掉。检查方法是打开控制台过滤Mixed Content,有就说明某个地方硬编码了地址,需要统一收敛到/device/前缀。
4. 页面接入:从初始化到预览的最小闭环
配置层面通了,接下来是页面怎么接。这段代码量不大,但顺序错了就各种诡异。我把整个过程拆成三步:引脚本、初始化、登录取流。三步之间都有依赖,跳步会出现"对象是 undefined"这类报错。
4.1 脚本引入的顺序和全局对象
SDK 的主脚本一般会挂一个全局对象出来,名字在包内文档里写着。引入顺序上,如果包里带了 jQuery 之类的依赖,必须在主脚本之前引入,否则主脚本初始化时找不到依赖直接抛错。这一点在把代码搬到已有工程里时特别容易踩,因为主站可能已经引了另一个版本的 jQuery。
<!-- 依赖在前,SDK 主脚本在后,路径按实际部署目录调整 --> <script src="/sdk/js/jquery.min.js"></script> <script src="/sdk/js/webVideoCtrl.js"></script> <script> // 主脚本加载完再检查全局对象是否存在,比报错后猜快得多 window.addEventListener('load', function () { if (typeof WebVideoCtrl === 'undefined') { console.error('SDK 主脚本未加载,检查路径和 script 标签顺序'); } }); </script>typeof判断比直接访问安全,未声明时不会抛异常。这个自检块上线后可以删,调试期留着能省不少时间。如果这里报 undefined,先看 Network 面板里主脚本是不是 200,再看是不是被主站的其他脚本覆盖了同名对象。
4.2 初始化参数:把设备地址收敛到一个前缀
初始化要传的核心参数就三个:页面协议、设备地址前缀、端口。关键在于把设备地址统一写成/device,而不是每处都写一遍 IP。后面换设备、加反代、切 HTTPS,只改这一处。
// 初始化配置:地址前缀统一收敛,避免散落在业务代码里 const sdkOptions = { protocol: window.location.protocol, // 跟随页面协议,避免混合内容 host: window.location.host, // 同源,交给 Nginx 转发 prefix: '/device', // 与 Nginx location 保持一致 port: window.location.port || (window.location.protocol === 'https:' ? 443 : 80), lang: 'zh' // 界面语言,按需改 }; // 初始化失败通常是路径前缀和设备实际服务路径对不上 const inited = WebVideoCtrl.init(sdkOptions); if (!inited) { console.error('初始化失败,核对 prefix 与 Nginx location 是否一致'); }protocol和host直接取window.location,这样本地调试、测试环境、生产环境三套地址不用改代码。prefix必须和 Nginx 里location /device/完全一致,包括结尾斜杠的处理方式,两边差一个字符就是 404。端口那段是为了兼容非标准端口部署,如果站点跑在 8443,取到的就是 8443,不会硬编码成 443。
4.3 登录、取通道、播放、释放
登录拿会话,取通道列表,选一路播放,页面关掉时释放。这四步的生命周期管理是新手最容易漏的,漏了释放会导致设备端连接数越积越多,跑一天之后新用户进不来,现象是"早上好好的,下午就打不开"。
// 登录 -> 取通道 -> 播放 -> 释放,完整最小闭环 async function preview(ip, port, user, pwd) { // 1. 登录,成功后 SDK 内部会持有会话 const loginRes = await WebVideoCtrl.login(ip, port, user, pwd); if (loginRes.code !== 0) { console.error('登录失败', loginRes.code, loginRes.msg); return; } // 2. 取通道列表,模拟通道一般是第一个 const channels = await WebVideoCtrl.getChannels(); if (!channels.length) { console.error('设备未返回通道,检查固件是否支持无插件取流'); return; } // 3. 起预览,容器 id 对应页面上的 div WebVideoCtrl.startPreview(channels[0].id, 'player-container'); // 4. 页面卸载时释放,这一步不能省 window.addEventListener('beforeunload', function () { WebVideoCtrl.stopPreview(channels[0].id); WebVideoCtrl.logout(); }); }login的返回码要判断,不要只看有没有抛异常,认证失败时很多实现是返回码非零而不是抛错。getChannels返回空数组通常有两个原因:账号权限不够,或者固件不支持,前者换账号验证,后者只能升级设备固件。beforeunload里做释放是兜底,更严谨的做法是在路由切换时也调一次,单页应用里beforeunload不一定触发。
还有一个隐性限制:浏览器对同一域名下的并发连接数有上限,通常是 6 个。多路视频同时播放时,超出部分会排队,表现是后面几路一直转圈。常规做法是把不同通道分散到不同子域,或者限制单页同时播放的路数,别指望浏览器放开限制。
5. 排查记录:五条踩过的坑和它们的解法
下面这五条都是实际遇到过、并且当时卡了不止半小时的。按现象、原因、解决的顺序写,遇到类似症状可以直接对号入座。
5.1 页面正常但登录一直转圈,Network 里 OPTIONS 返回 405
现象:入口页面完整显示,输入账号密码点登录,按钮一直转圈,控制台没有明显报错,Network 面板里能看到一条 OPTIONS 请求返回 405。
原因:页面和设备不同源,浏览器先发预检,设备端的 Web 服务不支持 OPTIONS 方法,也没有返回允许跨源的响应头,预检失败后真正的请求根本没发出去。
解决:把设备请求统一收敛到/device/前缀,通过 Nginx 转发,让浏览器认为是同源请求。同时确认 Nginx 配置里proxy_set_header Authorization $http_authorization;没被注释掉,认证头丢了同样会表现为登录失败。
5.2 登录成功但画面全黑,播放器区域没有任何报错
现象:登录返回成功,通道列表也拿到了,播放器区域就是黑的,没有报错弹窗,控制台安静得反常。
原因:转发链路把码流缓冲了。Nginx 默认开启proxy_buffering,会把响应攒到一定大小再下发,视频流这种低频小包的数据就卡在中间层。另一种可能是proxy_read_timeout用默认 60 秒,长连接被静默掐断,前端感知不到。
解决:location /device/里加proxy_buffering off;,把proxy_read_timeout和proxy_send_timeout一起调到 3600s。改完nginx -t校验再 reload,别忘了 reload 之后浏览器强刷一次,旧连接还挂在那儿。
5.3 WebSocket 握手 101 通过,三秒后连接被关闭
现象:Network 面板的 WS 标签里能看到状态 101,说明握手成功,但紧接着连接状态变成 closed,间隔差不多三秒,反复重连。
原因:Connection头被写成了固定值"upgrade",在非升级请求上这个头是错的;或者上游设备的会话校验没过,握手完成后设备主动断开。还有一种情况是中间还有一层负载均衡,那一层没配长连接超时。
解决:把Connection改成"upgrade"只在升级场景生效(用 map 按$http_upgrade判断),并确认设备端账号有取流权限。如果前面还有一层转发,那一层的超时也要同步调大,任何一层先断,整条链路就断。
5.4 换成 HTTPS 之后整体失效,控制台一片红色
现象:HTTP 下一切正常,切到 HTTPS 后页面能开,但所有设备请求失败,控制台大量Mixed Content和证书错误。
原因:页面里残留了 http:// 开头的地址,或者证书缺少 SAN 字段,浏览器不认。另外自签证书没导入受信任根,fetch会直接拒绝。
解决:全局搜一遍http://,把设备地址改成走/device前缀的相对路径;重新生成带 SAN 的证书并把域名和 IP 都写进去;把证书导入操作系统受信任根。三步都做完再刷新,只做其中一步往往还是红。
5.5 升级 SDK 后行为变了,但代码一行没改
现象:把包从旧版本换成 V3.2,接口名和参数看着一样,实际跑起来某些功能失效,或者返回值结构对不上。
原因:浏览器的旧脚本缓存没清,加载的还是上一版主脚本;或者新旧版本在初始化和返回码定义上有差异,代码里对返回码的判断失效。
解决:先强刷并确认 Network 里主脚本的响应大小变了,再对比新旧包的说明文档,重点看初始化和登录两处的参数变化。升级时把所有 JS 文件名带上版本号,从根上避免缓存问题,比每次让人手动清缓存靠谱得多。
注意:排查顺序建议从外往里——先看浏览器 Network,再看 Nginx 错误日志,最后才怀疑设备和 SDK。反过来查,八成时间会浪费在设备上。
6. 验证跑通:五个检查点按顺序过一遍
装完之后别急着交付,按顺序过五个检查点,任何一步不过就停在那儿查,不要跳步。跳步的结果是问题叠加,最后分不清是哪一层的事。
| 序号 | 检查点 | 操作 | 期望结果 |
|---|---|---|---|
| 1 | 静态资源 | 浏览器打开入口页,看 Network | 主脚本 200,大小与包内一致 |
| 2 | 转发链路 | curl 打设备信息接口 | 返回 200,内容为设备 JSON |
| 3 | 协议升级 | 看 WS 面板或 curl 模拟握手 | 状态 101,且保持不关闭 |
| 4 | 业务闭环 | 页面登录并播放一路 | 有画面,切换通道正常 |
| 5 | 资源释放 | 关闭页面看设备连接数 | 连接数回落到 0 |
前两个用命令行就能确认,不用开浏览器。
# 检查点 1:静态资源是否真的可取,顺便看响应体大小 curl -s -o /dev/null -w "%{http_code} %{size_download}\n" \ https://web.demo.local/sdk/demo/index.html # 检查点 2:转发链路是否通,-u 带上设备账号密码 curl -s -o /dev/null -w "%{http_code}\n" -u admin:passwd \ https://web.demo.local/device/ISAPI/System/deviceInfo # 检查点 3:模拟一次 WebSocket 握手,看返回码是不是 101 curl -i -s -N --max-time 5 \ -H "Connection: Upgrade" -H "Upgrade: websocket" \ -H "Sec-WebSocket-Version: 13" \ -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ https://web.demo.local/device/ws第一条的%{size_download}比状态码更有信息量,200 但大小是 0 说明命中了空文件或者被拦截了重定向。第二条返回 401 说明认证头没透传,返回 404 说明proxy_pass的斜杠写错。第三条里的/device/ws是占位路径,实际路径从 demo 页面的 Network 面板里抄,--max-time 5是为了不让命令卡住,握手成功后会超时退出,看到 101 就算过。
检查点 5 最容易被忽略,但它是长期运行的关键。连续打开关闭页面五到十次,然后在设备端看连接数,如果不回落,说明释放逻辑没生效,跑一天之后设备连接池就打满了。我习惯在beforeunload里加一个console.count做临时统计,确认每次关闭都进了释放分支,验证完再删掉。
浏览器侧还有个细节:预览窗口切到后台标签页时,部分浏览器会降低定时器频率,心跳发不出去,回来之后画面就僵住了。测试时把页面切走五分钟再切回来,看能不能自动恢复,恢复不了就在visibilitychange事件里补一次重连。
从那以后我每次交付这类项目,都强制走一遍这五个检查点,哪怕只是改了一行 Nginx 配置也照走——翻车的从来不是大改动,都是"就改了一点点应该没事"。希望帮到你。
本文还有配套的精品资源,点击获取