周末档期厨房爆单,带宽账单也爆了——不是直播难,是主码流 + always 计划在烧钱

一、推流机还没到,客人已经在问「厨房在哪」

周五傍晚,城郊农家乐老板把土鸡预售发到社群:有人问「灶台干不干净」,有人要「看活鸡再下单」。运营同学第一反应是租推流机、开直播账号、连麦、美颜------报价单甩过来,周末档期已经来不及。

其实厨房里已经挂着一台联网摄像头:App 里能看,客人却进不去。

真正卡住的不是「会不会做直播间」,而是------怎么用最低成本,把「设备预览」变成「客人可点开的 HLS 页」,并且只在出餐档期出流。

下文按现行云直播 OpenAPI,把这条最低成本路径一次走通。


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

2.1 带货直播 ≠ 监控预览,但厨房场景刚好落在中间

农家乐 / 家庭厨房带货,常见诉求只有三句:

  1. 看得见:客人打开 H5 / 公众号菜单,能看到灶台、案板、出餐口。
  2. 控得住:非营业时段、歇业日不要一直拉流。
  3. 花得起:周末高峰几十上百人同时看,不能按「每人一路高清主码流」去烧带宽。

对照乐橙开放平台常见路径(官网能力分层,延迟为经验量级):

路径 协议 延迟量级 对讲 / 推流 对接成本 适不适合厨房带货
云直播 HLS(亦可按需 FLV/RTMP) 约数秒~十余秒 不做推流主播,做「看得见」 极低(小时级) 首选最低成本路径
轻应用 JS HLS/RTSP 等 约 2~3s 可扩展权限与 UI 低(约 7 天级) 要回放/细权限时再上
移动端 OpenSDK 私有协议 约 1~2s 对讲、深度控制 App 深度定制再用

电商平台(抖音 / 淘宝等)的「带货直播间」是另一条产品线:要主播、美颜、货架、成交链路。本文解决的是 「透明厨房 / 明厨亮灶式可看」 ------用设备出流证明卫生与真实,引流到你自己的点餐页或私域下单。两条链路可以并存,不要用推流机方案去扛「只是让客人瞄一眼灶台」这件事。

2.2 最低成本心智:辅码流 + 计划时段 + 业务签发

text 复制代码
┌────────────────────────────────────────────────────────────┐
│ 厨房 IPC → 绑定到开发者资产(控制台 / App / bindDevice)     │
└────────────────────────────┬───────────────────────────────┘
                             │
                             ▼
┌────────────────────────────────────────────────────────────┐
│ 云直播 OpenAPI                                              │
│  accessToken → bindDeviceLive(streamId=1)                   │
│  getLiveStreamInfo → 取 HTTPS 辅码流 m3u8                   │
│  batchModifyLivePlan → 仅午餐/晚餐/周末档期                 │
│  modifyLivePlanStatus → 歇业日 off                          │
└────────────────────────────┬───────────────────────────────┘
                             │ 后端短效签发,禁止前端藏密钥
                             ▼
┌────────────────────────────────────────────────────────────┐
│ 业务 H5(公众号菜单 / 小程序 web-view / 点餐页内嵌)         │
│  video + hls.js;封面用 coverUrl 占位                       │
└────────────────────────────────────────────────────────────┘

一句话:平台负责「设备变成可播的 HLS」;你的后端负责「谁、何时、哪路厨房能拿地址」;码流与计划负责「账单不炸」。

2.3 成本从哪省下来

决策 省什么 风险若做错
streamId=1 辅码流默认 并发观看时的媒体带宽 默认 0 主码流,周末高峰账单陡增
batchModifyLivePlan 只开档期 夜间 / 歇业无效出流 默认 always,摄像头 24h 可被拉
业务层签发 HTTPS,不写死在商品详情 泄露面可控、可审计 m3u8 写进详情页 = 永久公开链接
云直播 HLS,不上 OpenSDK 研发人天 为「瞄一眼灶台」上 App SDK,周期与包体积双亏

下面进入可跑通的实操:先贴代码,再讲为什么。


三、从鉴权到 H5 可播的完整实操

3.0 统一请求壳(签名 + accessToken)

所有现行 HTTP OpenAPI 均为:

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

