前言:海康前端开发的那些"老大难"

做过海康威视摄像机、NVR 前端接入的朋友都知道,官方 WebSDK_noPlugin V3.4.0 虽然解决了"无插件"问题,但开发体验依然停留在十年前:

  • 接口风格混乱:同步返回success/error 回调Promise 三种调用形态混杂;
  • 没有 TypeScript 类型定义,any 满天飞,IDE 几乎无法自动补全;
  • 错误处理全靠"猜",错误码散落在文档各处;
  • 组件卸载后忘记销毁,Worker 和 WebSocket 连接泄漏,页面越用越卡;
  • Vue 3、React、Svelte 等现代框架接入,需要自己写一堆胶水代码。

hikvideoctrl 就是为了解决这些痛点而生 —— 它是海康官方 WebSDK_noPlugin V3.4.0 的现代化 TypeScript 封装,把所有底层调用统一为 async/await,提供完整类型定义、语义化事件、结构化错误处理。

NPM:https://www.npmjs.com/package/hikvideoctrl
GitHub:https://github.com/joygqz/hikvideoctrl(欢迎 Star、Issue、PR)


一、为什么选 hikvideoctrl?五大核心优势

1. 全面 Promise 化,告别回调嵌套

登录、预览、回放、抓拍、PTZ、录像下载、固件升级……所有 API 都支持 await,失败统一抛出 HikError,错误处理终于可以用 try/catch 一把梭。

2. 完整 TypeScript 支持,IDE 自动补全爽到飞起

导出所有常量字面量类型、参数类型、事件负载类型与错误码联合类型。再也不用对照着 PDF 文档去猜 iStreamType1 还是 2,直接 STREAM_TYPE.Main 一气呵成。

3. 资源自动回收,杜绝内存泄漏

destroy() 自动停止所有播放、释放底层 Worker、清空事件监听。Vue / React 组件卸载时一行调用,干干净净。

4. 零运行时依赖

只依赖海康原生 webVideoCtrl.js 及其同目录静态资源,不引入任何 npm 依赖。打包体积友好,不会污染你的依赖树。

5. 渐进式接入

高频能力(预览、回放、PTZ、抓拍)已封装为高级 API;特殊 ISAPI 请求或未封装方法可通过 sendHttpRequest() 与低级桥接 API 直接透传。封装但不绑架


二、运行环境与兼容性

项目 要求
浏览器 Chromium 91+(Chrome、Edge、Brave、国产 Chromium 内核浏览器)
设备 摄像机或 NVR 固件需支持 WebSocket 取流(V3.4 兼容 ISAPI 设备)
视频编码 H.264、H.265、smartH264、smartH265
接入协议 HTTP / HTTPS;HTTPS 或跨网段访问通常需要 WebSocket 代理
框架 Vue 2/3、React、Svelte、Angular、原生 JS/TS 均可

⚠️ 不支持:IE 浏览器、Node.js / SSR 渲染阶段、有插件 OCX 模式


三、5 分钟快速接入

Step 1:安装依赖

pnpm add hikvideoctrl
# 或
npm i hikvideoctrl
# 或
yarn add hikvideoctrl

Step 2:准备底层静态资源

海康开放平台 下载无插件 Web 开发包,把 codebase 目录复制到项目静态目录(如 Vite 的 public/codebase):

public/
└─ codebase/
   ├─ webVideoCtrl.js
   ├─ jsPlugin/
   ├─ encryption/
   └─ ...

要求 webVideoCtrl.js 能通过浏览器 URL 访问,例如 /codebase/webVideoCtrl.js目录结构必须原样保留,脚本会按相对路径加载 Worker 和加密模块。

Step 3:写代码!

import { createHikPlayer, loadWebVideoCtrl, PTZ_COMMAND, STREAM_TYPE } from 'hikvideoctrl'

// 加载底层脚本(全局只需一次)
await loadWebVideoCtrl('/codebase/webVideoCtrl.js')

// 创建播放器
const player = createHikPlayer()

await player.init({
  container: '#player',
  width: '100%',
  height: '100%',
  layout: 1, // 1×1 单画面
})

// 登录设备
const device = await player.login({
  host: '192.168.1.64',
  port: 80,
  protocol: 'http',
  username: 'admin',
  password: 'YourPassword',
})

// 获取通道并播放第一路子码流
const channels = await player.getChannels(device.id)
await player.startPreview(device.id, {
  channel: Number(channels[0].id),
  streamType: STREAM_TYPE.Sub,
})

// 云台向右转 0.5 秒
await player.ptzStart({ action: PTZ_COMMAND.Right, speed: 5 })
setTimeout(() => player.ptzStop(PTZ_COMMAND.Right), 500)

// 抓拍
await player.capture({ fileName: 'snapshot.jpg' })

// 组件卸载时销毁
await player.destroy()

就这么简单。没有回调嵌套,没有 iWndIndex 这种匈牙利命名,没有 XML 字符串解析


四、Vue 3 完整示例

