鸡圈在乡下、人在城里:户外 4G 机 + 云直播,把庭院画面嵌进自家小程序

10 阅读17分钟

一、周六早上,邻居电话比鸡还早

周六七点,城里手机响了。乡下邻居说:院门开着,鸡在路上啄菜。你打开那只「能看院子」的 App——设备离线。再拨村里的侄子,他回:「Wi-Fi 是隔壁借的,路由器上周就没电。」

院子里其实已经挂着户外机。缺的不是镜头,是一条不靠邻家宽带、还能嵌进自家小程序的远程看路径:4G 插卡上电,云端签发可播地址,家人点开就能看。


二、为什么「院子能看」值得单独做成一条工程路径

2.1 庭院 / 小农场不是「再装一路监控」

城里看乡下院子,真正卡住的通常是这四件事同时成立:

  1. 没网:菜地、鸡圈、院门很少有稳定光纤或 Wi-Fi。
  2. 人远:业主在市区,侄子在镇上,要看的是同一路画面。
  3. 流量贵:4G 卡按量计费,主码流 24 小时裸奔,月账单比鸡还贵。
  4. 要进自己的页:家人要嵌进小程序 / 公众号 H5,而不是每人再下一套 App。

开放平台侧,和这条路径直接相关的现行能力可以收成一张表:

业务问题现行能力 / 接口庭院场景怎么用
院子没宽带4G 物联网卡:插卡联网,适用智慧养殖 / 户外点位户外 4G 机上电即上云,先不要挖沟
设备在不在、在不在线listDeviceDetailsByPage台账过滤 deviceStatus + channelStatus 都为 online
家人点开看现在bindDeviceLivestreamId=1)+ getLiveStreamInfo默认辅码流 HTTPS HLS,嵌 H5
别 24 小时烧流量batchModifyLivePlan / modifyLivePlan + modifyLivePlanStatus只在早饲、午巡、晚归开流
Web 值班墙要更低延迟createDeviceFlvLivegetKitToken + ImouPlayer业主后台第二路径,不要和家人 H5 揉进同一个 <video>

选型不要一上来啃私有协议。开发总览里的对接分层,用在庭院远程看也成立:

路径关键接口 / 组件延迟体感对接成本庭院建议
云直播 HLSbindDeviceLive较高(秒级偏上)极低家人「看一眼」主路径
云直播 FLVcreateDeviceFlvLive通常优于 HLS业主 Web 值班墙第二选择
轻应用getKitToken + imouPlayerPC 约 2~3s 量级低~中要回放 / 云台时再上
移动 OpenSDK客户端 SDK更低要做原生农事 App 再上

本文主线:4G 接入 + 列表验收 online + 辅码流 HLS + 看园档期。对讲、云台深度控制、告警流水线另文展开,避免第一周范围膨胀。

2.2 解决思路:把「卡、设备、流、人」拆成四层

┌──────────────────┐    ┌────────────────┐    ┌──────────────────────┐
│ 户外 4G 机 + 物联网卡 │ → │ 开发者资产池     │ → │ 现行 OpenAPI 出流      │
│ 上电 / 插卡       │    │ bind / 列表     │    │ HLS / FLV / kitToken  │
└──────────────────┘    └────────────────┘    └──────────┬───────────┘
                                                         │
                                                         ▼
                                               ┌──────────────────────┐
                                               │ 农事业务后端(BFF)     │
                                               │ 院落↔设备映射 + 鉴权    │
                                               │ 家人 H5 / 小程序 web-view │
                                               └──────────────────────┘

业务同学不该关心这路是 4G 还是将来换成光纤——他们只该知道:

  • 这路在不在线(deviceStatus / channelStatus);
  • 怎么拿到可播地址(优先辅码流 HTTPS);
  • 谁有权看这一路(你们自己的登录态,不是把 m3u8 写死在前端)。

一句话:4G 解决「连得上」;云直播解决「进你的系统、按你的档期播」;业务后端解决「谁能看哪个院子」。三者缺一,邻居电话来了你还是只能干瞪眼。


三、从插卡上电到 H5 出画

3.1 端到端主流程

