跨地域监控难管理?通过 GB28181 把各地海康大华乐橙摄像头注册到云端平台

杭州仓的枪机、成都仓的球机、青岛仓的录像机,各自开在本地录像机网页里。总部夜班要回答一句:现在哪座城市的卸货口还在线。三个网页、三套密码,刷新顺序一变,离线口径就对不上。林工把三地私网地址抄进一张表,在总部 ping,全部超时。网络同事据此建议拉专线。

专线让总部能够访问这些私网地址。三地要出现在同一份设备账里,靠的是另一件事:支持 GB/T 28181 的摄像机或录像机,用 TCP 向同一个云端 SIP 注册;乐橙协议的摄像机绑定到同一个开发者账号。城市和城市之间可以继续互不相通。

读完可以做成五件事,按这个顺序做:

  1. 分清现场是单台设备向外注册,还是已经有一套下级平台。
  2. 建成一个国标项目,三地按同一张 SIP 表注册。
  3. 乐橙协议摄像机进入同一个开发者账号。
  4. 服务端按城市打出在线通道。
  5. 给其中一路在线通道签发辅码流 HLS。

三座城市注册进同一个国标项目,乐橙协议摄像机绑定同一开发者账号。对账接口是 listDeviceDetailsByPage 和 deviceOnline。做完时,服务端能按城市列出在线通道,并为其中一路拿到辅码流地址。云台、语音对讲、云录像和智能检测,以该设备当前能力集与已开通服务为准。

三套录像机网页,离线各说各话

本地网页能看,说明人坐在那座仓库的局域网里。总部要的是另一份账:这座城市有哪些通道、这一刻在不在线、画面能不能调出来。三套网页给不出这份账。每套网页只认识自己下面的通道,刷新时间也不一样,夜班记录里就会出现「杭州说在线、总部说离线」。

把三地摄像机的 IP 写进总部客户端,同样组不出这份账。那些地址只在各自仓库的光猫后面有意义。总部解析不到,列表里就不会多出一行。

专线或组网打通之后,总部可以按私网地址去拉流。设备列表、城市编码、在线口径仍然要另做。跨地域难管,卡在资产分散,也卡在「在线」没有统一来源。

注册进云端之后,城市账才有同一套口径

实时查看分成两段。信令回答「这台设备是谁、现在在不在、要看哪一路」。媒体把码流送到播放器。两段都到达同一个云端,总部页面就能出画。摄像机的私网地址留在仓库局域网里。

国标这条线里,IPC 或 NVR 主动向云端 SIP 注册。注册成功后,云端成为这台设备的信令和媒体汇合点。仓库宽带只要允许设备向外建立连接,杭州不必看见成都。GB/T 28181-2016 是当前这套接入所依据的协议版本,通信在《产品介绍》里写明优先 TCP。TCP、UDP、TLS 里,生产按 TCP 填写。

乐橙协议的摄像机走另一条连接,方向同样是设备向外。它按《开发总览》添加到开发者账号,不填写 SIP。两条线在设备列表汇合,在仓库交换机上并不汇合。

注册完成之后,业务侧认的主键是接口返回的设备序列号 deviceId,加上通道号 channelId。设备 Web 上填写的设备国标 ID 只服务于 SIP 注册。城市编码写在自己的业务库里,平台列表不携带「杭州仓」这三个字。

国标设备注册在线之后,查询、在线状态、云直播与乐橙协议设备使用同一组现行方法。《产品介绍》的产品范围里,实时预览、本地录像、云直播、设备查询、云台和报警消息查询各有对应说明。语音对讲在同一页写了适用范围。云录像写的是连续云存储,存储采用行业通用明文传输。这些说明是能力边界,按钮是否出现,仍看该通道当前的能力集,以及对应服务是否已经开通。

text 复制代码
三地宽带(彼此可以没有专线)
  杭州 / 成都 / 青岛
    国标 IPC、NVR ── TCP 注册 ──┐
    乐橙协议摄像机 ── 账号绑定 ──┼──► 同一开发者账号
                                ▼
                     listDeviceDetailsByPage
                                │
               业务库:deviceId + channelId → 城市编码
                                │
                     单城抽查 deviceOnline
                                │
                在线通道才签发辅码流 HLS