<script setup lang="ts">
import type { HikPlayer } from 'hikvideoctrl'
import { createHikPlayer, loadWebVideoCtrl, STREAM_TYPE } from 'hikvideoctrl'
import { onBeforeUnmount, onMounted, ref } from 'vue'

const containerRef = ref<HTMLDivElement>()
let player: HikPlayer | null = null

onMounted(async () => {
  await loadWebVideoCtrl('/codebase/webVideoCtrl.js')
  player = createHikPlayer()
  await player.init({
    container: containerRef.value!,
    width: '100%',
    height: '100%',
  })

  const device = await player.login({
    host: '192.168.1.64',
    username: 'admin',
    password: 'YourPassword',
  })
  const [first] = await player.getChannels(device.id)
  if (first) {
    await player.startPreview(device.id, {
      channel: Number(first.id),
      streamType: STREAM_TYPE.Sub,
    })
  }
})

onBeforeUnmount(async () => {
  await player?.destroy()
  player = null
})
</script>

<template>
  <div ref="containerRef" style="width: 960px; height: 540px" />
</template>

五、React 完整示例

import type { HikPlayer } from 'hikvideoctrl'
import { createHikPlayer, loadWebVideoCtrl, STREAM_TYPE } from 'hikvideoctrl'
import { useEffect, useRef } from 'react'

export function CameraPanel() {
  const containerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    let disposed = false
    let player: HikPlayer | null = null

    ;(async () => {
      await loadWebVideoCtrl('/codebase/webVideoCtrl.js')
      if (disposed || !containerRef.current)
        return

      player = createHikPlayer()
      await player.init({
        container: containerRef.current,
        width: '100%',
        height: '100%',
      })

      const device = await player.login({
        host: '192.168.1.64',
        username: 'admin',
        password: 'YourPassword',
      })
      const [first] = await player.getChannels(device.id)
      if (first) {
        await player.startPreview(device.id, {
          channel: Number(first.id),
          streamType: STREAM_TYPE.Sub,
        })
      }
    })().catch(console.error)

    return () => {
      disposed = true
      player?.destroy().catch(console.error)
      player = null
    }
  }, [])

  return <div ref={containerRef} style={{ width: 960, height: 540 }} />
}

六、常见业务场景一站式覆盖

场景 1:四宫格预览

import { LAYOUT, STREAM_TYPE } from 'hikvideoctrl'

await player.changeLayout(LAYOUT.Quad) // 2×2

const channels = (await player.getChannels(device.id)).slice(0, 4)
await Promise.all(channels.map((ch, i) =>
  player.startPreview(device.id, {
    channel: Number(ch.id),
    windowIndex: i,
    streamType: STREAM_TYPE.Sub,
  }),
))

场景 2:按当天时间搜索并回放录像

import { todayTimeRange } from 'hikvideoctrl'

const { start, end } = todayTimeRange()
const result = await player.searchRecords(device.id, {
  channel: 1,
  startTime: start,
  endTime: end,
})

const [record] = result.matches
if (record) {
  await player.startPlayback(device.id, {
    channel: 1,
    startTime: record.startTime,
    endTime: record.endTime,
  })
}

场景 3:异常断流后自动重试

import { PLUGIN_EVENT, STREAM_TYPE } from 'hikvideoctrl'

player.on('plugin:event', async ({ eventType, windowIndex }) => {
  if (eventType !== PLUGIN_EVENT.PlayAbnormal)
    return

  await player.stop(windowIndex).catch(() => {})
  await player.startPreview(device.id, {
    channel: 1,
    windowIndex,
    streamType: STREAM_TYPE.Sub,
  })
})

场景 4:按时间段下载录像

const result = await player.searchRecords(device.id, {
  channel: 1,
  startTime: '2026-05-17 09:00:00',
  endTime: '2026-05-17 18:00:00',
})

const [record] = result.matches
await player.downloadRecordByTime(device.id, record.playbackUri, {
  fileName: 'morning.mp4',
  startTime: '2026-05-17 09:00:00',
  endTime: '2026-05-17 12:00:00',
})

场景 5:抓拍并自定义处理图片字节

await player.capture({
  fileName: 'snapshot.jpg',
  onData: async (data: Uint8Array) => {
    // 不触发浏览器下载,直接上传到后端
    await fetch('/api/upload', { method: 'POST', body: data })
  },
})

七、结构化错误处理,告别"魔法字符串"

所有封装 API 失败都抛出 HikErrorcode 字段是稳定的程序判断依据:

import { HikError } from 'hikvideoctrl'

try {
  await player.startPreview(device.id, { channel: 1 })
}
catch (err) {
  if (err instanceof HikError) {
    switch (err.code) {
      case 'SDK_NOT_FOUND':
        alert('底层脚本未加载,请检查 /codebase/webVideoCtrl.js 是否就位')
        break
      case 'DEVICE_NOT_FOUND':
        alert('设备未登录或已登出')
        break
      case 'INVALID_ARGUMENT':
        alert('参数错误:' + err.message)
        break
      case 'SDK_CALL_FAILED':
        console.error('底层调用失败', err.details?.status, err.details?.responseXml)
        break
      default:
        console.error(err.message)
    }
  }
}

