杭州仓的枪机、成都仓的球机、青岛仓的录像机,各自开在本地录像机网页里。总部夜班要回答一句:现在哪座城市的卸货口还在线。三个网页、三套密码,刷新顺序一变,离线口径就对不上。林工把三地私网地址抄进一张表,在总部 ping,全部超时。网络同事据此建议拉专线。
专线让总部能够访问这些私网地址。三地要出现在同一份设备账里,靠的是另一件事:支持 GB/T 28181 的摄像机或录像机,用 TCP 向同一个云端 SIP 注册;乐橙协议的摄像机绑定到同一个开发者账号。城市和城市之间可以继续互不相通。
读完可以做成五件事,按这个顺序做:
- 分清现场是单台设备向外注册,还是已经有一套下级平台。
- 建成一个国标项目,三地按同一张 SIP 表注册。
- 乐橙协议摄像机进入同一个开发者账号。
- 服务端按城市打出在线通道。
- 给其中一路在线通道签发辅码流 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 时再填。合同已经约定下级平台级联、并且下级平台身份齐全的项目,使用平台级参数,本文这张设备表不要照填。