值班室画面慢半拍?5 分钟拿齐乐橙设备 HLS / FLV / 低延迟三条可播路径

一、对讲已经说完,画面还在「上一句」

周五傍晚,物业值班室。门口访客对着摄像头喊「帮我开门」,对讲里声音几乎实时,墙上的网页预览却还卡在两秒前的背影。值班员下意识敲刷新------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 多传一个参数就能完成。

所以本文对比的「三条路」是:

  1. HLS:兼容面广、适合大屏墙 / 外链分发,延迟通常数秒级。
  2. FLV :Web 端用 flv.js 等拉流,延迟往往优于 HLS。
  3. 低延迟栈(类 WebRTC 体验)getKitToken + imouPlayer,不走公开 m3u8。

2.2 端到端链路(选型前先对齐数据流)

scss 复制代码
                    ┌─ 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 准备清单

  1. 开放平台 创建应用,拿到 appId / appSecret(控制台 → 我的应用 → 应用信息)。
  2. 设备已配网,并绑定到该开发者账号(App 能预览 ≠ 已进开放平台账号,两边都要通)。
  3. 确认通道在线;通道号 IPC 多为 "0"
  4. 只走现行网关:https://openapi.lechange.cn/openapi/{method},请求体含 system + params + id
  5. 不要混用文档里「旧版本协议」栏目下的域名 / 签名方式。

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
javascript 复制代码
// 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

接口:accessTokenparams 可为空;管理员 token 约 3 天 有效,遇 TK1002 再刷,不要每个业务请求都重新取。

javascript 复制代码
// 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

文档:创建设备源直播地址

要点(踩坑素材直接写进选型):

  1. 设备解绑会自动删直播地址。
  2. 创建时后台会默认准备主/辅 × HTTP/HTTPS 多路,但本接口只返回当前所选码流的 HTTP HLS
  3. 要看全量地址,用 getLiveStreamInfo 或控制台。
  4. 直播 URL 公开即可看画面,务必按权限分发,不要写进前端仓库。
javascript 复制代码
// 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);
})();

返回里典型字段:liveTokenstreams[].hls(形如 .../xxx.m3u8)、coverUrl

3.3.2 查询全量 HLS:getLiveStreamInfo

文档:根据序列号获取直播地址和直播状态须先 bindDeviceLive,否则查不到。

javascript 复制代码
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):

html 复制代码
<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

javascript 复制代码
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(仅实时)。

javascript 复制代码
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 播(代码先行)

html 复制代码
<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)

目标若是「值班员说话时画面跟得上」,优先走轻应用组件

  1. 服务端 getKitTokenkitToken2 小时有效,建议自建缓存约 1 小时)。
  2. 前端引入 imou-player.js / imou-player.css,并把 WasmLib 放到 public(文档写明:重要且必须)。
  3. new imouPlayer({ deviceId, channelId, token: kitToken, type: 1, ... })
javascript 复制代码
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;
}
javascript 复制代码
// 浏览器侧(资源从开放平台「资源下载」轻应用套件获取)
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',            // 开了自定义加密则填密钥;仅设备密码则填密码;否则常用序列号
});

多线程解码还需响应头:

makefile 复制代码
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

若你坚持标准 WebRTC,只能自建 media server 转码,成本与运维自担------那已超出「OpenAPI 直接拿地址」范畴。

3.6 一条脚本串起「三种结果」

javascript 复制代码
// 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 直播开着却黑屏?查状态 / 计划

javascript 复制代码
// 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[].status0 正常;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 混用。

五、小结与延伸

同一台乐橙设备,「直播」至少可以拆成三件事:

  1. HLSbindDeviceLive →(可选)getLiveStreamInfo,适合兼容与分发。
  2. FLVcreateDeviceFlvLive / queryDeviceFlvLive + flv.js,适合自建 Web 降延迟。
  3. 低延迟预览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

相关推荐
qq_365185311 小时前
2026B 端工业抖音代运营公司测评:技术驱动破解制造企业获客困局
大数据·人工智能·物联网·制造
BingoGo2 小时前
PHP 引用计数机制深度解析
后端
人间凡尔赛2 小时前
2026 云原生“后容器时代“:WebAssembly 如何重构后端架构?
后端·云原生·架构
IT_陈寒2 小时前
React的useEffect依赖数组把我坑惨了,原来这样写才靠谱
前端·人工智能·后端
木易 士心2 小时前
服务器构建指南:从选型、部署到高可用架构详解
运维·服务器·后端·架构
卷无止境2 小时前
从源码到货架:拆解 Python 打包发布的核心逻辑
后端·python
mldong2 小时前
为什么在 Flowable 时代还要写一个轻量工作流引擎
后端
程序员爱钓鱼3 小时前
Rust 所有权 Ownership 详解:理解内存安全的核心机制
前端·后端·rust
JaguarJack3 小时前
PHP 引用计数机制深度解析
后端·php·服务端