常见错误码一览:

code 含义
SDK_NOT_FOUND 未加载底层脚本,或当前浏览器不支持
SDK_METHOD_MISSING 当前底层资源版本缺少目标方法
SDK_CALL_FAILED 底层调用失败,可能带 status/responseXml
SCRIPT_LOAD_FAILED loadWebVideoCtrl() 加载失败或超时
NOT_INITIALIZED 尚未调用 init()
ALREADY_INITIALIZED 重复调用 init()
INVALID_ARGUMENT 参数非法
DEVICE_NOT_FOUND 设备未登录或已登出
WINDOW_NOT_PLAYING 对未播放窗口执行停止等操作

八、语义化事件系统

事件名采用 <domain>:<action> 命名约定,IDE 完美补全:

const off = player.on('preview:started', ({ deviceId, channel, windowIndex }) => {
  console.log(`设备 ${deviceId} 通道 ${channel} 在窗口 ${windowIndex} 开始预览`)
})

// 取消订阅
off()

常用事件清单:

事件名 说明
plugin:initialized / plugin:destroyed 初始化 / 销毁完成
plugin:event 播放异常、回放结束、对讲失败、空间不足
plugin:error 插件运行时错误
device:connected / device:disconnected 设备登录登出
preview:started / preview:stopped 预览开始 / 停止
playback:started / playback:stopped 回放开始 / 停止
recording:started / recording:stopped 本地录像开始 / 停止
capture:completed 抓拍完成
window:selected / window:dblclick 窗口选中 / 双击

九、避坑指南:常见问题排查

Q1:报错 SDK_NOT_FOUND

  • 是否先调用了 await loadWebVideoCtrl('/codebase/webVideoCtrl.js')
  • 浏览器 Network 面板里 /codebase/webVideoCtrl.js 是否 200;
  • codebase 目录结构是否原样保留;
  • 是否在 SSR / Node.js 环境中提前创建了播放器。

Q2:HTTPS 页面预览失败

HTTPS 部署或跨网段访问时,WebSocket 直连通常会被浏览器或网络策略拦截,需要配置代理:

  1. 用 Nginx 配置 /ISAPI/SDK/webSocketVideoCtrlProxy 转发(官方包内附 nginx-1.28.0/conf/nginx.conf 示例);
  2. 调用预览或回放时传 useProxy: true
  3. 自动协商失败时显式传 webSocketPort
await player.startPreview(device.id, {
  channel: 1,
  useProxy: true,
  webSocketPort: 7681,
})

Q3:多窗口只有一路成功

  • 是否先调用 changeLayout() 或在 init() 里设置足够的 layout
  • 每路播放是否传了不同的 windowIndex
  • 多画面建议统一用 STREAM_TYPE.Sub 子码流,避免主码流压垮浏览器。

Q4:登录失败

  • host 不要带 http://,只传 IP 或域名;
  • protocolport 是否匹配(HTTP 默认 80,HTTPS 默认 443);
  • 浏览器能否直接访问设备的 Web 登录页。

Q5:抓拍没有触发下载

  • 如果传了 onData 回调,底层不会触发浏览器下载,需要自行处理字节数据;
  • 检查浏览器是否拦截了多次下载。

十、为什么不直接用官方 webVideoCtrl.js?

对比项 官方原生 SDK hikvideoctrl 封装
调用风格 同步 / 回调 / Promise 混合 全部 async/await
TypeScript 无类型定义 完整类型导出
错误处理 各 API 返回值含义不一 统一 HikError + 错误码
事件系统 散落在各个 init 选项中 统一 on/once/off
资源回收 手动 I_StopI_LogoutI_DestroyPlugin 一行 destroy() 全部搞定
Vue/React 接入 自己写胶水 官方示例直接复制
时间格式工具 内置 formatDate / todayTimeRange
IP / 端口校验 内置 isValidHost / isValidPort

十一、写在最后

hikvideoctrl 让海康威视无插件 Web 视频开发回归现代前端工程化的体验:

  • ✅ TypeScript 优先,类型即文档;
  • ✅ Promise 优先,逻辑清晰可控;
  • ✅ Vue / React / Svelte 一视同仁;
  • ✅ 完整事件 + 结构化错误 + 工具函数三件套;
  • ✅ MIT 协议,可自由商用。

如果你正在做安防监控、智慧园区、智慧工地、智慧校园、应急指挥等需要接入海康摄像机、NVR、DVR 的项目,强烈推荐试一试

觉得有帮助的话,点个 Star 是对作者最大的支持 🌟。有疑问、踩坑或新需求,欢迎在 GitHub Issues 区交流。

本项目为社区项目,仅作为海康官方无插件 Web 开发包的 TypeScript 上层封装,与海康威视官方无附属关系。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