家长端看幼儿园 live:云直播 HLS 嵌入公众号 / H5 的合规要点

一、链贴进公众号的第二天

周一早上,某民办园公众号推送了一篇「透明课堂」:文末直接贴了一段教室 m3u8。家长刷朋友圈时能播,转发到隔壁小区家长群也能播------甚至没入园的路人点开,也能看见小班吃加餐。

园长以为「公开透明」;法务下午就来电话:未成年人画面不能当永久公开链接。

真正炸锅的不是延迟,也不是卡顿,而是------直播地址一旦写死在图文里,权限就失控了。

下文用现行云直播能力,把「家长能看」做成「只在在园时段、只对自己孩子班、只经登录签发」的可上线方案。


二、为什么「能播」不等于「能上线」

2.1 幼教 live 的三个硬约束

家长端看班,技术上往往被简化成「拿个 HLS 塞进 video」。上线前至少要同时过三关:

  1. 未成年人保护:画面属敏感个人信息场景,需监护人同意、目的限定、最小必要;不能 24 小时裸奔。
  2. 触达形态 :园所习惯用公众号触达家长。图文正文几乎不能跑复杂脚本;可靠路径是 菜单 / 模板消息 → 业务 H5,在 H5 里播。
  3. 出流成本:云直播对接成本极低(小时级~天级),协议以 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 计算 signaccessToken 由服务端缓存(通常约 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 的时分秒格式与 batchModifyLivePlanHH: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>

公众号侧配置清单(无厂商名,纯微信侧):

  1. 公众号后台 → 设置与开发 → 公众号设置 → 功能设置:业务域名填你的 H5 域名(需 ICP 与校验文件)。
  2. JS 接口安全域名:若要用微信 JS-SDK(分享禁用、关闭右上角转发等),一并配置。
  3. 自定义菜单 :跳转 https://your-domain/parent-live.html?classroomId=xxxclassroomId 可在登录后由服务端解析,避免泄露内部编号)。
  4. 不要在图文正文里贴 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 / getLiveStreamInfo
  • modifyLivePlan · batchModifyLivePlan · modifyLivePlanStatus
  • 云直播与轻应用、OpenSDK 的能力对比表

若你正在做幼教、校园或同类「监护人远程看护」H5,优先选 视频能力开放、带云直播与直播计划 的平台:用低代码式出流把教室画面嵌进公众号菜单页,把合规做成默认路径,而不是上线后再补丁。注册开发者后通常可领取基础接入与媒体带宽试用资源,先拿一间教室跑通「计划 + 签发 + 家长 H5」闭环,再复制到全园。


本文接口速查(现行云直播)

方法 用途
bindDeviceLive 创建设备源直播,拿到 liveToken 与初始 HLS
getLiveStreamInfo 按设备通道查询 HTTP/HTTPS、主/辅码流与计划
modifyLivePlan 单计划:always / once / everyday
batchModifyLivePlan 多周期规则(工作日分段等)
modifyLivePlanStatus 计划总开关 on/off
queryLiveStatus / liveList / unbindLive 状态查询、列表、删除(按需)
相关推荐
xingren2 小时前
CT(DCM)外轮廓实时识别 → 逐层拼3D → 导出STL到Blender
后端
foggyprojects2 小时前
同一条销售查询,两个用户为什么看到不同结果?
后端
梅头脑2 小时前
Java镜像1.2GB→200MB,K8s Pod OOMKilled 137半夜报警——容器化的真实踩坑记录
后端
2601_956743682 小时前
智能硬件互联产业数字化:上海物联网定制开发合作评估思路
物联网·智能硬件·开发经验·上海
netCode2 小时前
IDEA使用 Alibaba Cloud Toolkit
后端
foggyprojects2 小时前
业务要给发票明细加 8 个筛选条件,我没有再写 8 个查询接口
后端
爱勇宝3 小时前
人到了一定年纪,才会看懂这些人性真相
前端·后端
阿标在干嘛3 小时前
从RESTful到GraphQL:政策快报平台接口设计的演进
后端·restful·graphql
智购科技自动售卖机厂家3 小时前
从STM32到RK3588——自动售货机嵌入式主控方案的架构演进~YH
stm32·嵌入式硬件·物联网·架构·lua·零售·symfony