项目经理周五下午四点发来一张工地图:塔吊基础刚开槽,围挡还在拼,光缆路由「下周一再谈」。安监科长的消息紧跟其后------「周一早会要在值班大屏上看塔吊与出入口,别跟我说布线。」
你站在空地上环顾:没有弱电井,没有机房,连临时用电都是配电箱挂表。传统「枪机 + NVR + 专网」在这里不是方案,是工期笑话。
真正能周一交差的路径只有一条:4G IPC 插卡上电 → 设备进开发者资产池 → 后台用现行 OpenAPI 出可播地址 → 安监页嵌预览。 布线可以慢慢做,预览不能等。
下面这篇是智慧工地安监场景的免布线预览落地笔记:可运行 Node.js、真实接口参数、以及把踩坑写进正文的联调过程。
一、「免布线」不是偷懒,是验收时间表
工地安监的第一场仗,很少输在算法,更多输在现场还没网、领导已经要画面。
典型反差是:
- 合同写着「可视化监管」,实施清单却从「挖沟穿管」开始;
- 设备到场了,SIM 卡没开;卡开了,设备却绑在私人 App 号下;
- 后端接口调通了,值班墙用了主码流,流量池两小时见底。
钩子只有一句话:4G 解决「连得上」,开放平台解决「进你的系统、按你的权限播」。 两者缺一,周一早会都会难看。
二、为什么工地安监必须认真对待「4G + 云预览」
2.1 场景真相:临时性、流动性、弱基础设施
| 维度 | 工地现实 | 对视频方案的约束 |
|---|---|---|
| 工期 | 点位随施工阶段迁移 | 不能按永久机房设计 |
| 网络 | 早期常无光纤 / Wi-Fi | 优先 4G / 物联网卡 |
| 角色 | 总包、分包、安监、业主多方看 | 权限必须在业务侧裁决 |
| 成本 | 流量与带宽敏感 | 默认辅码流做值班预览 |
| 验收 | 「大屏能看」先于「全套安防」 | 先最小闭环,再加回放 / 告警 |
开放平台侧与本文相关的能力边界(对照官网「4G 物联网卡」「云直播」「轻应用」等现行说明):
- 4G 物联网卡:插卡即联网,适合户外 / 临时点位上云,不必先等有线。
- 设备资产 OpenAPI :绑定后用
listDeviceDetailsByPage同步在线状态与通道。 - 云直播 :
bindDeviceLive签发 HLS;getLiveStreamInfo查全量地址;createDeviceFlvLive给 Web 低延迟一点的路径。 - 轻应用 :
getKitToken+imouPlayer,PC 网页值班墙延迟更友好,对接周期通常按「天」计。
2.2 解决思路:把「卡、设备、流、人」拆成四层
text
┌──────────────────┐ ┌────────────────┐ ┌──────────────────────┐
│ 4G IPC + 物联网卡 │ → │ 开发者资产池 │ → │ 现行 OpenAPI 出流 │
│ 上电 / 插卡 │ │ bind / 列表 │ │ HLS / FLV / kitToken │
└──────────────────┘ └────────────────┘ └──────────┬───────────┘
│
▼
┌──────────────────────┐
│ 安监业务后台(BFF) │
│ 工地↔设备映射 + 鉴权 │
│ 值班墙 / H5 / 大屏 │
└──────────────────────┘
业务同学不该关心这路是 4G 还是将来换成光纤------他们只该知道:
- 这路在不在线(
deviceStatus/channelStatus); - 怎么拿到可播地址(优先辅码流);
- 谁有权看这一路(你们自己的 JWT / 角色,不是把
m3u8写死在前端)。
2.3 选型对照:三条出流路径怎么配工地
| 路径 | 关键接口 / 组件 | 延迟体感 | 对接成本 | 工地建议 |
|---|---|---|---|---|
| 云直播 HLS | bindDeviceLive |
较高(秒级偏上) | 极低 | 业主 / 分包「看一眼」、外链受限场景 |
| 云直播 FLV | createDeviceFlvLive |
通常优于 HLS | 低 | Web 值班墙第二选择 |
| 轻应用 | getKitToken + imouPlayer |
PC 约 2~3s 量级 | 低~中 | 安监值班室主路径 |
| 移动 OpenSDK | 客户端 SDK | 更低 | 高 | 要做原生安监 App 再上 |
本文主线:4G 接入 + 列表验收 online + 辅码流 HLS 最小闭环,再加一条轻应用值班墙进阶。对讲、云台、复杂告警流水线另文展开,避免第一周范围膨胀。
三、架构、可运行代码与实操步骤
3.1 端到端主流程
3.2 前置:签名自检(不过这一关别写业务)
域名与报文约定(开发规范):
- POST:
https://openapi.lechange.cn:443/openapi/[method] system.ver = "1.0",time为秒级时间戳(与真实时间误差 ≤ 5 分钟)nonce5 分钟内不可复用(否则SN1005)- 签名原始串:
time:{time},nonce:{nonce},appSecret:{appSecret}→ MD5 → 32 位小写
官方标准用例:
text
time:1706511734,nonce:f5a1ae2d-c09c-4d39-a744-83a5c2c653c2,appSecret:test123456789test123456789
→ sign = fd37b62889e4757c58b8f3bf05fb9976
js
// sign-selfcheck.js
const crypto = require('crypto');
const time = '1706511734';
const nonce = 'f5a1ae2d-c09c-4d39-a744-83a5c2c653c2';
const appSecret = 'test123456789test123456789';
const raw = `time:${time},nonce:${nonce},appSecret:${appSecret}`;
const sign = crypto.createHash('md5').update(raw, 'utf8').digest('hex');
console.log(sign);
// 期望: fd37b62889e4757c58b8f3bf05fb9976
封装可复用客户端:
js
// openapi-client.js
const crypto = require('crypto');
const { randomUUID } = require('crypto');
const OPENAPI_BASE = process.env.OPENAPI_BASE || 'https://openapi.lechange.cn/openapi';
const APP_ID = process.env.APP_ID; // 控制台 - 我的应用 - 应用信息
const APP_SECRET = process.env.APP_SECRET;
function buildSign(time, nonce, appSecret) {
const raw = `time:${time},nonce:${nonce},appSecret:${appSecret}`;
return crypto.createHash('md5').update(raw, 'utf8').digest('hex');
}
async function openapiCall(method, params = {}) {
const time = Math.floor(Date.now() / 1000);
const nonce = randomUUID();
const body = {
system: {
ver: '1.0',
appId: APP_ID,
sign: buildSign(time, nonce, APP_SECRET),
time,
nonce,
},
id: randomUUID(),
params,
};
const res = await fetch(`${OPENAPI_BASE}/${method}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const json = await res.json();
if (String(json?.result?.code) !== '0') {
throw new Error(`${method} failed: ${JSON.stringify(json)}`);
}
return json.result.data;
}
module.exports = { openapiCall };
3.3 Step 1:拿管理员 accessToken(约 3 天有效)
params 可为空对象。即将过期或报 TK1002 再刷;勿高频刷刷刷占配额。超过 2 天未满 3 天再调会返回新 token,新旧各自独立可用。
js
// get-token.js
const { openapiCall } = require('./openapi-client');
async function main() {
const data = await openapiCall('accessToken', {});
console.log(data);
// { accessToken: 'At_xxxx', expireTime: 259176 }
}
main().catch(console.error);
3.4 Step 2:现场 4G 上电 + 绑定进开发者资产
现场 SOP(可直接贴进实施手册):
text
1. 物联网卡入网开通(控制台申请 / 运营商侧激活,以现场卡类型为准)
2. 卡插入 4G IPC,上电;等待指示灯进入可联网状态
3. 用开发者主账号在乐橙 App / 控制台确认设备可预览(排除「卡没流量」伪故障)
4. 将设备绑定到开放平台应用对应的开发者账号
- 控制台绑定,或
- HTTP:bindDevice(部分新设备可能需结合客户端 SDK 完成绑定)
5. listDeviceDetailsByPage 验收 deviceStatus === online
bindDevice 参数要点(对照文档):
| 参数 | 说明 |
|---|---|
token |
管理员 accessToken |
deviceId |
设备序列号 |
code |
未改密:标签 / 二维码 8 位安全码;已改密:新密码;无 8 位安全码且未改密:可传空 |
encryptCode |
可选,与 code 二选一,安全要求高时用 AES 规则加密 |
js
// bind-device.js
const { openapiCall } = require('./openapi-client');
async function main() {
const { accessToken } = await openapiCall('accessToken', {});
await openapiCall('bindDevice', {
token: accessToken,
deviceId: process.env.DEVICE_ID,
code: process.env.DEVICE_CODE || '', // 按设备标签实际情况填写
});
console.log('bindDevice ok');
}
main().catch(console.error);
踩坑 ①(真实发生过) :监理用私人乐橙号在 App 里加设备,开发者侧 listDeviceDetailsByPage 永远 0 台,安监页写「暂无画面」。
处理:绑定必须落在公司开发者主账号(或明确托管 / 分享链路),再同步资产。
3.5 Step 3:分页台账------工地安监的「设备是否真在线」
js
// list-devices.js
const { openapiCall } = require('./openapi-client');
async function listAllDevices(accessToken) {
const pageSize = 50; // 文档范围 1~50
let page = 1;
const all = [];
while (true) {
const data = await openapiCall('listDeviceDetailsByPage', {
token: accessToken,
pageSize,
page,
source: 'bindAndShare', // bind | share | bindAndShare
});
const list = data.deviceList || [];
all.push(...list);
if (list.length < pageSize) break;
page += 1;
}
return all;
}
async function main() {
const { accessToken } = await openapiCall('accessToken', {});
const devices = await listAllDevices(accessToken);
const rows = devices.map((d) => ({
deviceId: d.deviceId,
name: d.deviceName,
model: d.deviceModel,
catalog: d.catalog, // IPC / NVR ...
status: d.deviceStatus, // online | offline | sleep | upgrading
channels: (d.channelList || []).map((c) => ({
channelId: c.channelId,
channelStatus: c.channelStatus,
abilities: c.channelAbility,
})),
}));
console.table(
rows.map((r) => ({
deviceId: r.deviceId,
status: r.status,
channel0: r.channels[0]?.channelStatus,
})),
);
}
main().catch(console.error);
业务库建议最小字段:
sql
-- 示意:工地点位与云端设备映射
CREATE TABLE site_camera (
id BIGINT PRIMARY KEY,
site_id VARCHAR(64) NOT NULL, -- 工地项目编号
point_name VARCHAR(128), -- 塔吊 / 出入口 / 临边
device_id VARCHAR(64) NOT NULL,
channel_id VARCHAR(16) NOT NULL DEFAULT '0',
prefer_stream INT NOT NULL DEFAULT 1, -- 工地默认辅码流
UNIQUE (site_id, device_id, channel_id)
);
踩坑 ② :只看 deviceStatus=online,忽略通道 channelStatus。少数场景设备在线但通道休眠 / 异常,出流仍失败。值班墙过滤条件建议两者都过。
3.6 Step 4:最小出画------bindDeviceLive 辅码流
工地 4G 流量金贵:PoC 与多屏轮询一律 streamId: 1(标清辅码流) ;领导验收特写再临时切 0。
接口要点:
- URL:
https://openapi.lechange.cn/openapi/bindDeviceLive - 必填:
token、deviceId、channelId、streamId(0 主 / 1 辅) liveMode可不填或固定"proxy"- 本接口返回当前所选码流的 HTTP HLS ;全量(主/辅 × HTTP/HTTPS)用
getLiveStreamInfo - 直播地址公开即可看,切勿写进前端仓库或永久贴进群公告
js
// smoke-live.js
const { openapiCall } = require('./openapi-client');
async function createHls({ accessToken, deviceId, channelId = '0', streamId = 1 }) {
const data = await openapiCall('bindDeviceLive', {
token: accessToken,
deviceId,
channelId: String(channelId),
streamId,
liveMode: 'proxy',
});
return {
liveToken: data.liveToken,
liveStatus: data.liveStatus, // 1 开启;2 暂停
hls: data.streams?.[0]?.hls,
coverUrl: data.streams?.[0]?.coverUrl,
};
}
async function listAllHls({ accessToken, deviceId, channelId = '0' }) {
// 需先 bindDeviceLive,否则可能查不到
const data = await openapiCall('getLiveStreamInfo', {
token: accessToken,
deviceId,
channelId: String(channelId),
});
return (data.streams || []).map((s) => ({
streamId: s.streamId,
liveToken: s.liveToken,
hls: s.hls,
status: s.status,
coverUrl: s.coverUrl,
}));
}
async function main() {
const { accessToken } = await openapiCall('accessToken', {});
const deviceId = process.env.DEVICE_ID;
const live = await createHls({ accessToken, deviceId, streamId: 1 });
console.log('辅码流 HLS:', live);
const all = await listAllHls({ accessToken, deviceId });
console.log('全量流:', all);
}
main().catch(console.error);
浏览器侧用 hls.js(或原生支持 HLS 的 Safari)播即可。安监 BFF 正确姿势 :前端只拿「短效播放票据」,由服务端校验「该用户能否看该 site_id」后,再返回当前 HTTPS / HTTP 可播地址。
js
// express 示意:短效签发,勿把 appSecret 暴露到浏览器
app.get('/api/sites/:siteId/preview', authMiddleware, async (req, res) => {
const mapping = await db.findCamera(req.params.siteId);
if (!mapping || !canView(req.user, mapping)) {
return res.status(403).json({ error: 'forbidden' });
}
const { accessToken } = await getCachedAdminToken(); // 服务端缓存,勿每次 accessToken
const live = await createHls({
accessToken,
deviceId: mapping.device_id,
channelId: mapping.channel_id,
streamId: mapping.prefer_stream ?? 1,
});
res.json({ hls: live.hls, expireHintSec: 300 });
});
踩坑 ③ :把 bindDeviceLive 返回的 HTTP m3u8 硬塞进 HTTPS 页面,浏览器混合内容拦截。处理:优先取 HTTPS 地址(getLiveStreamInfo 全量里选),或页面与播放同策略降级,并在联调清单里单列一项。
踩坑 ④ :一创建直播就默认主码流 + 四宫格常开,流量池下午告警。处理:轮询预览强制辅码流;无人值守时暂停直播计划(modifyLivePlanStatus 等,见云直播文档),不要 24 小时裸奔拉流。
3.7 Step 5:值班墙要更跟手------轻应用
当安监科说「HLS 慢半拍,塔吊动作对不上口令」,把值班室主路径切到轻应用:
js
// get-kit-token.js
const { openapiCall } = require('./openapi-client');
async function main() {
const { accessToken } = await openapiCall('accessToken', {});
const data = await openapiCall('getKitToken', {
token: accessToken,
deviceId: process.env.DEVICE_ID,
channelId: '0',
type: '1', // 0 全部;1 实时预览;2 录像回放;6 云台
});
console.log(data); // kitToken 有效期约 2 小时;建议服务端缓存约 1 小时
}
main().catch(console.error);
前端(资源从开放平台「资源下载 → 轻应用直播套件」获取,引入 css/js,并按文档放置 WasmLib):
html
<link href="./imou-player.css" rel="stylesheet" />
<script src="./imou-player.js"></script>
<div id="root"></div>
<script>
// kitToken 必须由你们后端签发,前端只消费
const player = new imouPlayer({
id: 'root',
width: 960,
height: 540,
deviceId: 'YOUR_DEVICE_ID',
channelId: 0,
token: 'Kt_xxxx', // getKitToken 返回
type: 1, // 1 直播;2 录播
streamId: 1, // 工地默认标清
templateMode: 'pc',
WasmLibPath: '/', // 按项目 public 实际路径调整
// 自定义加密密钥 / 设备密码场景按文档填 code
});
</script>
多线程解码若启用 SharedArrayBuffer,需按文档配置:
nginx
add_header Cross-Origin-Opener-Policy "same-origin";
add_header Cross-Origin-Embedder-Policy "require-corp";
踩坑 ⑤ :WasmLib 路径错导致 Unexpected token '<'。先独立打开 Wasm 资源 URL,确认不是 HTML 404 页,再调 WasmLibPath。
3.8 Step 6:Web 要 FLV 时
js
// flv-live.js
const { openapiCall } = require('./openapi-client');
async function main() {
const { accessToken } = await openapiCall('accessToken', {});
const data = await openapiCall('createDeviceFlvLive', {
token: accessToken,
deviceId: process.env.DEVICE_ID,
channelId: '0',
type: 'realTime', // realTime | playback
});
console.log({ flv: data.flv, flvHD: data.flvHD });
// 工地默认播 flv(标清);特写再用 flvHD
}
main().catch(console.error);
回放场景再传 type: "playback" + beginTime / endTime(最大跨度 24 小时)+ recordType(localRecord / cloudRecord)。实时预览 PoC 先别搅和回放参数。
3.9 一页纸验收清单
text
□ open.imou.com 应用已创建,拿到 appId / appSecret(仅服务端)
□ sign-selfcheck 与官方用例一致
□ 4G 卡已开通,设备指示灯 / App 可预览
□ 设备绑定在开发者主账号(非私人号)
□ listDeviceDetailsByPage:目标 deviceStatus / channelStatus = online
□ bindDeviceLive streamId=1:hls 非空,liveStatus=1
□ 安监页经 BFF 鉴权后出画(前端无 appSecret)
□ 四宫格压测 30 分钟,流量与卡顿可接受
□ (可选)轻应用 kitToken 出画;COOP/COEP 与 WasmLib 就绪
四、边界、性能与生产注意事项
4.1 边界:4G 预览解决什么、不解决什么
| 做得到 | 别指望这一篇闭环 |
|---|---|
| 临时点位免布线出画 | 替代永久专网的全部合规取证链路 |
| 多工地资产进同一开放平台账号 | 业务侧「谁能看哪路」自动完成------必须自建映射与鉴权 |
| HLS / FLV / 轻应用三条 Web 路径 | 超低延迟对讲指挥(要走 SDK / 对应能力) |
| 辅码流控流量 | 无视套餐的「全点位主码流常开」 |
光纤通了之后,同一套 deviceId + channelId 与 OpenAPI 不用推翻;变的是现场接入介质,不是你的 BFF 契约------这是选开放平台资产模型的隐性收益。
4.2 性能与流量:工地特供策略
- 默认辅码流 :列表页、轮询、四宫格一律
streamId: 1/flv。 - 按需出流 :进入详情再
bindDeviceLive/getKitToken;离开销毁播放器(轻应用destroy())。 - token 缓存 :
accessToken按过期时间缓存;kitToken建议缓存约 1 小时(文档有效期 2 小时)。 - 并发墙 :多路轻应用比纯
video+hls更吃 CPU(解密 + canvas)。弱终端减少同屏路数,或降清晰度。 - 卡池监控:流量告警与「主码流误开」告警打到同一值班群,否则总在周末爆。
4.3 生产环境注意
- 密钥 :
appSecret只活在服务端;CI 用环境变量,别进前端包。 - 直播 URL:等同于公开摄像头;结合登录态、短效签发、审计日志(谁、何时、哪路、哪工地)。
- 绑定策略 :新设备可能无法仅靠 HTTP
bindDevice,预留 App / OpenSDK 配网绑定预案(见「应用开发」现行说明)。 - 旧协议:文档树里的「旧版本协议(后续不再维护)」整栏跳过;列表、直播、轻应用都走现行章节。
- 合规 :工地画面常含人员与作业过程,对外分享、分包账号要最小化权限,避免把永久
m3u8丢进微信群。
4.4 扩展方向(本文不展开,可作下一篇)
- 云录像 +
createDeviceRecordHls:事故回溯; - 消息推送:周界 / 人形告警进工单;
- 设备托管:总包统管、分包只读;
- 国标利旧:后期固定点位混接 GB28181(与 4G 新点位同一资产池消费)。
五、小结与延伸
工地安监第一周的胜利条件,不是「上齐整套智慧工地平台」,而是:
- 现场:4G IPC + 物联网卡,免布线连上云;
- 资产 :开发者账号绑定,
listDeviceDetailsByPage能对上台账; - 出画 :
bindDeviceLive辅码流跑通,BFF 鉴权后嵌进安监页; - 值班 :需要更低延迟时,再上
getKitToken+ 轻应用。
把「卡是否有流量」「设备绑在谁名下」「有没有误开主码流」这三件土事写进验收清单,比堆功能更能保住周一早会。
附录:关键返回字段速查
| 接口 | 关键字段 | 工地用法 |
|---|---|---|
accessToken |
accessToken / expireTime |
服务端缓存 |
listDeviceDetailsByPage |
deviceStatus / channelList[].channelId |
台账与上线验收 |
bindDeviceLive |
liveToken / streams[].hls / liveStatus |
最小 HLS 闭环 |
getLiveStreamInfo |
streams[] |
取 HTTPS / 主辅全量 |
createDeviceFlvLive |
flv / flvHD |
Web FLV |
getKitToken |
kitToken(约 2h) |
轻应用值班墙 |