Body 固定包含 system(含 sign)、idparams。签名按「开发规范」:

text 复制代码
签名原始串 = time:{time},nonce:{nonce},appSecret:{appSecret}
sign = MD5(原始串).hex().toLowerCase()   // UTF-8

标准自测用例(文档给出):

time:1706511734,nonce:f5a1ae2d-c09c-4d39-a744-83a5c2c653c2,appSecret:test123456789test123456789

sign = fd37b62889e4757c58b8f3bf05fb9976

本地算出一样,说明签名实现正确。

js 复制代码
// server/imou/openapi.js
import crypto from 'crypto';
import { randomUUID } from 'crypto';

const OPENAPI_BASE = 'https://openapi.lechange.cn/openapi';
const APP_ID = process.env.IMOU_APP_ID;
const APP_SECRET = process.env.IMOU_APP_SECRET;

function calcSign(time, nonce) {
  const raw = `time:${time},nonce:${nonce},appSecret:${APP_SECRET}`;
  return crypto.createHash('md5').update(raw, 'utf8').digest('hex');
}

export async function openApi(method, params = {}) {
  const time = Math.floor(Date.now() / 1000); // UTC 秒;与真实时间误差须 < 5 分钟
  const nonce = randomUUID(); // 5 分钟内不可重复,否则 SN1005
  const body = {
    system: {
      ver: '1.0',
      appId: APP_ID,
      sign: calcSign(time, nonce),
      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?.result)}`);
  }
  return json.result.data;
}

/** 管理员 token 约 3 天有效;TK1002 时刷新。勿每次业务请求都打 accessToken。 */
let cachedToken = null;
let tokenExpireAt = 0;

export async function getAccessToken() {
  const now = Date.now();
  if (cachedToken && now < tokenExpireAt - 60_000) return cachedToken;

  const data = await openApi('accessToken', {});
  cachedToken = data.accessToken;
  // expireTime 为剩余有效秒数
  tokenExpireAt = now + Number(data.expireTime || 0) * 1000;
  return cachedToken;
}

踩坑 1time 用了毫秒,或服务器时钟漂移超过 5 分钟------一律签名失败。厨房档期服务器建议开 NTP。

踩坑 2 :把 appSecret 写进 H5「图方便」------等于把名下所有设备钥匙交给浏览器。密钥只活在服务端。


3.1 步骤一:为厨房通道创建直播(只做一次)

前置:设备已在开发者资产池且在线(控制台可见,或经 listDeviceDetailsByPage 查到 online)。未进资产池时,云直播接口再正确也出不了有效流。

js 复制代码
// server/imou/createKitchenLive.js
import { openApi, getAccessToken } from './openapi.js';

/**
 * streamId: 0 高清主码流;1 标清辅码流
 * 厨房带货默认 1:多人同时看时带宽友好
 */
export async function createKitchenLive({ deviceId, channelId = '0' }) {
  const token = await getAccessToken();
  const data = await openApi('bindDeviceLive', {
    token,
    deviceId,
    channelId: String(channelId),
    streamId: 1,
    liveMode: 'proxy', // 可不填,或固定 proxy
  });

  // liveToken:后续改计划 / 启停的唯一句柄,务必入库
  return {
    liveToken: data.liveToken,
    liveStatus: data.liveStatus, // 1 开启;2 暂停
    httpHls: data.streams?.[0]?.hls, // 文档:本接口通常只回所选码流的 HTTP
    deviceId: data.deviceId,
    channelId: data.channelId,
  };
}

文档要点(直接决定你后面会不会踩坑):

  1. 创建时后台会准备 主/辅码流 × HTTP/HTTPS 共四类地址bindDeviceLive 往往只返回你选中码流的 HTTP
  2. 设备解绑会自动删除直播地址------换机要重新创建并更新业务映射。
  3. 直播地址一旦对外公开,他人可直接看画面------厨房场景也要当机密句柄,不要印在桌贴二维码的明文里。

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


3.2 步骤二:getLiveStreamInfo 取齐 HTTPS 与状态

js 复制代码
// server/imou/getKitchenStreams.js
import { openApi, getAccessToken } from './openapi.js';

export async function getKitchenStreams({ deviceId, channelId = '0' }) {
  const token = await getAccessToken();
  const data = await openApi('getLiveStreamInfo', {
    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,
    // status: "0" 直播中;"10" 暂停;其它见文档(封面异常、源异常等)
    sdStatus: httpsSd?.status,
    coverUrl: httpsSd?.coverUrl,
    liveToken: httpsSd?.liveToken || data.streams?.[0]?.liveToken,
  };
}

客人默认下发 HTTPS + 辅码流 ;仅店主后台预览可切主码流。coverUrl 适合做加载占位,别当权限凭证。

踩坑 4 :以为「没有 HTTPS」。其实要用 getLiveStreamInfo(或开发者控制台直播服务页)才能一次看到四类地址。HTTPS 地址常见带 ?proto=https,端口与 HTTP 不同------按返回原样使用,不要手改。


3.3 步骤三:把 always 改成「出餐档期」------省钱第一刀

默认计划往往是 period: "always"。农家乐周末午晚高峰才需要出流:

js 复制代码
// server/imou/setKitchenPlan.js
import { openApi, getAccessToken } from './openapi.js';

/** 周末全天档 + 工作日午晚两档;按店铺真实档期改 rules */
export async function setKitchenRushPlan(liveToken) {
  const token = await getAccessToken();
  await openApi('batchModifyLivePlan', {
    token,
    liveToken,
    rules: [
      {
        period: 'saturday,sunday',
        beginTime: '10:00',
        endTime: '21:00',
      },
      {
        period: 'monday,tuesday,wednesday,thursday,friday',
        beginTime: '11:00',
        endTime: '14:00',
      },
      {
        period: 'monday,tuesday,wednesday,thursday,friday',
        beginTime: '17:00',
        endTime: '21:00',
      },
    ],
  });
}

/** 歇业 / 台风停业:一键关 */
export async function pauseKitchenLive(liveToken) {
  const token = await getAccessToken();
  await openApi('modifyLivePlanStatus', {
    token,
    liveToken,
    status: 'off', // on | off
  });
}

export async function resumeKitchenLive(liveToken) {
  const token = await getAccessToken();
  await openApi('modifyLivePlanStatus', {
    token,
    liveToken,
    status: 'on',
  });
}

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

js 复制代码
await openApi('modifyLivePlan', {
  token: await getAccessToken(),
  liveToken,
  period: 'everyday', // always | once | everyday
  beginTime: '11:00:00', // everyday 用 HH:mm:ss;平台秒位常归一为 00
  endTime: '21:00:00',
});

注意:batchModifyLivePlan 的时间格式是 HH:mmmodifyLivePlaneverydayHH:mm:ss------别混用。计划重叠时平台会合并。

踩坑 5 :只在业务层把「看厨房」按钮夜间灰掉,平台侧仍是 always。历史 m3u8 若曾泄露,夜间仍可能被拉流------平台计划 + 业务鉴权要双开


3.4 步骤四:业务表与短效签发(H5 永远不直连密钥)

sql 复制代码
-- 门店厨房 ↔ 设备映射;不落库永久对外 HLS
CREATE TABLE kitchen_live_bind (
  shop_id     VARCHAR(64) PRIMARY KEY,
  device_id   VARCHAR(64) NOT NULL,
  channel_id  VARCHAR(8)  NOT NULL DEFAULT '0',
  live_token  VARCHAR(64) NOT NULL,
  stream_pref TINYINT     NOT NULL DEFAULT 1, -- 1 辅码默认
  updated_at  DATETIME    NOT NULL
);

CREATE TABLE kitchen_live_audit (
  id         BIGINT PRIMARY KEY AUTO_INCREMENT,
  shop_id    VARCHAR(64) NOT NULL,
  viewer_id  VARCHAR(64),          -- 可空:未登录游客策略按产品定
  action     VARCHAR(32) NOT NULL, -- issue / denied / paused
  ip         VARCHAR(64),
  created_at DATETIME NOT NULL
);
js 复制代码
// server/routes/kitchenLive.js
import { getKitchenStreams } from '../imou/getKitchenStreams.js';

const TICKET_TTL_SEC = 180; // 业务层:3 分钟内开播;可按风控加严

export async function issueKitchenLive(req, res) {
  const { shopId } = req.body;
  const bind = await db.kitchen.find(shopId);
  if (!bind) return res.status(404).json({ error: 'shop_not_bound' });

  // 示例:仅登录会员或持有订单号可看;按你们私域规则改
  if (!req.session?.userId && !req.body.orderToken) {
    await db.audit.insert({ shopId, action: 'denied', ... });
    return res.status(401).json({ error: 'login_or_order_required' });
  }

  if (!isWithinBizHours(shopId, new Date())) {
    return res.status(403).json({ error: 'outside_rush_hours' });
  }

  const streams = await getKitchenStreams({
    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' });
  }

  await db.audit.insert({
    shopId,
    viewerId: req.session?.userId || 'guest',
    action: 'issue',
  });

  return res.json({
    hls: streams.httpsSdHls,
    coverUrl: streams.coverUrl,
    expireIn: TICKET_TTL_SEC,
    // 可选:弱网提示文案;需要高清时再单独开店主鉴权接口发 httpsHdHls
  });
}

说明:平台侧 HLS URL 本身可能具备较长可访问周期,业务必须用登录 / 订单态 + 档期 + 审计补齐「人」的维度 。过期后拒绝续签,歇业配合 modifyLivePlanStatus=off 做熔断。


3.5 步骤五:H5 播放(代码优先)

html 复制代码
<!-- public/kitchen-live.html -->
<video id="v" controls playsinline webkit-playsinline
       poster="" style="width:100%;background:#000"></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.7/dist/hls.min.js"></script>
<script>
async function playKitchen(shopId) {
  const r = await fetch('/api/kitchen/live', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include',
    body: JSON.stringify({ shopId }),
  });
  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 播放');
  }
}
playKitchen(new URLSearchParams(location.search).get('shopId'));
</script>

公众号入口建议:菜单 / 模板消息 → 业务域名 H5 ,不要把 m3u8 写进图文正文。小程序侧可用 web-view 打开同一 H5;若走原生直播组件,另按「小程序对接指南」选型,本文最低成本主线仍是云直播 HLS。

流程图(端到端):

sequenceDiagram participant Guest as 客人H5 participant Biz as 业务后端 participant Imou as 乐橙OpenAPI participant Cam as 厨房设备 Guest->>Biz: POST /api/kitchen/live Biz->>Biz: 登录/订单 + 档期校验 Biz->>Imou: getLiveStreamInfo Imou->>Cam: 代理出流(若计划开启) Imou-->>Biz: HTTPS 辅码流 m3u8 Biz-->>Guest: 短效播放信息 Guest->>Guest: hls.js / 原生 HLS 播放

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

text 复制代码
[ ] 签名标准用例算出 fd37b62889e4757c58b8f3bf05fb9976
[ ] accessToken 成功,且服务端缓存,未下发前端
[ ] 设备在资产池且 online
[ ] bindDeviceLive(streamId=1) 返回 liveToken 已入库
[ ] getLiveStreamInfo 能拿到 https + streamId=1
[ ] batchModifyLivePlan 后,非档期拉流失败或 status=暂停类
[ ] modifyLivePlanStatus=off 后 H5 签发接口返回 live_paused / 不可用
[ ] 微信内 HTTPS 可播;HTTP 地址已从产品路径剔除
[ ] 审计表能查到 issue / denied

四、边界、性能与生产注意

4.1 能力边界:别用云直播扛「主播连麦带货」

需求 云直播最低成本路径 建议改道
透明厨房、证明卫生 ✅ HLS 嵌入 ---
低延迟对讲、远程喊话 OpenSDK / 轻应用 / 云广播等
电商平台官方直播间成交 ❌(产品形态不同) 平台直播工具 + 本页「透明厨房」作辅证
录像片段回看举证 可用 createDeviceRecordHls 与实时直播分开产品入口
超低延迟 PC 墙 可评估 FLV 接口族 复杂度高于 HLS,非「最低成本」首选

4.2 性能与账单:并发时先砍码流,再砍时段

  1. 默认辅码流:客人侧永不主动发主码流;店长后台单独开 HD。
  2. 计划外不签发 :业务层 isWithinBizHours 与平台 job 双校验,避免无效 getLiveStreamInfo 风暴。
  3. token 缓存accessToken 约 3 天生命周期;文档说明接近过期再取可并存新旧 token------按剩余秒数刷新即可,勿每秒刷。
  4. 播放器:HLS 本身有分片延迟,厨房「看得见」场景可接受;不要为了抠 1~2 秒延迟上整套原生 SDK。
  5. 封面刷新coverUpdate 单位为秒,封面异常(status=1)时优先检查设备在线与镜头遮挡,而不是重绑直播。

4.3 安全与合规(厨房画面也敏感)

  • 后厨可能拍到员工面部、电话号码贴纸、监控屏二次成像------详情页禁止裸奔 m3u8
  • 歇业、转让、撤机:先 modifyLivePlanStatus=off,再视需要 unbindLive,并清空业务绑定。
  • 多门店:一店一 liveToken 映射;不要用一个全局 HLS 给所有店铺复用。
  • 应急:社群反馈「链接外传」→ 立刻 off → 轮换业务签发策略 → 通知持有订单用户重新进入 H5。

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

现象 根因 处理
接口 SN1005 5 分钟内 nonce 复用 每次请求 randomUUID()
TK1002 accessToken 失效 刷新并更新缓存
微信里黑屏 用了 HTTP m3u8 改 HTTPS(getLiveStreamInfo
创建成功但 getLive 空 未先 bindDeviceLive / 已解绑 重建直播并更新 liveToken
夜间仍被拉流 平台计划仍为 always batchModifyLivePlan + 业务拒签
账单异常高 默认主码流 + 高并发 全量切 streamId=1,高峰再观察

五、小结与延伸

农家乐 / 家庭厨房「看得见的带货」,最低成本路径可以收成一条短链路:

  1. 鉴权壳accessToken + 开发规范签名(服务端)。
  2. 出流bindDeviceLive(streamId=1)getLiveStreamInfo 取 HTTPS 辅码流。
  3. 省钱闸门batchModifyLivePlan 锁档期,modifyLivePlanStatus 做歇业熔断。
  4. 触达 :业务 H5 + hls.js;公众号走菜单进页,不把 m3u8 写进图文。
  5. 边界:云直播解决「可看」;对讲、电商官方直播间、深度 App 能力另选组件。

延伸阅读(平台现行文档栏目,便于继续深挖):

  • 设备直播说明:bindDeviceLive / getLiveStreamInfo / 计划与启停全家桶
  • 开发规范:域名、system.sign、返回码约定
  • 轻应用 / 小程序对接指南:当 H5 不够、需要更低代码 UI 或小程序原生播放时
  • 明厨亮灶 / 食品安全相关解决方案页:把「透明厨房」从单店 H5 扩到连锁巡检时的产品视野

若你正在把后厨摄像头做成可嵌入的业务页,可在 乐橙开放平台 注册开发者应用,用控制台领取的接入与媒体资源做联调。平台以视频技术与安全能力为核心,并提供云直播、轻应用等偏低代码的组件,适合第三方与个人开发者把「设备画面」较快接到自己的 H5 / 小程序业务里------厨房档期场景,从 bindDeviceLive 跑通辅码流 HTTPS,通常就是最短闭环。

相关推荐
老孙讲技术1 小时前
校园开放日前夜才说要「全校透明」?我用轻应用把 9 路教室预览和回放嵌进了校园后台
后端·物联网
Python私教1 小时前
AI Agent 可观测性实战:从 correlationId 到失败时间线
人工智能·后端
用户7791666846542 小时前
给 Agent 开权限:身份不能进模型的上下文
后端
掘金一周2 小时前
掘友们,你们现在下班了都玩啥游戏?| 沸点周刊 8.13
前端·人工智能·后端
shengjk12 小时前
从 StarRocks Stream Load 400 报错,彻底搞懂 HTTP 100-continue
后端
程序员cxuan3 小时前
Anthropic:session 之间可以相互通信了
人工智能·后端·程序员
卷无止境3 小时前
FastAPI 前端托管全攻略:从静态文件到大型全栈项目架构
后端·python
webmote333 小时前
用 NVIDIA Nemotron 3 Super + .NET 构建有记忆的多轮对话
后端·算法
神奇小汤圆3 小时前
JUC三大常用工具类CountDownLatch、CyclicBarrier、Semaphore
后端