flowchart TB
  subgraph A[现场接入]
    S1[户外 4G 机插物联网卡上电]
    S2[确认设备联网 / App 可看]
    S3[开发者账号绑定设备]
  end
  subgraph B[云端资产]
    T[accessToken]
    L[listDeviceDetailsByPage]
    M[(本地表: yardId / deviceId / channelId / liveToken)]
  end
  subgraph C[出流消费]
    H[bindDeviceLive streamId=1]
    I[getLiveStreamInfo 取 HTTPS]
    P[batchModifyLivePlan 看园档期]
    W[家人 H5 / 小程序 web-view]
  end
  S1 --> S2 --> S3 --> L
  T --> L --> M
  M --> H --> I --> W
  H --> P

环境约定:Node.js ≥ 18(自带 fetch / crypto)。先在 open.imou.com 创建应用,于「控制台 → 我的应用 → 应用信息」取 appId / appSecret

mkdir farm-yard-live && cd farm-yard-live
npm init -y
# .env   永远不要写成 VITE_ / REACT_APP_ 前缀,否则会被打进前端包
IMOU_APP_ID=lcdxxxxxxxxx
IMOU_APP_SECRET=your_secret
DEVICE_ID=TESTQWERXXXX
DEVICE_CODE=        # 未改密:标签/二维码 8 位安全码;已改密:新密码

3.2 现行签名壳:HMAC-SHA256,不要抄网上的 MD5

请求统一:

POST https://openapi.lechange.cn/openapi/{method}

开发规范 现行算法分三步:

原始串 = time:{秒},nonce:{随机串},appSecret:{密钥}
password = lowercase(hex(SHA-256(appSecret)))
sign     = Base64(HMAC-SHA256(原始串, password))

time 是 UTC 秒级时间戳,与服务器误差超过 5 分钟会返回 SN1002nonce 五分钟内不能复用,否则 SN1005。官方标准案例可自测:time=1706511734nonce=f5a1ae2d-c09c-4d39-a744-83a5c2c653c2appSecret=test123456789test123456789,算出来的 sign 必须是:

xjhCQBoJ9hRDsCjyDcHjtDNzRZ3ZJezcawsfWeiaoxU=

对不上,先别调任何业务接口。网上大量「MD5 32 位小写」示例对应的是已不维护的旧协议,新接入按现行规范走 HMAC。

// server/imou-client.js
import crypto from 'node:crypto';
import { randomUUID } from 'node:crypto';

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

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');
}

