一、对讲已经说完,画面还在「上一句」
周五傍晚,物业值班室。门口访客对着摄像头喊「帮我开门」,对讲里声音几乎实时,墙上的网页预览却还卡在两秒前的背影。值班员下意识敲刷新——m3u8 还在播,就是「对不上口型」。
同款事故我见过好几次:有人把「拿到直播地址」当成终点,却没问清楚——要给谁看、延迟能不能忍、浏览器能不能直接播。乐橙设备侧其实已经把几条现行路径拆开了:HLS 适合广覆盖分发,FLV 适合 Web 降一档延迟,真正要冲「对话级」观感,往往不是再多拼一个 .m3u8,而是换播放栈。
下面用一套可复用的服务端壳,把三条路都摸到可播。
二、为什么「直播地址」值得单独拆一篇
2.1 先画边界:OpenAPI 直出什么,什么要另选栈
现行设备直播模块里,和「实时预览地址」强相关的接口大致是:
| 能力 | 接口 | 典型产物 |
|---|---|---|
| 创建 HLS 直播 | bindDeviceLive | 指定码流的 HTTP HLS |
| 查询全量 HLS | getLiveStreamInfo | 主/辅码流 × HTTP/HTTPS |
| 创建 / 查询 FLV | createDeviceFlvLive / queryDeviceFlvLive | flv / flvHD |
| RTMP(小程序等) | createDeviceRtmpLive | rtmp 地址(本文不展开) |
| 轻应用凭证 | getKitToken | kitToken,配合 imouPlayer |
社区里常把 WebRTC 和上面几种混为一谈。需要先说清:
现行 OpenAPI 不会直接返回
webrtc://或标准 WHIP/WHEP 信令地址。
若你要的是「浏览器里接近对话级」的延迟,官方可行路径是 轻应用 JS 组件(Wasm 解码私有流) 或 客户端 OpenSDK 实时预览;自建网关把 FLV/RTSP 再转 WebRTC 属于架构自选,不是某个bindXxx多传一个参数就能完成。
所以本文对比的「三条路」是:
- HLS:兼容面广、适合大屏墙 / 外链分发,延迟通常数秒级。
- FLV:Web 端用
flv.js等拉流,延迟往往优于 HLS。 - 低延迟栈(类 WebRTC 体验):
getKitToken+imouPlayer,不走公开 m3u8。
2.2 端到端链路(选型前先对齐数据流)
┌─ bindDeviceLive ──────► HLS (.m3u8)
设备在线 + 已绑定 ──┤
accessToken ───────┼─ createDeviceFlvLive ─► FLV (flv / flvHD)
│
└─ getKitToken ─────────► imouPlayer(轻应用低延迟预览)
2.3 延迟与场景怎么对齐(经验量级,非 SLA)
| 路径 | 常见延迟观感 | 适合 | 不适合 |
|---|---|---|---|
| HLS | 约 2~6s+(分片与缓冲相关) | 多端兼容、外部分发、大屏轮巡 | 强互动对讲同屏 |
| FLV | 通常优于 HLS | 自建 Web 监控页、内网值班 | 需要原生 <video> 无插件时需注意兼容 |
| 轻应用 / SDK | 更接近「实时预览」 | 要控延迟、对讲、云台同页 | 只要一条可公开的永久 URL |
选型一句话:能忍数秒 → HLS;Web 要快一点 → FLV;要对上口型 → 轻应用 / SDK。
三、从签名到三种可播结果
3.0 准备清单
- 在 开放平台 创建应用,拿到
appId/appSecret(控制台 → 我的应用 → 应用信息)。 - 设备已配网,并绑定到该开发者账号(App 能预览 ≠ 已进开放平台账号,两边都要通)。
- 确认通道在线;通道号 IPC 多为
"0"。 - 只走现行网关:
https://openapi.lechange.cn/openapi/{method},请求体含system+params+id。 - 不要混用文档里「旧版本协议」栏目下的域名 / 签名方式。
3.1 签名 + 统一请求壳(先贴代码)
按开发规范:sign = MD5("time:{time},nonce:{nonce},appSecret:{appSecret}"),UTF-8,32 位小写。可用文档标准案例自测:
- 原始串:
time:1706511734,nonce:f5a1ae2d-c09c-4d39-a744-83a5c2c653c2,appSecret:test123456789test123456789 - 期望 sign:
fd37b62889e4757c58b8f3bf05fb9976
// openapi-client.js
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
const OPENAPI_BASE = 'https://openapi.lechange.cn/openapi';
function calcSign(time, nonce, appSecret) {
const raw = `time:${time},nonce:${nonce},appSecret:${appSecret}`;
return crypto.createHash('md5').update(raw, 'utf8').digest('hex');
}
async function callOpenApi(method, appId, appSecret, params = {}) {
const time = Math.floor(Date.now() / 1000);
const nonce = uuidv4();
const body = {
system: {
ver: '1.0',
appId,
time,
nonce,
sign: calcSign(time, nonce, appSecret),
},
id: uuidv4(),
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 (!json.result || json.result.code !== '0') {
const msg = json.result ? `${json.result.code}: ${json.result.msg}` : JSON.stringify(json);
throw new Error(`[${method}] ${msg}`);
}
return json.result.data;
}
module.exports = { callOpenApi, calcSign };
解释(短):time 与服务器误差超过 5 分钟、或 5 分钟内 nonce 复用,会分别踩鉴权 / SN1005。业务失败看 result.code,不要只看 HTTP 200。
3.2 拿管理员 accessToken
接口:accessToken。params 可为空;管理员 token 约 3 天有效,遇 TK1002 再刷,不要每个业务请求都重新取。
// get-token.js
const { callOpenApi } = require('./openapi-client');
async function getAccessToken(appId, appSecret) {
const data = await callOpenApi('accessToken', appId, appSecret, {});
// data.accessToken / data.expireTime(剩余秒数)
return data;
}
module.exports = { getAccessToken };
3.3 路径 A:5 分钟拿到 HLS
3.3.1 创建:bindDeviceLive
文档:创建设备源直播地址。
要点(踩坑素材直接写进选型):
- 设备解绑会自动删直播地址。
- 创建时后台会默认准备主/辅 × HTTP/HTTPS 多路,但本接口只返回当前所选码流的 HTTP HLS。
- 要看全量地址,用
getLiveStreamInfo或控制台。 - 直播 URL 公开即可看画面,务必按权限分发,不要写进前端仓库。
// create-hls.js
const { callOpenApi } = require('./openapi-client');
const { getAccessToken } = require('./get-token');
async function createHlsLive({ appId, appSecret, deviceId, channelId = '0', streamId = 1 }) {
const { accessToken } = await getAccessToken(appId, appSecret);
// streamId: 0 高清主码流;1 标清辅码流
const data = await callOpenApi('bindDeviceLive', appId, appSecret, {
token: accessToken,
deviceId,
channelId,
streamId,
// liveMode 可不填,或固定 "proxy"
});
return {
liveToken: data.liveToken,
liveStatus: data.liveStatus, // 1 开启;2 暂停
hls: data.streams?.[0]?.hls,
coverUrl: data.streams?.[0]?.coverUrl,
};
}
// 示例
(async () => {
const r = await createHlsLive({
appId: process.env.IMOU_APP_ID,
appSecret: process.env.IMOU_APP_SECRET,
deviceId: process.env.IMOU_DEVICE_ID,
channelId: '0',
streamId: 1,
});
console.log(r);
})();
返回里典型字段:liveToken、streams[].hls(形如 .../xxx.m3u8)、coverUrl。
3.3.2 查询全量 HLS:getLiveStreamInfo
文档:根据序列号获取直播地址和直播状态。须先 bindDeviceLive,否则查不到。
async function listAllHls({ appId, appSecret, deviceId, channelId = '0' }) {
const { accessToken } = await getAccessToken(appId, appSecret);
const data = await callOpenApi('getLiveStreamInfo', appId, appSecret, {
token: accessToken,
deviceId,
channelId,
});
// streams 通常含:主/辅 × http/https 四类
return (data.streams || []).map((s) => ({
streamId: s.streamId,
liveToken: s.liveToken,
hls: s.hls,
status: s.status, // "0" 正常直播中;"10" 暂停等,见文档
coverUrl: s.coverUrl,
}));
}
浏览器侧最小播放(原生 HLS,Safari / 部分环境;Chrome 可用 hls.js):
<video id="v" controls autoplay muted playsinline></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
<script>
const url = 'https://cmgw-vpc.lechange.com:8890/LCO/.../dev_xxx.m3u8?proto=https';
const video = document.getElementById('v');
if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url;
} else if (window.Hls && Hls.isSupported()) {
const hls = new Hls({ enableWorker: true });
hls.loadSource(url);
hls.attachMedia(video);
}
</script>
HTTPS 页面请优先用返回里带 ?proto=https 的地址,避免混合内容拦截。
3.4 路径 B:FLV 降延迟
3.4.1 创建:createDeviceFlvLive
文档:创建设备 flv 直播。实时预览 type 默认 / 填 realTime;回放才需要 beginTime / endTime / recordType。
async function createFlvLive({ appId, appSecret, deviceId, channelId = '0' }) {
const { accessToken } = await getAccessToken(appId, appSecret);
const data = await callOpenApi('createDeviceFlvLive', appId, appSecret, {
token: accessToken,
deviceId,
channelId,
type: 'realTime',
});
// flv: 标清;flvHD: 高清
return { flv: data.flv, flvHD: data.flvHD };
}
已创建过可再查:queryDeviceFlvLive(仅实时)。
async function queryFlvLive({ appId, appSecret, deviceId, channelId = '0' }) {
const { accessToken } = await getAccessToken(appId, appSecret);
return callOpenApi('queryDeviceFlvLive', appId, appSecret, {
token: accessToken,
deviceId,
channelId,
});
}
3.4.2 前端用 flv.js 播(代码先行)
<script src="https://cdn.jsdelivr.net/npm/flv.js/dist/flv.min.js"></script>
<video id="flvPlayer" controls muted></video>
<script>
const flvUrl = 'https://....../xxx.flv?proto=https'; // 服务端下发,勿写死密钥侧逻辑
if (flvjs.isSupported()) {
const player = flvjs.createPlayer({
type: 'flv',
url: flvUrl,
isLive: true,
hasAudio: true,
hasVideo: true,
});
player.attachMediaElement(document.getElementById('flvPlayer'));
player.load();
player.play().catch(console.error);
}
</script>
踩坑:部分 FLV 地址带 expire / digest,有时效;页面打开时再向你自己的后端换地址,不要提前缓存半小时再播。轻应用 FAQ 也强调:流地址要使用时再取,避免超时无效。
3.5 路径 C:低延迟体验(轻应用,而不是伪造 WebRTC URL)
目标若是「值班员说话时画面跟得上」,优先走轻应用组件:
- 服务端
getKitToken(kitToken约 2 小时有效,建议自建缓存约 1 小时)。 - 前端引入
imou-player.js/imou-player.css,并把 WasmLib 放到public(文档写明:重要且必须)。 new imouPlayer({ deviceId, channelId, token: kitToken, type: 1, ... })。
async function getKitToken({ appId, appSecret, deviceId, channelId = '0', type = '1' }) {
// type: 0 全部;1 实时预览;2 录像回放;6 云台
const { accessToken } = await getAccessToken(appId, appSecret);
const data = await callOpenApi('getKitToken', appId, appSecret, {
token: accessToken,
deviceId,
channelId,
type,
});
// 返回字段以现行文档样例为准,常见为 kitToken
return data;
}
// 浏览器侧(资源从开放平台「资源下载」轻应用套件获取)
const player = new imouPlayer({
id: 'root',
width: 800,
height: 450,
deviceId: 'YOUR_DEVICE_ID',
channelId: 0,
token: kitTokenFromServer, // Kt_ 开头的轻应用 token
type: 1, // 1 直播
streamId: 0, // 0 高清;1 标清
WasmLibPath: '/', // 按项目 public 路径调整
code: 'xxxxxx', // 开了自定义加密则填密钥;仅设备密码则填密码;否则常用序列号
});
多线程解码还需响应头:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
若你坚持标准 WebRTC,只能自建 media server 转码,成本与运维自担——那已超出「OpenAPI 直接拿地址」范畴。
3.6 一条脚本串起「三种结果」
// demo-three-paths.js
require('dotenv').config();
const { createHlsLive, /* 或内联上面函数 */ } = require('./create-hls'); // 按你拆分的文件调整
// 为简洁,此处假设已把 createFlvLive / getKitToken 导出
(async () => {
const cfg = {
appId: process.env.IMOU_APP_ID,
appSecret: process.env.IMOU_APP_SECRET,
deviceId: process.env.IMOU_DEVICE_ID,
channelId: '0',
};
const hls = await createHlsLive({ ...cfg, streamId: 1 });
console.log('[HLS]', hls.hls, 'liveToken=', hls.liveToken);
const flv = await createFlvLive(cfg);
console.log('[FLV]', flv.flvHD || flv.flv);
const kit = await getKitToken({ ...cfg, type: '1' });
console.log('[KitToken]', kit);
console.log('\n选型提示: 外链大屏→HLS;自建网页降延迟→FLV;要对口型→轻应用播 kitToken');
})();
依赖示例:npm i uuid dotenv(Node 18+ 自带 fetch)。
3.7 直播开着却黑屏?查状态 / 计划
// queryLiveStatus:按 liveToken 查
async function queryLiveStatus({ appId, appSecret, liveToken }) {
const { accessToken } = await getAccessToken(appId, appSecret);
return callOpenApi('queryLiveStatus', appId, appSecret, {
token: accessToken,
liveToken,
});
}
// modifyLivePlanStatus:on / off(注意接口名是 modify,不是列表页笔误的 query)
async function setLivePlanStatus({ appId, appSecret, liveToken, status = 'on' }) {
const { accessToken } = await getAccessToken(appId, appSecret);
return callOpenApi('modifyLivePlanStatus', appId, appSecret, {
token: accessToken,
liveToken,
status,
});
}
streams[].status:0 正常;10 暂停中;2 视频源异常等——先对状态,再怀疑播放器。
四、边界、性能与生产注意
4.1 安全与权限
- HLS/FLV URL 等同于可分享的观看凭证。生产环境应由业务后端按会话鉴权后下发,设短 TTL,日志脱敏。
appSecret、管理员accessToken只放服务端;前端最多持有短期kitToken或一次性播放 URL。- 设备解绑、
unbindLive/deleteDeviceFlvLive会切断旧地址,换机或撤权时记得清客户端缓存。
4.2 性能与并发
- 大屏墙多路 HLS:优先辅码流(
streamId: 1),降带宽;关键路再用主码流。 - 多路轻应用同页:Wasm + canvas 比原生 video 更吃 CPU,机器弱时会卡——文档已提示「多播放器卡顿」。
- FLV / 轻应用流:用时再取;并发乱取多路流源,容易 404 / 串流。
4.3 常见踩坑清单(联调日记)
| 现象 | 更可能原因 | 处理 |
|---|---|---|
SN1005 | nonce 5 分钟内重复 | 每次新 UUID |
| sign 失败 | 拼串顺序 / 编码不对 | 跑文档标准案例 |
TK1002 | accessToken 过期 | 刷新并缓存 |
getLiveStreamInfo 空 | 未先 bindDeviceLive | 先创建 |
| HTTPS 页 HLS 失败 | 用了 http:// 地址 | 换 ?proto=https |
| FLV 突然播不了 | URL 过期 | 重新 create / query |
| 轻应用 Wasm 报 Unexpected token '<' | WasmLib 路径 404 成了 HTML | 配 WasmLibPath |
| 对讲有声画面慢 | 选了 HLS 同屏 | 对讲场景改轻应用 / SDK |
接口列表写 queryLivePlanStatus | 文档目录笔误 | 实际调用 modifyLivePlanStatus |
4.4 和 RTMP / 录像 HLS 的边界
- 小程序推流对讲等场景会用到
createDeviceRtmpLive,与网页 FLV 拉流不是同一条产品路径。 createDeviceRecordHls是录像片段转 HLS,不是实时预览;别和bindDeviceLive混用。
五、小结与延伸
同一台乐橙设备,「直播」至少可以拆成三件事:
- HLS:
bindDeviceLive→(可选)getLiveStreamInfo,适合兼容与分发。 - FLV:
createDeviceFlvLive/queryDeviceFlvLive+flv.js,适合自建 Web 降延迟。 - 低延迟预览:
getKitToken+imouPlayer(或客户端 OpenSDK),才是冲「口型对齐」的那条路——别再空想 OpenAPI 直接吐 WebRTC URL。
延伸阅读(均属现行文档):
若你手头已有设备、还缺一套可调用的云端能力与播放组件,可在 乐橙开放平台 open.imou.com 注册开发者应用:平台以视频与安全能力为核心,开放 OpenAPI 与低代码播放组件,方便把设备预览、直播分发接到自己的网页或业务系统里。注册后从控制台取 appId / appSecret,按本文 accessToken → 三路径顺序,一般半天内就能在自己的页面看到第一帧画面。
注册入口:open.imou.com(登录 / 注册 → 创建应用 → 绑定设备 → 调 OpenAPI)
文内接口一览(便于检索)
accessToken · bindDeviceLive · getLiveStreamInfo · createDeviceFlvLive · queryDeviceFlvLive · queryLiveStatus · modifyLivePlanStatus · getKitToken