动设备之前,先对上现场手里的东西

改任何一台录像机之前,用下面四次核对决定停在哪一层。

现场要保留的是按私网地址取流的旧系统。 总部现有录像系统只能认摄像机的私网 IP,视频也不能离开自建网络。这种项目先做网络,让私网地址从总部可达。验收是总部拉得到指定地址。开放接口里出现一条设备记录,是另一条验收。

现场已经有下级平台,合同写的是目录上推。 下级平台用自己的平台身份向上注册,上级订阅目录,再协商媒体。参数是平台身份、目录订阅和媒体地址,以该上级平台当前的书面说明为准。本文的 SIP 六项是给单台 IPC 或 NVR 填的,两张表不要抄到一起。

现场只有摄像机和仓库录像机。 按设备接入做。一个国标项目接收各地出口。设备在自己的局域网里连到文档给出的 SIP 域名和端口,传输选 TCP。出口放行之后约 60 秒,控制台能看到注册状态。这一步通过,值班页就不必等待三地互通。

页面要的是短时播放地址,还是内网固定地址。 自有 Web 向自己的服务要地址。地址在设备已经在线之后签发。专线通了,也不会自动生成这条地址。

三座仓库、没有下级平台、总部用网页值班的项目,落在后两条。

各地出口指向同一个国标项目

应用、国标项目和接口说明都从乐橙云开放平台的控制台进入。创建应用之后,在「我的应用 → 应用信息」取得 appId 和 appSecret,只放在服务端。国标设备要先完成企业认证,再创建国标项目。认证没过,先不要改现场设备。《总体流程》的顺序是:申请国标项目、绑定国标设备、设备端国标配置、调试、应用对接。

控制台支持自动生成设备国标 ID,也支持自定义。原项目已经在用的国标 ID,用自定义方式复用,设备 Web 少改几项。点位多时,可以按数量自动生成 IPC 或 NVR,也可以上传自定义国标 ID 模板批量绑定,再导出设备表。设备国标 ID 用导出表传递。口头抄写会在两座城市之间写重。

国标侧的公开说明在《产品介绍》《总体流程》《详细流程》。当前这几页写的是设备接入:支持 GB/T 28181 的 IPC、NVR 向平台 SIP 注册。多目摄像机按 NVR 方式接入。实时预览、本地录像、云直播可以在设备在线后继续做。云台、语音对讲、云录像、智能检测是否可用,以该设备当前能力集和已开通服务为准。

实施单固定这些列,取值以《详细流程》的名词表为准:

现场要填的项 取值
SIP 服务 ID 国标项目的唯一 ID
SIP 服务域 服务 ID 的前十位
SIP 服务器域名 文档当前写明 gbaccess-online-hz.imou.com。设备不能填域名时,在安装地 ping 该域名;《详细流程》同时给出 IP 8.154.21.254,文档更新后以当页为准
SIP 服务器端口 控制台该项目显示的端口
设备国标 ID、密码 与控制台该行一致
传输 TCP。协议版本选 GB/T 28181-2016

域名和端口全项目相同,写在表头。每一行的设备国标 ID 和密码单独核对。NVR 再加一列:通道国标 ID 的顺序与控制台通道顺序一致。只改通道名称,纠正不了串画面。

设备 Web 的国标配置页,按《详细流程》第四节对应机型的本地配置填写。保存后约 60 秒,到控制台「国标 GB28181 接入 → 设备管理」看注册状态。状态为在线后,先在「直播服务」里试播 HLS。试播能出画,再把设备序列号交给应用。超过约两分钟仍离线,回到导出表逐项核对。

资源在这一步看完。《开发总览》写明个人版账号提供 10 路接入和 1M 带宽,实际剩余以控制台「我的资源」为准。《详细流程》写明:国标通道和普通通道统一计入接入数,每台国标通道算 1 路;拉流带宽按实际值计入。同页可以配置国标预警,关注日流量上限,以及当天产生推流的国标通道路数上限。三座城市同时打开主码流之前,先用辅码流把第一路跑通。