export async function callOpenApi(method, appId, appSecret, params = {}) {
  const time = Math.floor(Date.now() / 1000);
  const nonce = randomUUID();
  const body = {
    system: { ver: '1.0', appId, time, nonce, sign: calcSign(time, nonce, appSecret) },
    id: randomUUID(),
    params,
  };

  const res = await fetch(`${OPENAPI}/${method}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (String(json?.result?.code) !== '0') {
    const err = new Error(`${method} failed: ${json?.result?.code} ${json?.result?.msg}`);
    err.payload = json;
    throw err;
  }
  return json.result.data;
}

解释appSecret 只出现在这一层。浏览器、小程序、埋点日志里都不该出现它。乡下机房或云主机都要开 NTP,避免整点前后批量 SN1002

自测脚本(先跑通再写业务):

// sign-selfcheck.js
import crypto from 'node:crypto';

const time = '1706511734';
const nonce = 'f5a1ae2d-c09c-4d39-a744-83a5c2c653c2';
const appSecret = 'test123456789test123456789';
const raw = `time:${time},nonce:${nonce},appSecret:${appSecret}`;
const password = crypto.createHash('sha256').update(appSecret, 'utf8').digest('hex');
const sign = crypto.createHmac('sha256', password).update(raw, 'utf8').digest('base64');

console.log(sign);
// 期望: xjhCQBoJ9hRDsCjyDcHjtDNzRZ3ZJezcawsfWeiaoxU=

踩坑 1:从旧博客抄来 md5(time:...,nonce:...,appSecret:...),标准案例对不上,后面所有接口都会 SN1001。不是设备坏了,是签名壳过期了。

3.3 Step 1:拿管理员 accessToken(约 3 天有效)

accessTokenparams 可空;返回 accessTokenexpireTime剩余秒数)。有效期约 3 天;过期或 TK1002 再取。超过 2 天再请求会拿到新 token,新旧各自独立可用,但不要为了「刷新」而打满调用次数。

// server/access-token.js
import { callOpenApi } from './imou-client.js';

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

export async function getAccessToken(appId, appSecret) {
  const now = Date.now();
  if (cache.value && now < cache.expireAt) return cache.value;

  const data = await callOpenApi('accessToken', appId, appSecret, {});
  cache = {
    value: data.accessToken,
    // expireTime 是剩余秒;提前 2 小时刷新,避开临界窗
    expireAt: now + (Number(data.expireTime) - 7200) * 1000,
  };
  return cache.value;
}

踩坑 2:每次家人点开页面都打一次 accessToken。文档写得很清楚:每个管理员 token 具备独立的 3 天生命周期,频繁调用会占配额。缓存即可。

3.4 Step 2:现场 4G 上电 + 绑定进开发者资产

现场 SOP(可直接贴进实施手册):

1. 物联网卡入网开通(控制台申请 / 运营商侧激活,以现场卡类型为准)
2. 卡插入户外 4G 机,上电;等待指示灯进入可联网状态
3. 用开发者主账号在乐橙 App / 控制台确认设备可预览(排除「卡没流量」伪故障)
4. 将设备绑定到开放平台应用对应的开发者账号
   - 控制台绑定,或
   - HTTP:bindDevice(部分新设备可能需结合客户端 SDK 完成绑定)
5. listDeviceDetailsByPage 验收 deviceStatus === online

bindDevice 参数要点:

参数说明
token管理员 accessToken
deviceId设备序列号
code未改密:标签 / 二维码 8 位安全码;已改密:新密码;无 8 位安全码且未改密:可传空
encryptCode可选,与 code 二选一,安全要求高时用文档给出的 AES 规则加密
// bind-device.js
import { callOpenApi } from './server/imou-client.js';
import { getAccessToken } from './server/access-token.js';

const APP_ID = process.env.IMOU_APP_ID;
const APP_SECRET = process.env.IMOU_APP_SECRET;

async function main() {
  const token = await getAccessToken(APP_ID, APP_SECRET);
  await callOpenApi('bindDevice', APP_ID, APP_SECRET, {
    token,
    deviceId: process.env.DEVICE_ID,
    code: process.env.DEVICE_CODE || '',
  });
  console.log('bindDevice ok');
}

main().catch(console.error);

踩坑 3(真实发生过):侄子用私人乐橙号在 App 里加设备,开发者侧 listDeviceDetailsByPage 永远 0 台,家人页写「暂无画面」。
处理:绑定必须落在公司 / 业主开发者主账号(或明确托管 / 分享链路),再同步资产。App 能看、列表没有,多半还没绑进开发者账号。

3.5 Step 3:分页台账——「设备是否真在线」

分页查询设备详细信息 是现行列表接口。page 从 1 起,pageSize 为 1–50;source 默认 bindAndShare。返回数组字段叫 deviceList,通道在 channelList——不要去调已停维护栏目里的旧列表方法名

// server/list-yard-devices.js
import { callOpenApi } from './imou-client.js';
import { getAccessToken } from './access-token.js';

export async function listAllDevices(appId, appSecret) {
  const token = await getAccessToken(appId, appSecret);
  const pageSize = 50;
  let page = 1;
  const all = [];

  while (true) {
    const data = await callOpenApi('listDeviceDetailsByPage', appId, appSecret, {
      token,
      pageSize,
      page,
      source: 'bindAndShare', // bind | share | bindAndShare
    });
    const list = data.deviceList || [];
    all.push(...list);
    if (list.length < pageSize) break;
    page += 1;
  }
  return all;
}

export function toYardRows(devices) {
  return 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,
      cameraStatus: c.cameraStatus, // on=遮罩打开(画面被盖)
      abilities: c.channelAbility,
    })),
  }));
}

业务库建议最小字段:

-- 示意:院落点位与云端设备映射
CREATE TABLE yard_camera (
  id            BIGINT PRIMARY KEY,
  yard_id       VARCHAR(64) NOT NULL,  -- 院落 / 地块编号
  point_name    VARCHAR(128),          -- 院门 / 鸡圈 / 菜地
  device_id     VARCHAR(64) NOT NULL,
  channel_id    VARCHAR(16) NOT NULL DEFAULT '0',
  live_token    VARCHAR(64),
  prefer_stream INT NOT NULL DEFAULT 1, -- 4G 默认辅码流
  UNIQUE (yard_id, device_id, channel_id)
);

踩坑 4:只看 deviceStatus=online,忽略通道 channelStatus。少数场景设备在线但通道休眠 / 异常,出流仍失败。家人页过滤条件建议两者都过。cameraStatus === "on" 时遮罩是盖上的,播放器出画也会是一块隐私盖,别当成组件坏了。sleep 在户外 4G 机上很常见——省电策略把通道睡过去,列表不是「没这台」,是「这台在睡觉」。

3.6 Step 4:最小出画——bindDeviceLive 辅码流

4G 流量金贵:家人预览一律 streamId: 1(标清辅码流);业主要看鸡脚环、看菜叶病斑,再临时切 0

bindDeviceLive 要点:

  • URL:https://openapi.lechange.cn/openapi/bindDeviceLive
  • 必填:tokendeviceIdchannelIdstreamId(0 主 / 1 辅)
  • liveMode 可不填或固定 "proxy"
  • 后台会默认创建 主/辅码流 × HTTP/HTTPS 共四类地址;本接口只返回当前所选码流的 HTTP HLS
  • 全量地址用 getLiveStreamInfo,或到开发者控制台直播服务页查看
  • 设备解绑会自动删除直播地址
  • 直播地址对外公开后,他人可直接看画面——当机密句柄,不要印在院门口的铁皮牌上
// server/create-yard-live.js
import { callOpenApi } from './imou-client.js';
import { getAccessToken } from './access-token.js';

export async function createYardLive(appId, appSecret, {
  deviceId,
  channelId = '0',
  streamId = 1,
}) {
  const token = await getAccessToken(appId, appSecret);
  const data = await callOpenApi('bindDeviceLive', appId, appSecret, {
    token,
    deviceId,
    channelId: String(channelId),
    streamId,
    liveMode: 'proxy',
  });

  // liveToken:后续改计划 / 启停的唯一句柄,务必入库
  return {
    liveToken: data.liveToken,
    liveStatus: data.liveStatus, // 1 开启;2 暂停
    httpHls: data.streams?.[0]?.hls, // 通常只回所选码流的 HTTP
    coverUrl: data.streams?.[0]?.coverUrl,
    deviceId: data.deviceId,
    channelId: data.channelId,
    job: data.job, // 新建后常见 period: always, status: true
  };
}

踩坑 5:创建完立刻把返回的 http://…m3u8 塞进微信。内置浏览器对明文 HTTP 媒体极不友好,常见「转圈无画面」。下一步必须拿 HTTPS。

3.7 Step 5:getLiveStreamInfo 取齐 HTTPS 与状态

getLiveStreamInfo:需先 bindDeviceLive,否则查不到。一次能拿到高清主码流、标清辅码流、HTTP、HTTPS 四类地址。

streams[].status 对照:

status含义
0正在直播中
1正在直播中,但视频封面异常
2视频源异常
3码流转换异常
4云存储访问异常
10直播暂停中
// server/get-yard-streams.js
import { callOpenApi } from './imou-client.js';
import { getAccessToken } from './access-token.js';

export async function getYardStreams(appId, appSecret, { deviceId, channelId = '0' }) {
  const token = await getAccessToken(appId, appSecret);
  const data = await callOpenApi('getLiveStreamInfo', appId, appSecret, {
    token,
    deviceId,
    channelId: String(channelId),
  });

  const httpsSd = data.streams?.find(
    (s) => Number(s.streamId) === 1 && String(s.hls).startsWith('https://'),
  );
  const httpsHd = data.streams?.find(
    (s) => Number(s.streamId) === 0 && String(s.hls).startsWith('https://'),
  );

  return {
    job: data.job, // period / beginTime / endTime / status
    httpsSdHls: httpsSd?.hls,
    httpsHdHls: httpsHd?.hls,
    sdStatus: httpsSd?.status,
    coverUrl: httpsSd?.coverUrl,
    liveToken: httpsSd?.liveToken || data.streams?.[0]?.liveToken,
  };
}

家人默认下发 HTTPS + 辅码流;仅业主后台预览可切主码流。coverUrl 适合做加载占位,别当权限凭证。HTTPS 地址常见带 ?proto=https,端口与 HTTP 不同——按返回原样使用,不要手改。

踩坑 6:以为「没有 HTTPS」。其实要用 getLiveStreamInfo(或控制台)才能一次看到四类地址。bindDeviceLive 只回 HTTP,是文档写明的行为,不是接口坏了。

3.8 Step 6:把 always 改成「看园档期」——4G 省钱第一刀

新建直播后,job 常见是 period: "always"。庭院远程看不是安防值班墙,不必 24 小时出流。早饲、午巡、晚归三档就够大多数家庭:

// server/set-yard-plan.js
import { callOpenApi } from './imou-client.js';
import { getAccessToken } from './access-token.js';

/** 每天三档看园;按真实作息改 rules */
export async function setYardWatchPlan(appId, appSecret, liveToken) {
  const token = await getAccessToken(appId, appSecret);
  await callOpenApi('batchModifyLivePlan', appId, appSecret, {
    token,
    liveToken,
    rules: [
      {
        period: 'monday,tuesday,wednesday,thursday,friday,saturday,sunday',
        beginTime: '06:00',
        endTime: '08:00',
      },
      {
        period: 'monday,tuesday,wednesday,thursday,friday,saturday,sunday',
        beginTime: '11:30',
        endTime: '13:00',
      },
      {
        period: 'monday,tuesday,wednesday,thursday,friday,saturday,sunday',
        beginTime: '17:00',
        endTime: '20:00',
      },
    ],
  });
}

/** 回城过年 / 整院腾空:一键关 */
export async function pauseYardLive(appId, appSecret, liveToken) {
  const token = await getAccessToken(appId, appSecret);
  await callOpenApi('modifyLivePlanStatus', appId, appSecret, {
    token,
    liveToken,
    status: 'off', // on | off
  });
}

export async function resumeYardLive(appId, appSecret, liveToken) {
  const token = await getAccessToken(appId, appSecret);
  await callOpenApi('modifyLivePlanStatus', appId, appSecret, {
    token,
    liveToken,
    status: 'on',
  });
}

batchModifyLivePlan:对单个 liveToken 写多条规则;period 用英文星期,逗号分隔;beginTime / endTime 格式是 HH:mm;重叠时段平台会合并。

若只需「每天同一时段」,用单计划接口即可:

await callOpenApi('modifyLivePlan', appId, appSecret, {
  token: await getAccessToken(appId, appSecret),
  liveToken,
  period: 'everyday', // always | once | everyday
  beginTime: '06:00:00', // everyday 用 HH:mm:ss;平台秒位常归一为 00
  endTime: '20:00:00',
});

注意:batchModifyLivePlan 的时间格式是 HH:mmmodifyLivePlaneverydayHH:mm:ss——别混用once 则是 yyyy-MM-dd HH:mm:ss,且 endTime 必须大于当前时间。

踩坑 7:只在业务层把「看院子」按钮夜间灰掉,平台侧仍是 always。历史 m3u8 若曾泄露,夜间仍可能被拉流——平台计划 + 业务鉴权要双开。4G 卡账单异常高,第一怀疑对象永远是「主码流 + always」。

3.9 Step 7:业务签发 + H5 播放(代码优先)

// server/routes/yard-live.js
import { getYardStreams } from '../get-yard-streams.js';

const TICKET_TTL_SEC = 180; // 业务层:3 分钟内开播

export async function issueYardLive(req, res) {
  const { yardId } = req.body;
  const bind = await db.yard.find(yardId);
  if (!bind) return res.status(404).json({ error: 'yard_not_bound' });

  // 示例:仅登录家人可看;按你们家庭组 / 租户规则改
  if (!req.session?.userId) {
    return res.status(401).json({ error: 'login_required' });
  }
  if (!canViewYard(req.session.userId, yardId)) {
    return res.status(403).json({ error: 'not_this_yard' });
  }

  const streams = await getYardStreams(process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET, {
    deviceId: bind.device_id,
    channelId: bind.channel_id,
  });

  if (String(streams.sdStatus) === '10') {
    return res.status(503).json({ error: 'live_paused' });
  }
  if (!streams.httpsSdHls) {
    return res.status(503).json({ error: 'stream_unavailable' });
  }

  return res.json({
    hls: streams.httpsSdHls,
    coverUrl: streams.coverUrl,
    expireIn: TICKET_TTL_SEC,
  });
}
<!-- public/yard-live.html -->
<video id="v" controls playsinline webkit-playsinline
       poster="" style="width:100%;background:#111"></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.7/dist/hls.min.js"></script>
<script>
async function playYard(yardId) {
  const r = await fetch('/api/yard/live', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include',
    body: JSON.stringify({ yardId }),
  });
  if (!r.ok) {
    alert('当前不在看园时段,或设备暂不可看');
    return;
  }
  const { hls, coverUrl } = await r.json();
  const video = document.getElementById('v');
  if (coverUrl) video.poster = coverUrl;

  if (video.canPlayType('application/vnd.apple.mpegurl')) {
    // iOS Safari / 部分微信环境:原生 HLS
    video.src = hls;
  } else if (window.Hls && Hls.isSupported()) {
    const player = new Hls({ enableWorker: true, lowLatencyMode: false });
    player.loadSource(hls);
    player.attachMedia(video);
  } else {
    alert('当前环境不支持 HLS 播放');
  }
}
playYard(new URLSearchParams(location.search).get('yardId'));
</script>

公众号入口建议:菜单 / 模板消息 → 业务域名 H5,不要把 m3u8 写进图文正文。小程序侧可用 web-view 打开同一 H5。平台侧 HLS URL 本身可能具备较长可访问周期,业务必须用登录态 + 档期 + 审计补齐「人」的维度

sequenceDiagram
  participant Family as 家人H5
  participant Biz as 农事后端
  participant Imou as OpenAPI
  participant Cam as 户外4G机

  Family->>Biz: POST /api/yard/live
  Biz->>Biz: 登录 + 院落 ACL + 档期
  Biz->>Imou: getLiveStreamInfo
  Imou->>Cam: 代理出流(若计划开启)
  Imou-->>Biz: HTTPS 辅码流 m3u8
  Biz-->>Family: 短效播放信息
  Family->>Family: hls.js / 原生 HLS

3.10 一次联调检查清单(可当发版门禁)

[ ] 签名标准用例算出 xjhCQBoJ9hRDsCjyDcHjtDNzRZ3ZJezcawsfWeiaoxU=
[ ] accessToken 成功,且服务端缓存,未下发前端
[ ] 物联网卡已开通,设备指示灯可联网
[ ] 设备在开发者资产池,且 deviceStatus / channelStatus 均为 online
[ ] bindDeviceLive(streamId=1) 返回 liveToken 已入库
[ ] getLiveStreamInfo 能拿到 https + streamId=1
[ ] batchModifyLivePlan 后,非档期拉流失败或 status=10
[ ] modifyLivePlanStatus=off 后签发接口返回 live_paused
[ ] 微信内 HTTPS 可播;HTTP 地址已从产品路径剔除
[ ] 家人账号只能看到自己的 yardId,串院测试被 403

四、边界、4G 流量与生产注意

4.1 能力边界:别用云直播扛「对讲赶鸡」

需求云直播 HLS 路径建议改道
家人看一眼院门 / 鸡圈
远程喊话、对讲赶牲口轻应用 / 移动 OpenSDK
云台转到死角播放器侧要 getKitToken 且 type 覆盖 60不要指望 HLS <video> 出云台
翻昨天傍晚「谁开的门」可用 createDeviceRecordHls / createDeviceFlvLive type=playback与实时看园分开入口
超低延迟业主墙评估 createDeviceFlvLive type=realTime返回 flv(标清)/ flvHD(高清)

createDeviceFlvLive 实时预览只需:

const data = await callOpenApi('createDeviceFlvLive', appId, appSecret, {
  token: await getAccessToken(appId, appSecret),
  deviceId,
  channelId: '0',
  type: 'realTime', // 默认值;playback 才需要 beginTime / endTime / recordType
});
// data.flv    标清
// data.flvHD  高清

回放跨度最大 24 小时,可跨天;recordTypelocalRecordcloudRecord。查询已创建的实时 FLV 用 queryDeviceFlvLive,删除用 deleteDeviceFlvLive(只支持删实时直播)。

业主 Web 墙若走轻应用:getKitTokentoken 必须是管理员 At_type 用字符串(1 仅预览,0 全权限,6 云台);kitToken 约 2 小时有效,文档建议服务端缓存约 1 小时。播放器初始化的 type: 1 是直播、type: 2 是回放——和 getKitToken 的 type 不是同一套枚举

4.2 4G 流量与性能:先砍码流,再砍时段,再砍并发

  1. 默认辅码流:家人侧永不主动发主码流;业主后台单独开 HD。
  2. 计划外不签发:业务层档期校验与平台 job 双校验,避免无效 getLiveStreamInfo 风暴。
  3. token 缓存accessToken 约 3 天;按剩余秒数刷新即可。
  4. 播放器:HLS 本身有分片延迟,「看得见院子」可接受;不要为了抠 1~2 秒延迟上整套原生 SDK。
  5. 封面刷新coverUpdate 单位为秒;status=1(封面异常)时先查镜头遮挡和设备在线,而不是重绑直播。
  6. 休眠:户外 4G 机 deviceStatus=sleep 时,先别创建直播。省电策略和「卡没流量」在现场长得很像,要用列表字段分开。

4.3 安全与合规(院子画面也敏感)

  • 院门可能拍到邻居、门牌、孩子进出——详情页禁止裸奔 m3u8
  • 整院腾空、转租、撤机:先 modifyLivePlanStatus=off,再视需要走 unbindLive(按 liveToken 删除直播地址),并清空业务绑定。设备解绑也会自动删直播地址。
  • 多院落:一院一 liveToken 映射;不要用一个全局 HLS 给所有亲戚复用。
  • 应急:家族群反馈「链接外传」→ 立刻 off → 轮换业务签发策略 → 通知家人重新进入 H5。

4.4 真实踩坑速记(可当值班手册)

现象根因处理
标准案例 sign 对不上抄了旧 MD5改现行 HMAC-SHA256
接口 SN1002time 用了毫秒,或时钟漂移 > 5 分钟秒级时间戳 + NTP
接口 SN10055 分钟内 nonce 复用每次请求 randomUUID()
TK1002accessToken 失效刷新并更新缓存
列表 0 台,App 却能看绑在私人号下绑到开发者主账号
微信里黑屏用了 HTTP m3u8getLiveStreamInfo 挑 https
创建成功但 getLive 空未先 bind / 已解绑重建直播并更新 liveToken
夜间仍被拉流平台计划仍为 alwaysbatchModifyLivePlan + 业务拒签
月流量炸了默认主码流 + always全量切 streamId=1,锁三档看园
设备 online 仍无画通道 sleep / 遮罩 on同时看 channelStatuscameraStatus

五、小结与延伸

庭院 / 小农场「人在城里、院在乡下」的远程看,可以收成一条短链路:

  1. 现场:户外 4G 机插卡上电,绑进开发者资产,而不是侄子的私人 App。
  2. 鉴权壳:现行 HMAC-SHA256 + accessToken(只活在服务端)。
  3. 出流bindDeviceLive(streamId=1)getLiveStreamInfo 取 HTTPS 辅码流。
  4. 省钱闸门batchModifyLivePlan 锁看园档期,modifyLivePlanStatus 做腾空熔断。
  5. 触达:业务 H5 + hls.js;公众号走菜单进页,不把 m3u8 写进图文。
  6. 边界:云直播解决「可看」;对讲、云台、深度 App 能力另选轻应用或 OpenSDK。

延伸阅读(平台现行文档栏目,查阅时跳过「旧版本协议」):

若你正在把乡下院子的户外机做成可嵌入的家人页,可在 开放平台 注册开发者应用,用控制台领取的接入与媒体资源做联调。平台以视频技术和安全为核心,开放偏低代码的开发组件,适合第三方厂商和个人开发者把「设备画面」较快接到自己的 H5 / 小程序业务里——庭院远程看这条线,从 4G 上电到 bindDeviceLive 跑通辅码流 HTTPS,通常就是最短闭环。