一、链贴进公众号的第二天
周一早上,某民办园公众号推送了一篇「透明课堂」:文末直接贴了一段教室 m3u8。家长刷朋友圈时能播,转发到隔壁小区家长群也能播------甚至没入园的路人点开,也能看见小班吃加餐。
园长以为「公开透明」;法务下午就来电话:未成年人画面不能当永久公开链接。
真正炸锅的不是延迟,也不是卡顿,而是------直播地址一旦写死在图文里,权限就失控了。
下文用现行云直播能力,把「家长能看」做成「只在在园时段、只对自己孩子班、只经登录签发」的可上线方案。
二、为什么「能播」不等于「能上线」
2.1 幼教 live 的三个硬约束
家长端看班,技术上往往被简化成「拿个 HLS 塞进 video」。上线前至少要同时过三关:
- 未成年人保护:画面属敏感个人信息场景,需监护人同意、目的限定、最小必要;不能 24 小时裸奔。
- 触达形态 :园所习惯用公众号触达家长。图文正文几乎不能跑复杂脚本;可靠路径是 菜单 / 模板消息 → 业务 H5,在 H5 里播。
- 出流成本:云直播对接成本极低(小时级~天级),协议以 HLS 为主,延迟通常高于私有协议预览,但足够「看一眼在干什么」;对讲等能力不在云直播路径上,别混需求。
对照主流视频开放平台的能力分层(查阅文档时避开「旧版本协议」):
| 路径 | 协议 | 延迟量级 | 对讲 | 对接成本 | 适不适合家长看班 |
|---|---|---|---|---|---|
| 云直播 | HLS | 约 8~10s | 不支持 | 极低 | 适合:公众号 / H5 嵌入 |
| 轻应用组件 | HLS/RTSP | 约 2~3s | 可支持 | 低 | 要回放/对讲时再上 |
| 移动端 OpenSDK | 私有协议 | 约 1~2s | 支持 | 高 | App 深度定制 |
本文主线:云直播 HLS + 业务 H5 + 公众号入口,把合规写进签发与计划,而不是写进推文 HTML。
2.2 正确心智:平台管「流」,你管「人」
text
┌──────────────────────────────────────────────────────────┐
│ 园所侧:教室 IPC 绑定到开发者资产 │
│ bindDeviceLive 创建直播 → liveToken + 初始 HLS │
│ batchModifyLivePlan 限定工作日在园时段 │
│ modifyLivePlanStatus 寒暑假 / 敏感日一键 off │
└────────────────────────────┬─────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 业务后端(合规核心) │
│ 1. 家长登录(手机号 / 园务系统 OAuth) │
│ 2. 家长 ↔ 班级 ↔ deviceId/channelId 白名单 │
│ 3. 监护人知情同意记录 │
│ 4. 短效「播放票据」:校验通过后才返回 HTTPS m3u8 │
│ 5. 审计:谁、何时、看了哪路 │
└────────────────────────────┬─────────────────────────────┘
│ 仅返回短时可播地址
▼
┌──────────────────────────────────────────────────────────┐
│ 公众号菜单「看班」→ 业务域名 H5 │
│ video + hls.js(Android / 内置浏览器) │
│ iOS Safari / 微信可走原生 HLS │
└──────────────────────────────────────────────────────────┘
开放平台保证「设备能出 HLS」;「这个家长此刻能不能看这一路」必须由你的业务裁决。 漏了这一层,就是合规事故,不是播放器 bug。
2.3 为什么不能把 m3u8 写进推文
| 做法 | 结果 |
|---|---|
图文末尾硬贴 http(s)://...m3u8 |
任何人转发即可看;无法按家长撤销;难审计 |
| 控制台「永久计划 always」+ 公开页 | 夜间、假期仍可被拉流 |
仅用 bindDeviceLive 返回的 HTTP 地址塞进微信 |
混合内容 / 安全域名问题频发 |
| 菜单进 H5 + 登录签发 HTTPS 流 + 直播计划 | 可关停、可鉴权、可留痕 |
三、从创建直播到家长能看的完整实操
3.0 前置:鉴权与调用约定
所有 OpenAPI 走开发者 appId + appSecret 计算 sign,accessToken 由服务端缓存(通常约 3 天有效,以现行文档为准)。密钥与 token 禁止下发到家长 H5。
下面用统一封装示意(Node.js);签名算法请严格按你所用平台「开发规范」现行章节实现,勿抄旧协议栏目。
js
// server/openapi.js ------ 示意封装
import crypto from 'crypto';
import { randomUUID } from 'crypto';
const OPENAPI_BASE = process.env.OPENAPI_BASE; // 控制台文档中的 openapi 根地址
const APP_ID = process.env.APP_ID;
const APP_SECRET = process.env.APP_SECRET;
function sign({ time, nonce }) {
// 以现行「开发规范」为准;此处为常见 MD5 拼接形态示意
const raw = `time:${time},nonce:${nonce},appSecret:${APP_SECRET}`;
return crypto.createHash('md5').update(raw).digest('hex');
}
export async function openApi(method, params) {
const time = Math.floor(Date.now() / 1000);
const nonce = randomUUID();
const body = {
system: { ver: '1.0', appId: APP_ID, sign: sign({ 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;
}
踩坑 1 :把 appSecret 写进前端「图方便」------等于把园所全部设备交给浏览器。密钥只活在服务端。
3.1 步骤一:为教室通道创建直播(只做一次)
先贴代码:
js
// server/live/createClassroomLive.js
import { openApi } from '../openapi.js';
/**
* streamId: 0 高清主码流;1 标清辅码流
* 家长多并发看班:优先 streamId=1,省带宽、省钱
*/
export async function createClassroomLive({ accessToken, deviceId, channelId = '0' }) {
const data = await openApi('bindDeviceLive', {
token: accessToken,
deviceId,
channelId: String(channelId),
streamId: 1,
liveMode: 'proxy', // 可不填或固定 proxy
});
// data.liveToken 是后续改计划 / 启停的句柄,务必入库
// data.streams[0].hls 往往是当前码流的 HTTP 地址(文档说明:本接口只返回所选码流 HTTP)
return {
liveToken: data.liveToken,
liveStatus: data.liveStatus, // 1 开启;2 暂停
httpHls: data.streams?.[0]?.hls,
deviceId: data.deviceId,
channelId: data.channelId,
};
}
说明(先码后文):
- 创建时后台会准备主/辅码流 × HTTP/HTTPS 共四类地址;本接口通常只回你选中码流的 HTTP。
- 设备解绑会自动删直播地址------园所换机要重新
bindDeviceLive并更新业务映射。 - 重要:文档明确提醒------直播地址对外公开后,他人可直接看画面;幼教场景默认当作机密句柄,不要进推文、不要进二维码海报。
踩坑 2 :创建完就兴冲冲把返回的 http://...m3u8 塞进微信。微信内置浏览器对明文 HTTP 媒体极不友好,且混合内容会静默失败。下一步必须拿 HTTPS。
3.2 步骤二:用 getLiveStreamInfo 取齐 HTTPS 地址与状态
js
// server/live/getStreams.js
import { openApi } from '../openapi.js';
export async function getClassroomStreams({ accessToken, deviceId, channelId = '0' }) {
const data = await openApi('getLiveStreamInfo', {
token: accessToken,
deviceId,
channelId: String(channelId),
});
// streams 中通常包含:辅码/主码 × HTTP/HTTPS
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,
liveToken: httpsSd?.liveToken || data.streams?.[0]?.liveToken,
};
}
家长端默认下发 HTTPS + 辅码流 ;弱网再提示「标清」。封面 coverUrl 可做加载占位,但别拿封面当权限凭证。
踩坑 3 :只调了 bindDeviceLive 就觉得「没有 HTTPS」。其实要用 getLiveStreamInfo(或控制台直播服务页)才能一次看到四类地址。
3.3 步骤三:把「永久 always」改成「在园时段」------合规第一刀
幼儿园最危险的默认值是 period: "always"。改成工作日在园时段:
js
// server/live/setSchoolHoursPlan.js
import { openApi } from '../openapi.js';
/** 工作日 08:00--17:30;周末关闭(不配规则即不播) */
export async function setSchoolHoursPlan({ accessToken, liveToken }) {
await openApi('batchModifyLivePlan', {
token: accessToken,
liveToken,
rules: [
{
period: 'monday,tuesday,wednesday,thursday,friday',
beginTime: '08:00',
endTime: '17:30',
},
],
});
}
/** 寒暑假 / 传染病停课:一键关计划 */
export async function pauseLivePlan({ accessToken, liveToken }) {
await openApi('modifyLivePlanStatus', {
token: accessToken,
liveToken,
status: 'off', // on | off
});
}
export async function resumeLivePlan({ accessToken, liveToken }) {
await openApi('modifyLivePlanStatus', {
token: accessToken,
liveToken,
status: 'on',
});
}
若只需「每天同一时段」,也可用 modifyLivePlan:
js
await openApi('modifyLivePlan', {
token: accessToken,
liveToken,
period: 'everyday',
beginTime: '08:00:00',
endTime: '17:30:00',
});
注意文档细节:everyday 的时分秒格式与 batchModifyLivePlan 的 HH:mm 略有不同;平台保存时秒位常归一为 00。以联调返回为准,写进你们自己的配置中心,不要硬编码魔法字符串散落各处。
计划时间重叠时,batchModifyLivePlan 会由平台合并------多园所多班次用规则数组即可。
踩坑 4 :只改了业务层「菜单夜间灰掉」,平台侧仍是 always。攻击者若曾拿到历史 m3u8,夜间仍可能拉流。平台计划 + 业务鉴权要双开。
3.4 步骤四:业务权限表------家长只能看自己班
sql
-- 最小必要:不存 HLS 明文;只存设备映射与同意状态
CREATE TABLE parent_classroom_acl (
parent_id BIGINT NOT NULL,
classroom_id VARCHAR(64) NOT NULL,
device_id VARCHAR(64) NOT NULL,
channel_id VARCHAR(8) NOT NULL DEFAULT '0',
live_token VARCHAR(64) NOT NULL,
consent_at DATETIME NOT NULL, -- 监护人同意时间
consent_ver VARCHAR(32) NOT NULL, -- 同意书版本号
PRIMARY KEY (parent_id, classroom_id)
);
CREATE TABLE live_play_audit (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
parent_id BIGINT NOT NULL,
device_id VARCHAR(64) NOT NULL,
action VARCHAR(32) NOT NULL, -- live_issue / live_denied
ip VARCHAR(64),
ua VARCHAR(255),
created_at DATETIME NOT NULL
);
签发接口(家长 H5 只拿短效播放信息):
js
// server/routes/parentLive.js
import { getClassroomStreams } from '../live/getStreams.js';
const TICKET_TTL_SEC = 120; // 业务层短效:2 分钟内开播;可按风控加严
export async function issueParentLive(req, res) {
const parentId = req.session.parentId;
const { classroomId } = req.body;
if (!parentId) return res.status(401).json({ error: 'login_required' });
const acl = await db.acl.find({ parentId, classroomId });
if (!acl?.consent_at) {
await db.audit.insert({ parentId, deviceId: '-', action: 'live_denied', ... });
return res.status(403).json({ error: 'no_consent_or_acl' });
}
// 可选:业务层再卡一次「是否在园时段」,与平台计划双保险
if (!isWithinSchoolHours(new Date())) {
return res.status(403).json({ error: 'outside_school_hours' });
}
const streams = await getClassroomStreams({
accessToken: await getCachedAccessToken(),
deviceId: acl.device_id,
channelId: acl.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({
parentId,
deviceId: acl.device_id,
action: 'live_issue',
ip: req.ip,
ua: req.headers['user-agent'],
created_at: new Date(),
});
// 不要把 liveToken、deviceId 无关字段甩给前端
return res.json({
hls: streams.httpsSdHls,
expiresIn: TICKET_TTL_SEC,
tip: '仅供监护人本人查看,请勿录屏传播',
});
}
说明:平台侧 HLS URL 本身可能较长生命周期,业务必须用登录态 + ACL + 计划 + 审计 补齐「人」的维度。若产品要求更强失效,可在过期后拒绝续签,并配合 modifyLivePlanStatus 做应急熔断。
3.5 步骤五:家长 H5 播放(代码优先)
html
<!-- public/parent-live.html ------ 挂在已备案业务域名,配进公众号 JS 安全域名 / 业务域名 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>看班</title>
<style>
body { margin: 0; background: #0f1419; color: #fff; font-family: system-ui, sans-serif; }
.wrap { padding: 12px; }
video { width: 100%; max-height: 70vh; background: #000; }
.tip { font-size: 12px; opacity: .75; margin-top: 8px; line-height: 1.5; }
button { margin-top: 12px; padding: 10px 16px; }
</style>
</head>
<body>
<div class="wrap">
<video id="player" controls playsinline webkit-playsinline poster=""></video>
<p class="tip">画面仅限监护人查看 · 请勿转发链接或录屏传播 · 非在园时段不可用</p>
<button id="btn">开始看班</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.7/dist/hls.min.js"></script>
<script>
const video = document.getElementById('player');
const classroomId = new URLSearchParams(location.search).get('classroomId');
async function issue() {
const r = await fetch('/api/parent/live/issue', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ classroomId }),
});
if (!r.ok) {
const err = await r.json().catch(() => ({}));
alert({
login_required: '请先登录家长账号',
no_consent_or_acl: '未开通本班看班权限或尚未完成知情同意',
outside_school_hours: '当前不在看班时段',
live_paused: '直播已暂停,请联系园所',
stream_unavailable: '暂时无法获取画面,请稍后重试',
}[err.error] || '暂时无法播放');
return null;
}
return r.json();
}
function playHls(url) {
if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url;
return video.play();
}
if (window.Hls && Hls.isSupported()) {
const hls = new Hls({
enableWorker: true,
lowLatencyMode: false, // 云直播非低延迟场景,别硬开 LL-HLS
});
hls.loadSource(url);
hls.attachMedia(video);
hls.on(Hls.Events.ERROR, (_, data) => {
if (data.fatal) console.error('hls fatal', data);
});
return video.play();
}
alert('当前环境不支持 HLS 播放');
}
document.getElementById('btn').onclick = async () => {
const ticket = await issue();
if (!ticket?.hls) return;
await playHls(ticket.hls);
};
</script>
</body>
</html>
公众号侧配置清单(无厂商名,纯微信侧):
- 公众号后台 → 设置与开发 → 公众号设置 → 功能设置:业务域名填你的 H5 域名(需 ICP 与校验文件)。
- JS 接口安全域名:若要用微信 JS-SDK(分享禁用、关闭右上角转发等),一并配置。
- 自定义菜单 :跳转
https://your-domain/parent-live.html?classroomId=xxx(classroomId可在登录后由服务端解析,避免泄露内部编号)。 - 不要在图文正文里贴 m3u8;最多放「点击菜单看班」引导。
建议在 H5 里关掉容易扩散的分享入口(需 JS-SDK):
js
// 在 wx.ready 后
wx.hideMenuItems({
menuList: [
'menuItem:share:appMessage',
'menuItem:share:timeline',
'menuItem:copyUrl',
'menuItem:openWithQQBrowser',
'menuItem:openWithSafari',
],
});
踩坑 5 :安卓微信能播、iOS 不能(或反过来)。iOS 多走原生 HLS;安卓依赖 hls.js。两套都要测。另:playsinline 必须加,否则 iOS 全屏打断「看一眼就走」的体验。
踩坑 6:云直播延迟体感约数秒到十余秒,家长会以为「卡死」。开播前用文案说明「非实时对讲、存在秒级延迟」,避免客服被刷屏。
3.6 端到端联调清单(建议按序打勾)
text
[ ] 设备在线,通道可预览(控制台或官方 App)
[ ] bindDeviceLive 成功,liveToken 入库
[ ] getLiveStreamInfo 能取到 https 辅码流
[ ] batchModifyLivePlan 设为工作日在园时段
[ ] 非时段拉流失败或 status=暂停
[ ] 家长 A 只能签发班级 A;跨班 403
[ ] 未同意监护人协议 → 403
[ ] 公众号菜单打开 H5,安卓 / iOS 均可播
[ ] 图文中不存在任何 m3u8 明文
[ ] 审计表有 live_issue / live_denied 记录
[ ] 寒暑假 modifyLivePlanStatus=off 验证
流程图(签发时刻):
text
家长点菜单
→ H5 带登录态请求 /api/parent/live/issue
→ ACL + 同意书 + 在园时段?
→ 否:403 + audit denied
→ 是:getLiveStreamInfo → 取 HTTPS 辅码
→ 返回 { hls, expiresIn }
→ hls.js / 原生 video 播放
四、边界、性能与生产注意
4.1 合规边界(写进产品说明书)
- 目的限定:看班 ≠ 长期云存公开回放。若要回放,单独做权限与留存周期,勿复用「永久直播 URL」。
- 知情同意 :入园协议 / 电子同意书版本号进库;撤回同意即删 ACL,必要时
modifyLivePlanStatus=off单班熔断。 - 最小化出镜:镜头避免卫生间、更衣区;云直播页不做人脸识别、不做公开评论弹幕。
- 传播控制:禁用分享菜单、水印(家长手机号后四位)、客服话术统一「禁止录屏外传」。
- 日志与响应 :保留签发审计;发生外泄时能定位班级与时间窗,并一键
off。
法律层面请法务按《个人信息保护法》《未成年人个人信息网络保护规定》出园所制度;工程侧至少落到:同意、时段、ACL、审计、熔断 五件套。
4.2 性能与费用
- 码流 :家长并发高时统一辅码流(
streamId=1);园长巡检可用主码流另开管理端。 - 并发:多家长看同一路会叠加并发与带宽;按「班额 × 同时在线率」估媒体带宽,预留寒假突发(开放日)。
- 流量不足 :部分平台
liveStatus会出现资源不足态;监控资源余量,避免开学第一天集体黑屏。 - 封面刷新 :
coverUpdate影响封面图频率,与出流费用无关,但别当心跳用。
4.3 与轻应用 / 小程序的边界
| 需求 | 建议 |
|---|---|
| 只看直播、公众号触达 | 本文:云直播 HLS + H5 |
| 要回放、要对讲 | 轻应用 / OpenSDK / 小程序组件路径 |
| 主体没有 live-player 资质 | 勿硬上小程序原生直播组件;H5 云直播或官方插件方案 |
云直播 不支持语音对讲------产品评审时把「家长喊话进教室」挡在范围外,免得研发空做。
4.4 生产环境注意
- accessToken 进程内缓存 + 过期前刷新;全部 OpenAPI 走服务端。
- HLS 域名变化以
getLiveStreamInfo为准,前端不写死历史 URL。 - HTTPS 全站;公众号业务域名校验文件勿删。
- 监控:
issue接口 4xx/5xx、HLS fatal、计划关闭后的误开播。 - 应急:班级投诉外泄 → 立刻
modifyLivePlanStatus=off→ 轮换业务票据策略 → 通知家长重置登录态。
五、小结与延伸
家长看幼儿园 live,难点从来不在「HLS 会不会播」,而在 把公开摄像头变成可控服务 :平台侧用 bindDeviceLive 出流、用 getLiveStreamInfo 取 HTTPS、用 batchModifyLivePlan / modifyLivePlanStatus 锁时段;业务侧用登录、班级 ACL、监护人同意和审计补齐「人」的维度;触达侧用公众号菜单进 H5,而不是把 m3u8 写进推文。
延伸阅读(自行在所用平台现行文档检索,避开旧版本协议栏目):
- 设备直播说明(创建 / 计划 / 启停总览)
bindDeviceLive/getLiveStreamInfomodifyLivePlan·batchModifyLivePlan·modifyLivePlanStatus- 云直播与轻应用、OpenSDK 的能力对比表
若你正在做幼教、校园或同类「监护人远程看护」H5,优先选 视频能力开放、带云直播与直播计划 的平台:用低代码式出流把教室画面嵌进公众号菜单页,把合规做成默认路径,而不是上线后再补丁。注册开发者后通常可领取基础接入与媒体带宽试用资源,先拿一间教室跑通「计划 + 签发 + 家长 H5」闭环,再复制到全园。
本文接口速查(现行云直播)
| 方法 | 用途 |
|---|---|
bindDeviceLive |
创建设备源直播,拿到 liveToken 与初始 HLS |
getLiveStreamInfo |
按设备通道查询 HTTP/HTTPS、主/辅码流与计划 |
modifyLivePlan |
单计划:always / once / everyday |
batchModifyLivePlan |
多周期规则(工作日分段等) |
modifyLivePlanStatus |
计划总开关 on/off |
queryLiveStatus / liveList / unbindLive |
状态查询、列表、删除(按需) |