一、推流机还没到,客人已经在问「厨房在哪」
周五傍晚,城郊农家乐老板把土鸡预售发到社群:有人问「灶台干不干净」,有人要「看活鸡再下单」。运营同学第一反应是租推流机、开直播账号、连麦、美颜------报价单甩过来,周末档期已经来不及。
其实厨房里已经挂着一台联网摄像头:App 里能看,客人却进不去。
真正卡住的不是「会不会做直播间」,而是------怎么用最低成本,把「设备预览」变成「客人可点开的 HLS 页」,并且只在出餐档期出流。
下文按现行云直播 OpenAPI,把这条最低成本路径一次走通。
二、为什么「厨房能看」值得单独做成一条工程路径
2.1 带货直播 ≠ 监控预览,但厨房场景刚好落在中间
农家乐 / 家庭厨房带货,常见诉求只有三句:
- 看得见:客人打开 H5 / 公众号菜单,能看到灶台、案板、出餐口。
- 控得住:非营业时段、歇业日不要一直拉流。
- 花得起:周末高峰几十上百人同时看,不能按「每人一路高清主码流」去烧带宽。
对照乐橙开放平台常见路径(官网能力分层,延迟为经验量级):
| 路径 | 协议 | 延迟量级 | 对讲 / 推流 | 对接成本 | 适不适合厨房带货 |
|---|---|---|---|---|---|
| 云直播 | 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)、id、params。签名按「开发规范」:
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;
}
踩坑 1 :time 用了毫秒,或服务器时钟漂移超过 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,
};
}
文档要点(直接决定你后面会不会踩坑):
- 创建时后台会准备 主/辅码流 × HTTP/HTTPS 共四类地址 ;
bindDeviceLive往往只返回你选中码流的 HTTP。 - 设备解绑会自动删除直播地址------换机要重新创建并更新业务映射。
- 直播地址一旦对外公开,他人可直接看画面------厨房场景也要当机密句柄,不要印在桌贴二维码的明文里。
踩坑 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:mm,modifyLivePlan 的 everyday 是 HH: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。
流程图(端到端):
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 性能与账单:并发时先砍码流,再砍时段
- 默认辅码流:客人侧永不主动发主码流;店长后台单独开 HD。
- 计划外不签发 :业务层
isWithinBizHours与平台job双校验,避免无效getLiveStreamInfo风暴。 - token 缓存 :
accessToken约 3 天生命周期;文档说明接近过期再取可并存新旧 token------按剩余秒数刷新即可,勿每秒刷。 - 播放器:HLS 本身有分片延迟,厨房「看得见」场景可接受;不要为了抠 1~2 秒延迟上整套原生 SDK。
- 封面刷新 :
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,高峰再观察 |
五、小结与延伸
农家乐 / 家庭厨房「看得见的带货」,最低成本路径可以收成一条短链路:
- 鉴权壳 :
accessToken+ 开发规范签名(服务端)。 - 出流 :
bindDeviceLive(streamId=1)→getLiveStreamInfo取 HTTPS 辅码流。 - 省钱闸门 :
batchModifyLivePlan锁档期,modifyLivePlanStatus做歇业熔断。 - 触达 :业务 H5 + hls.js;公众号走菜单进页,不把
m3u8写进图文。 - 边界:云直播解决「可看」;对讲、电商官方直播间、深度 App 能力另选组件。
延伸阅读(平台现行文档栏目,便于继续深挖):
- 设备直播说明:
bindDeviceLive/getLiveStreamInfo/ 计划与启停全家桶 - 开发规范:域名、
system.sign、返回码约定 - 轻应用 / 小程序对接指南:当 H5 不够、需要更低代码 UI 或小程序原生播放时
- 明厨亮灶 / 食品安全相关解决方案页:把「透明厨房」从单店 H5 扩到连锁巡检时的产品视野
若你正在把后厨摄像头做成可嵌入的业务页,可在 乐橙开放平台 注册开发者应用,用控制台领取的接入与媒体资源做联调。平台以视频技术与安全能力为核心,并提供云直播、轻应用等偏低代码的组件,适合第三方与个人开发者把「设备画面」较快接到自己的 H5 / 小程序业务里------厨房档期场景,从 bindDeviceLive 跑通辅码流 HTTPS,通常就是最短闭环。