乐橙协议摄像机不走上面的 SIP 表。按《开发总览》添加到同一个开发者账号:用乐橙 App 登录该账号后添加,或按开放平台 SDK DEMO 的添加方式完成。添加完成后,它和国标设备出现在同一次分页查询里。

一座城市先在线,服务端再按城市出账

请求一律 HTTP POST。根路径用《开发规范》当前页的接口域名,代码里的 OPENAPI_BASE 与该页保持一致,方法名接在后面。请求体分 system、id、params。签名三步:原始串 time:{time},nonce:{nonce},appSecret:{appSecret};password 为 appSecret 的 SHA-256 小写十六进制;sign 为 HMAC-SHA256 后再 Base64。固定入参先对一次:time=1706511734,nonce=f5a1ae2d-c09c-4d39-a744-83a5c2c653c2,appSecret=test123456789test123456789,sign 应为 xjhCQBoJ9hRDsCjyDcHjtDNzRZ3ZJezcawsfWeiaoxU=。对不上就先停。time 与服务器相差超过 5 分钟返回 SN1002,nonce 在 5 分钟内重复返回 SN1005,签名本身算错返回 SN1001。

管理员 accessToken 用方法 accessToken 换取,params 可以为空对象。返回里的 expireTime 单位是秒,缓存在服务端。将要过期或返回 TK1002 再取。浏览器只接收某一路的播放地址。谁能看哪座城市,在业务登录态里裁剪 deviceId。

建议的顺序是:签名样例通过 → accessToken → 控制台至少一座城市在线并完成 HLS 试播 → listDeviceDetailsByPage 打出城市账 → 对这座城市的设备调用 deviceOnline → 再为其中一路在线通道签发辅码流。第一路地址没拿到之前,不去铺三城宫格,也不去试云台。

下面是 Node.js 18+。城市归属写在业务配置里。一台录像机对应一座城市时,用 deviceId 做键。一台设备拆给两个场地时,把键改成 deviceId + channelId。

javascript 复制代码
const crypto = require('crypto');

const OPENAPI_BASE = 'https://openapi.lechange.cn/openapi';

const regionByDeviceId = {
  HZ_DEVICE_SERIAL: { regionCode: 'HD-HZ', regionName: '杭州仓' },
  CD_DEVICE_SERIAL: { regionCode: 'XN-CD', regionName: '成都仓' },
  QD_DEVICE_SERIAL: { regionCode: 'HB-QD', regionName: '青岛仓' }
};

function calcSign(time, nonce, appSecret) {
  const raw = `time:${time},nonce:${nonce},appSecret:${appSecret}`;
  const password = crypto.createHash('sha256').update(appSecret, 'utf8').digest('hex');
  return crypto.createHmac('sha256', password).update(raw, 'utf8').digest('base64');
}

function assertSignSample() {
  const sign = calcSign(
    1706511734,
    'f5a1ae2d-c09c-4d39-a744-83a5c2c653c2',
    'test123456789test123456789'
  );
  if (sign !== 'xjhCQBoJ9hRDsCjyDcHjtDNzRZ3ZJezcawsfWeiaoxU=') {
    throw new Error('sign sample mismatch');
  }
}

let adminToken = { value: '', expireAt: 0 };

async function callOpenApi(method, appId, appSecret, params, allowRetry = true) {
  const time = Math.floor(Date.now() / 1000);
  const nonce = crypto.randomUUID();
  const res = await fetch(`${OPENAPI_BASE}/${method}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      system: {
        ver: '1.0',
        appId,
        sign: calcSign(time, nonce, appSecret),
        time,
        nonce
      },
      id: crypto.randomUUID(),
      params
    })
  });
  const json = await res.json();
  const code = json?.result?.code;
  if (code === 'TK1002' && allowRetry && method !== 'accessToken') {
    adminToken = { value: '', expireAt: 0 };
    const token = await getAdminToken(appId, appSecret);
    return callOpenApi(method, appId, appSecret, { ...params, token }, false);
  }
  if (code !== '0') {
    const err = new Error(json?.result?.msg || 'openapi error');
    err.code = code;
    throw err;
  }
  return json.result.data || {};
}

async function getAdminToken(appId, appSecret) {
  const now = Math.floor(Date.now() / 1000);
  if (adminToken.value && adminToken.expireAt - now > 3600) return adminToken.value;
  const data = await callOpenApi('accessToken', appId, appSecret, {});
  adminToken = {
    value: data.accessToken,
    expireAt: now + Number(data.expireTime || 0)
  };
  return adminToken.value;
}

async function reconcile(appId, appSecret) {
  const token = await getAdminToken(appId, appSecret);
  const rows = [];
  for (let page = 1; page <= 100; page += 1) {
    const data = await callOpenApi('listDeviceDetailsByPage', appId, appSecret, {
      token,
      page,
      pageSize: 50,
      source: 'bindAndShare'
    });
    const list = data.deviceList || [];
    if (list.length === 0) break;
    for (const device of list) {
      const region = regionByDeviceId[device.deviceId] || {
        regionCode: 'UNMAPPED',
        regionName: '未建档'
      };
      const channels = device.channelList || [];
      const base = {
        regionCode: region.regionCode,
        regionName: region.regionName,
        deviceId: device.deviceId,
        deviceName: device.deviceName,
        brand: device.brand || '',
        catalog: device.catalog || '',
        deviceStatus: device.deviceStatus,
        deviceAbility: device.deviceAbility || ''
      };
      if (channels.length === 0) {
        rows.push({ ...base, channelId: '', channelStatus: '', channelAbility: '' });
        continue;
      }
      for (const ch of channels) {
        rows.push({
          ...base,
          channelId: String(ch.channelId),
          channelName: ch.channelName,
          channelStatus: ch.channelStatus,
          channelAbility: ch.channelAbility || ''
        });
      }
    }
    if (list.length < 50) break;
  }
  return rows;
}

async function spotCheck(appId, appSecret, regionCode) {
  const rows = await reconcile(appId, appSecret);
  const deviceIds = [...new Set(
    rows.filter((row) => row.regionCode === regionCode).map((row) => row.deviceId)
  )];
  const token = await getAdminToken(appId, appSecret);
  const checks = [];
  for (const deviceId of deviceIds) {
    const data = await callOpenApi('deviceOnline', appId, appSecret, { token, deviceId });
    checks.push({
      deviceId,
      onLine: data.onLine,
      channels: (data.channels || []).map((ch) => ({
        channelId: String(ch.channelId),
        onLine: ch.onLine
      }))
    });
  }
  return checks;
}

async function issueSubHls(appId, appSecret, deviceId, channelId) {
  const token = await getAdminToken(appId, appSecret);
  const params = { token, deviceId, channelId: String(channelId), streamId: 1 };
  try {
    await callOpenApi('bindDeviceLive', appId, appSecret, params);
  } catch (err) {
    if (err.code !== 'LV1001') throw err;
  }
  const info = await callOpenApi('getLiveStreamInfo', appId, appSecret, {
    token,
    deviceId,
    channelId: String(channelId)
  });
  const sub = (info.streams || []).find((item) => {
    return item.streamId === 1 && String(item.hls || '').includes('proto=https');
  });
  if (!sub) throw new Error('未拿到辅码流 HTTPS 地址');
  return sub.hls;
}

module.exports = { assertSignSample, reconcile, spotCheck, issueSubHls };

assertSignSample() 不抛错,签名壳才可用。accessToken 返回 result.code 为 "0",并且 data.accessToken 有值,令牌才算拿到。

listDeviceDetailsByPage 的说明在《分页查询设备详细信息》。page 从 1 开始,pageSize 范围 1~50。返回里的 count 是本次查到的设备数,用它当总页数会把后注册的城市丢掉。翻页直到本页为空,或本页不足 50 条。deviceStatus、channelStatus 取值包括 online、offline、sleep、upgrading。brand 为 lechange 表示乐橙设备,general 表示通用设备。城市归属仍以业务表为准,不靠 brand 猜测。deviceAbility、channelAbility 是逗号分隔的能力集。source=bindAndShare 对子账号不起作用;上面的代码用管理员 token,城市隔离放在业务侧。

成功的城市账长这样:杭州仓至少一行 deviceStatus 为 online,对应通道的 channelStatus 也为 online,regionCode 为 HD-HZ。出现 UNMAPPED,说明这台设备的序列号还没写进业务表。列表为空,先回到控制台看注册状态,再查这台设备是否加到了当前应用所属账号。

deviceOnline 的说明在《获取设备在线状态》。它返回的 onLine 是另一套枚举:0 不在线,1 在线,3 升级中,4 休眠。通道同样使用这套数字。列表里的 online 字符串和这里的 "1" 都表示在线,字段名和取值不能混用。文档写明:未绑定设备也能查在离线,但未绑定设备的结果只表示设备本身,不表示某个通道。单城抽查时,onLine 为 "1" 且目标通道也为 "1",才进入签发。sleep、升级中、"4"、"3" 都先停在状态上。

辅码流地址走《自定义直播》里的 bindDeviceLive,再用《根据序列号获取直播地址和直播状态》的 getLiveStreamInfo。streamId 为 0 是主码流,1 是辅码流。已经创建过时,创建接口常见返回 LV1001,捕获后改为查询。HTTPS 页面取地址里带 proto=https 的那条。streams[].status 为 0 表示正在直播,2 是视频源异常,10 是直播暂停。状态不是 0 时,先处理设备侧和直播开关。

这一路地址能播放,第一座城市的闭环就完成了。另外两座城市重复同一张 SIP 表,改的是导出表里那一行的国标 ID 和密码。三城同时打开主码流之前,先看「我的资源」。个人版并发到达上限时,返回码是 FL1006。取流失败也可能是带宽或流量额度,和设备离线不是同一条返回码,对照《全局返回码说明》分开处理。

页面若要嵌预览组件,放在这路 HLS 已经出画之后,按《详细流程》第六节指向的轻应用开发指南做。云直播那一条,同一节指向云直播开发指南。

对账不要按秒轮询。《接口调用限制说明》里,免费用户的账号类接口频率上限是每分钟 20 次,超限返回 OP1039。listDeviceDetailsByPage 的参数里没有设备序列号,按账号类接口理解。整点拉一次全量,单城需要复核时再调 deviceOnline。

这几页的标题和方法名可以对上

文档站按标题或方法名检索。下表只列本文用到的页。

文档标题 方法 用来核对什么
《开发总览》 --- 设备如何进入开发者账号;个人版 10 路接入与 1M 带宽
《开发规范》 accessToken 请求壳、签名三步、SN1001、SN1002、SN1005
《产品介绍》 --- GB/T 28181 设备接入范围;传输优先 TCP;连续云存储
《总体流程》 --- 建国标项目、绑定、设备配置、调试、应用对接
《详细流程》 --- SIP 六项、注册域名与 IP、约 60 秒在线、每通道 1 路、日流量和推流路数预警
《分页查询设备详细信息》 listDeviceDetailsByPage page / pageSize、count、brand、列表在线状态、能力集
《获取设备在线状态》 deviceOnline onLine 的 0 / 1 / 3 / 4;未绑定设备只返回设备级状态
《设备查询说明》 --- 上述查询方法在设备查询模块中的位置
《自定义直播》 bindDeviceLive 创建设备源直播;streamId 0 主码流、1 辅码流
《根据序列号获取直播地址和直播状态》 getLiveStreamInfo streams[].hls、streamId、status
《全局返回码说明》 --- LV1001、OP1043、OP1044、FL1006、TK1002、GB1001 至 GB1014、AI1011
《接口调用限制说明》 --- 免费用户账号类接口每分钟 20 次,超限 OP1039

《详细流程》第六节还写了「云直播开发指南」和「轻应用开发指南」。前者对到云直播这组接口,后者对到页面里的预览组件。

注册过了,城市和画面仍可能对不上

现象 先做什么
三地互 ping 不通,就认为不能统一管 看每台 IPC 或 NVR 能否向外注册。统一账依赖注册和绑定
控制台一直离线 核对六项是否与导出表一致,传输是否为 TCP,出口是否放行注册端口。企业认证和国标项目是否已建成
域名一栏填不进去 在安装地 ping gbaccess-online-hz.imou.com。IP 以《详细流程》当页为准
绑定或导入失败 对照 GB1005(设备国标 ID 格式错误)、GB1006(通道国标 ID 格式错误)、GB1001 / GB1008 / GB1011(ID 已存在或重复)、GB1009(国标项目不存在)、GB1012(通道绑定数量超限)
控制台在线,接口仍失败 OP1044 表示国标设备未连接云平台,GB1014 表示国标设备未接入。先等注册状态稳定,再重试设备接口
列表里只有最早接入的一座城市 page 从 1 翻到空页。count 是本页设备数
设备在线,页面上是「未建档」 业务表还没有这台 deviceId。国标 ID 写进业务表,对不上接口里的序列号
杭州格子里是成都的画面 核对 NVR 通道国标 ID 顺序。让人在对应镜头前挥手
多目摄像机只有一路 《详细流程》注明多目按 NVR 方式接入
onLine 为 "1",通道仍无画面 通道级 onLine 也要为 "1"。列表里的 sleep、upgrading 先排除
创建直播失败 已创建过则改调 getLiveStreamInfo(LV1001)。OP1022 时,先在乐橙 App「设置 → 账户与安全」打开「服务授权」
三城一齐开主码流后中断 宫格改辅码流 streamId=1。查看「我的资源」,并看国标预警里的日流量、推流路数。个人版并发上限对应 FL1006
整点对账偶尔失败 拉长间隔。频率超限是 OP1039
预览正常,云台、对讲或云录像没有 看 deviceAbility、channelAbility 和该通道是否已开通对应服务
给国标通道开了事件云存储 OP1043:国标设备当前仅支持连续云存储
给国标通道开 AI 智见套餐 AI1011:国标设备暂不支持开通该套餐。检测类能力以设备能力集与开通服务为准
播放地址出现在公开日志里 地址只发给已通过业务鉴权的页面。appSecret 出现在前端时,先在控制台更换密钥

离场前做一次跨地域核对:拔掉总部与任意一座仓库之间的临时远程。三地仍然互相 ping 不通。此时在线通道应继续出画,挥手的人出现在该城市对应的格子里。做到这一步,跨地域的在线账已经收在同一个云端项目里。

还没有国标摄像机、只准备新装乐橙协议设备的项目,按《开发总览》加进同一账号即可,SIP 六项留到现场出现国标 IPC 或 NVR 时再填。合同已经约定下级平台级联、并且下级平台身份齐全的项目,使用平台级参数,本文这张设备表不要照填。

相关推荐
动词ing1 小时前
【学习笔记】51单片机 模块化编程
笔记·学习·51单片机
广东融科数据服务有限公司1 小时前
网络监控经常卡顿、掉线、画面模糊?一线排查问答实录
开发语言·网络·php
Canmag 兴隆磁性1 小时前
钕铁硼磁片取向方向检测自动化方案:从人工目检到流水线在线分选的工程实践
运维·数据库·自动化·工业自动化·钕铁硼·自动化检测·磁材料
海绵宝宝转agent1 小时前
计网基础知识
网络·笔记·学习·智能路由器
Sarvartha1 小时前
Iterator 迭代器、ListIterator & foreach 循环
开发语言
樱花落木兰1 小时前
Redis 消息队列的可靠性演进:从 List、Pub/Sub 到 Stream 消费者组,以及 8.x 版本改变了什么
java·开发语言·经验分享·redis·分布式·python·软件工程
ACP广源盛139246256731 小时前
Type‑C 扩展坞选型笔记:USB3.0 双通道信号切换方案 LH3412@ACP 评估
笔记·硬件架构·硬件工程·国产芯片·开关
外收内放2 小时前
ENSP学习记录(6)
开发语言·学习·php
dialect_2 小时前
CODEFORCES ROUND 1127 DIV2
开发语言·c++·算法