Name Last Update
..
src Loading commit data...
.env.example Loading commit data...
.gitignore Loading commit data...
README.md Loading commit data...
docker-compose.yml Loading commit data...
package.json Loading commit data...
srs.conf Loading commit data...

README.md

小程序 RTMP ↔ LiveKit 网关(试验)

试验功能,默认关闭,勿用于生产。 小程序音视频经 RTMP 转码中转,端到端延迟预计为秒级(通常 1~3 秒,弱网更高),明显差于 PC / H5 / App 的 WebRTC 直连;回声消除、资源成本、真机兼容性均未经验收。

1. 为什么需要网关

微信小程序不能运行 livekit-client(无 WebRTC),原生 live-pusher 只能推 RTMP,live-player 只能拉 RTMP / FLV。网关在不更换 LiveKit 的前提下把小程序接入同一个房间:

小程序 live-pusher ──RTMP──▶ LiveKit Ingress ──WebRTC──▶ LiveKit 房间 ◀──WebRTC── PC / H5 / App
                                                              │
                         每个远端参与者 1 路 Track Composite Egress
                                                              ▼
小程序 live-player ◀──RTMP── SRS(推流、播放均回调网关鉴权)◀──RTMP──┘
  • 小程序自己的 Ingress 参与者 identity 即后端 Token 内的 userId(不加后缀),与 PC / App 同账号进房时沿用 LiveKit 同 identity 互踢;后端 webhook 的 joined / left 照常生效,无需修改 Java 代码。
  • 每个远端单独一路流,从不混入小程序自己的声音;视频屏幕共享优先、摄像头回退;Egress、Agent、SIP 等非业务参与者不转出。
  • 群通话一个远端一路 live-player,单会话默认最多 4 路,超出部分如实返回 overflow。

2. 目录

文件 说明
src/server.mjs 入口:小程序会话 API(8090)+ SRS 回调(8091,仅回环)
src/session-manager.mjs 会话、租约、Ingress / Egress 生命周期、SRS 鉴权、残留回收
src/jwt.mjs 校验后端签发的房间 Token(HS256)、签发管理 Token
src/livekit-api.mjs LiveKit Twirp API 最小客户端(无第三方依赖)
src/config.mjs 环境变量配置,MP_GATEWAY_ENABLED=true 才允许启动
docker-compose.yml 独立试验环境:Redis + LiveKit + Ingress + Egress + SRS + 网关
srs.conf SRS 配置,HTTP 回调鉴权
.env.example 凭据与地址占位,复制为 .env 使用

网关进程只使用 Node.js 22 内置模块(http、crypto、fetch),无需安装 npm 依赖;运行时依赖 LiveKit Server、Ingress、Egress 与 SRS,由同目录 docker-compose.yml 一并启动。

3. 本机回环联调

与上级目录 ../docker-compose.yml(LiveKit-only PoC)二选一,两者都占用 7880 等端口。

cd script/livekit-poc/mp-gateway
docker compose -f ../docker-compose.yml down   # 停掉 LiveKit-only PoC
cp .env.example .env                           # 填写与后端 yudao.im.rtc.api-key / api-secret 一致的凭据,MP_GATEWAY_ENABLED=true
docker compose up -d
docker compose logs -f mp-gateway              # 看到「小程序网关已启动(试验功能)」
curl -s http://127.0.0.1:8090/healthz          # {"status":"ok"}
  • 全部服务使用 network_mode: host。macOS 需 Docker Desktop 4.34+,并在 Settings → Resources → Network 勾选「Enable host networking」,否则容器能启动但端口不通。
  • 后端 yudao.im.rtc.livekit-url 仍为 ws://127.0.0.1:7880;webhook 默认回调 http://127.0.0.1:48080/admin-api/im/livekit/webhook。
  • 本机端口:LiveKit 7880 / 7881 / 7882udp,Ingress RTMP 1935、WHIP 18080、relay 19090、UDP 7885,Egress 健康检查 18082,SRS 1936,Redis 16379,网关 8090 / 8091。LiveKit HTTP、SRS、Redis、网关默认只监听回环;Ingress RTMP 与 LiveKit 媒体端口会监听所有网卡(推流靠一次性 stream key 鉴权),在不可信局域网中请配合防火墙。

3.1 用 FFmpeg 模拟小程序

  1. 用 H5 或 PC 账号 A 发起通话,账号 B(模拟小程序端)取得同一房间的 Token(例如在 H5 发起 / 接听接口响应中取得 token,只在本机终端使用,勿写日志或提交)。
  2. 创建会话:
   curl -s -X POST -H "Authorization: Bearer $ROOM_TOKEN" http://127.0.0.1:8090/mp-rtc/sessions
  1. 用返回的 pushUrl 推测试流(模拟 live-pusher):
   ffmpeg -re -f lavfi -i testsrc=size=640x360:rate=15 -f lavfi -i sine=frequency=440 \
     -c:v libx264 -preset veryfast -tune zerolatency -g 15 -c:a aac -ar 48000 -f flv "$PUSH_URL"

账号 A 的页面应出现 B 的测试画面与提示音;会话查询 publishing 变为 true。

  1. 每 2 秒查询会话(同时续租),待远端 A 的 state 为 ready 后,用 playUrl 拉流(模拟 live-player):
   curl -s -H "Authorization: Bearer $SESSION_TOKEN" http://127.0.0.1:8090/mp-rtc/sessions/$SESSION_ID
   ffplay -fflags nobuffer -flags low_delay "$PLAY_URL"
  1. 反向校验:去掉 playUrl 的 token 参数或改错一位,SRS 应拒绝播放;停止查询超过 leaseTtlMs,或 DELETE 会话后,Ingress、Egress 应被释放(docker compose logs mp-gateway 有「会话已关闭」)。

4. 配置

变量 默认值 说明
MP_GATEWAY_ENABLED 无 必须为 true 才启动
MP_GATEWAY_API_HOST / _PORT 127.0.0.1 / 8090 小程序 API;公网部署放在 HTTPS 反向代理之后
MP_GATEWAY_HOOK_HOST / _PORT 127.0.0.1 / 8091 SRS 回调,只允许 SRS 访问
MP_GATEWAY_LIVEKIT_HTTP_URL http://127.0.0.1:7880 LiveKit API 地址
MP_GATEWAY_LIVEKIT_API_KEY / _SECRET 必填 与后端一致;secret 至少 32 位
MP_GATEWAY_SRS_PUBLISH_BASE_URL rtmp://127.0.0.1:1936/yudao-mp-gw Egress 推到 SRS 的内部地址;app 必须为网关专用的 yudao-mp-gw 或 yudao-mp-gw-<后缀>,否则拒绝启动
MP_GATEWAY_SRS_PLAY_BASE_URL 同上 小程序拉流的外部地址,app 需与推流一致
MP_GATEWAY_POLL_INTERVAL_MS 2000 建议轮询间隔,也是网关同步房间的间隔
MP_GATEWAY_LEASE_TTL_MS 20000 租约,至少 3 倍轮询间隔;超时未查询即释放
MP_GATEWAY_MAX_SESSION_MS 2 小时 单会话最长存活,另受房间 Token exp 限制
MP_GATEWAY_MAX_SESSIONS 4 全局会话数(每个 1 路 Ingress 转码)
MP_GATEWAY_MAX_REMOTES_PER_SESSION 4 单会话最多拉取的远端
MP_GATEWAY_MAX_EGRESS 8 全局 Egress 路数
MP_GATEWAY_EGRESS_RETRY_MS 10000 单路 Egress 失败后的冷却
MP_GATEWAY_EGRESS_PUBLISH_TIMEOUT_MS 20000 Egress 启动后等待 SRS on_publish 的最长时间,超时停止并按 failed 冷却重试
MP_GATEWAY_CLEANUP_RETRY_MS 5000 启动回收失败后的重试间隔
MP_GATEWAY_RELEASE_RETRY_MS 5000 Ingress / Egress 释放失败后的重试间隔;确认释放前继续占用配额
MP_GATEWAY_REQUEST_TIMEOUT_MS 10000 调用 LiveKit API 的超时
MP_GATEWAY_CLOCK_SKEW_SEC 30 校验房间 Token nbf / exp 允许的时钟偏差
MP_GATEWAY_EGRESS_WIDTH / HEIGHT / FRAMERATE / VIDEO_KBPS / AUDIO_KBPS / KEYFRAME_SEC 640 / 360 / 15 / 600 / 64 / 1 Egress 编码(H264 Baseline + AAC)

上表是网关进程直接读取的环境变量,自行运行 node src/server.mjs 时全部按此配置。使用本目录 docker-compose.yml 时,网关容器的环境由 compose 组装,.env 中的变量并非全部原样转发:

  • compose 固定、.env 不生效:MP_GATEWAY_API_PORT=8090、MP_GATEWAY_HOOK_HOST=127.0.0.1、MP_GATEWAY_HOOK_PORT=8091、MP_GATEWAY_LIVEKIT_HTTP_URL=http://127.0.0.1:7880,与 srs.conf 及 compose 内各服务端口一一对应。
  • 改名转发:LiveKit 凭据读取 .env 的 LIVEKIT_API_KEY / LIVEKIT_API_SECRET(同时供 LiveKit、Ingress、Egress 使用),写 MP_GATEWAY_LIVEKIT_API_KEY / _SECRET 不生效。
  • 由 compose 变量拼出:MP_GATEWAY_SRS_PUBLISH_BASE_URL 固定为 rtmp://127.0.0.1:1936/<app>,MP_GATEWAY_SRS_PLAY_BASE_URL 为 rtmp://<MP_SRS_PLAY_PUBLIC_HOST>:1936/<app>;<app> 取 MP_GATEWAY_SRS_APP(默认 yudao-mp-gw),MP_SRS_PLAY_PUBLIC_HOST 默认 127.0.0.1。
  • 原样转发:MP_GATEWAY_ENABLED(默认 false)、MP_GATEWAY_API_HOST(默认 127.0.0.1),以及从 MP_GATEWAY_POLL_INTERVAL_MS 到 Egress 编码的其余可调参数,未配置即用上表默认值(见 .env.example)。

5. 会话 API

所有响应 Cache-Control: no-store,错误体为 { "code": "...", "message": "..." }。不支持 CORS(小程序 uni.request 不受 CORS 限制)。

5.1 创建会话 POST /mp-rtc/sessions

  • 请求头 Authorization: Bearer <后端签发的房间 Token>;请求体可为空(上限 4KB)。不得携带后端登录 accessToken。
  • 网关严格校验:HS256 签名、iss = apiKey、exp / nbf(允许 30 秒时钟偏差)、sub 为数字 userId、video.roomJoin / canPublish / canSubscribe 为 true、非 hidden / recorder。房间与身份只取自 Token。
  • 同一用户在同一房间已有未释放的会话或推流资源(含释放中、创建中)时返回 409 session_busy,不会替换正在推流的会话:替换已进房的 Ingress 会触发 participant_left,后端会据此结束私聊通话。原会话 DELETE 或租约到期、资源确认释放后才能再次创建;若该房间 + 用户存在创建结果未知的占位,则需重启网关才能再次创建(见第 7 节)。网关不会返回已有会话的凭证。

201 响应:

{
  "sessionId": "32 位十六进制",
  "sessionToken": "会话凭证,仅本会话后续请求使用",
  "userId": 1,
  "pushUrl": "rtmp://host:1935/x/<stream key>",
  "pollIntervalMs": 2000,
  "leaseTtlMs": 20000,
  "expiresAt": 1760000000000
}

错误:401 Token 无效 / 过期,403 授权不足,429 会话数已满,502 LiveKit / Ingress 不可用或未配置 ingress.rtmp_base_url,409 同一用户已有会话(session_busy)或会话在创建过程中被关闭(session_closed),503 网关正在退出(shutting_down)或启动回收尚未完成(cleanup_pending)。创建过程中客户端断开,网关会立即释放刚创建的资源。

5.2 查询并续租 GET /mp-rtc/sessions/{sessionId}

请求头 Authorization: Bearer <sessionToken>;会话不存在、已结束或凭证错误统一返回 404。

{
  "sessionId": "...",
  "status": "active",
  "publishing": true,
  "remotes": [
    { "userId": 2, "state": "ready", "ready": true, "playUrl": "rtmp://host:1936/yudao-mp-gw/<stream>?token=<拉流凭证>", "hasAudio": true, "hasVideo": true, "videoVisible": true }
  ],
  "error": null,
  "leaseTtlMs": 20000,
  "pollIntervalMs": 2000,
  "expiresAt": 1760000000000
}
  • publishing:小程序推流已作为 LiveKit 参与者进房(Ingress 参与者已有轨道)。
  • remotes[].state:
    • ready:SRS 已收到该远端的流,playUrl 可播放;只有此状态下 playUrl 非空。
    • starting:Egress 启动中。
    • waiting:远端已进房,暂无可转出的音视频轨道。
    • overflow:超出单会话路数上限,不会转出。
    • capacity:全局 Egress 已满,空出后自动重试。
    • failed:Egress 启动失败、超过 MP_GATEWAY_EGRESS_PUBLISH_TIMEOUT_MS 仍未推到 SRS,或中途结束时冷却后自动重试;创建结果未知(见「7. 生命周期与资源」)时不再为该远端新建转流,直到网关重启。具体原因(egress_failed / egress_no_media / egress_ended / 创建结果未知)只记录在网关日志。
  • 远端媒体组合变化(如开始 / 结束屏幕共享)时会换新的流名与 playUrl,客户端需按最新值重新播放。
  • error:最近一次同步房间失败为 livekit_unavailable,否则为 null。

5.3 结束会话 DELETE /mp-rtc/sessions/{sessionId}

请求头同上,成功 204,释放 Ingress 与全部 Egress。

5.4 健康检查 GET /healthz

启动回收完成后 200 {"status":"ok"};完成前 503 {"status":"cleanup_pending"}。

6. SRS 鉴权与凭证

  • 每路 Egress 使用新的随机流名、推流密钥(secret)、拉流凭证(token)。推流密钥与会话凭证只保存摘要;拉流凭证需要在会话查询中重新下发 playUrl,因此明文保存在网关进程内存中,会话关闭或远端流移除时随之删除。所有凭证都不落盘、不写日志。
  • on_publish:只允许当前存活会话中对应流名 + 正确 secret 推流;on_play:只允许正确 token 播放;网关不可用时 SRS 拒绝。
  • 日志只记录会话编号、房间、userId、资源编号,不记录房间 Token、会话凭证、stream key、推拉流 URL;LiveKit API 出错时只记录方法、HTTP 状态与 Twirp 错误码,不记录上游返回的错误原文。

7. 生命周期与资源

  • 会话超过 leaseTtlMs 未查询、到达 Token exp 或 MP_GATEWAY_MAX_SESSION_MS、房间被销毁(曾进房之后)时自动释放。
  • 会话关闭或远端流移除时立即撤销推拉流授权与会话查询;对应的 Ingress / Egress 删除失败时保留在内存待释放列表中,按 MP_GATEWAY_RELEASE_RETRY_MS 重试(同时最多 4 个重试请求),确认释放前继续计入会话数与 Egress 路数上限,同一用户也不能新建会话。
  • 异步创建 Ingress / Egress 期间会话被关闭或远端已变化时,结果返回后立即转入待释放;创建中的资源同样计入上限。
  • SIGINT / SIGTERM 时拒绝新会话,关闭全部会话,等待进行中的创建返回并确认全部资源释放后以 0 退出;15 秒内未能全部释放则以非零状态退出,剩余资源由下次启动回收或按 7.2 手动处理。
  • 创建结果未知:创建 Ingress / Egress 时超时、网络错误、HTTP 5xx(含反向代理 502 / 504),或成功响应缺少资源编号,LiveKit 侧都可能已建好资源而网关拿不到编号。网关按 fail-closed 处理:该次创建作为占位永久计入会话数 / Egress 路数上限,同一房间 + 用户不能再新建会话(409 session_busy),发现结果未知后不再为对应远端新建转流,避免不断叠加未知资源:若该远端换轨后的新转流在旧请求结果未知前已经启动,则继续按其真实状态显示和运行,否则显示 failed;旧的未知占位仍计入配额直到重启。占位只能重启网关后由启动回收(Ingress 按名称前缀、Egress 按专用 app)清除;存在占位时网关退出以非零状态结束。出现日志「创建结果未知」后请择机重启网关。注意 LiveKit 在 Egress 容量不足时也可能返回 5xx,同样会被保守占位、需要重启恢复,这是试验阶段的取舍;4xx 等明确拒绝的失败按未创建处理,沿用冷却重试。
  • 单实例限制:会话保存在内存,多实例共用同一 LiveKit 会互相清理,暂不支持。

7.1 启动回收与专用 SRS app

  • LiveKit 返回的 EgressInfo 会把 RTMP 地址最后一段(流名 + 查询参数)脱敏为 {首 3 位...末 3 位},例如 rtmp://127.0.0.1:1936/yudao-mp-gw/{yud...xyz},只有 scheme / host / port / app 可信。因此网关用专用 app 标识归属:yudao-mp-gw(或 yudao-mp-gw-<后缀>)只能给网关使用。
  • 网关启动后先回收残留:删除名称前缀为 yudao-mp-gw- 的 Ingress;停止输出地址为「MP_GATEWAY_SRS_PUBLISH_BASE_URL/单段流名」且流名为完整网关流名或 {yud...xxx} 脱敏形式的活跃 Egress。其它 app(如 live)下的 Egress 不受影响。
  • 回收任一步失败都会按 MP_GATEWAY_CLEANUP_RETRY_MS 后台重试;完成前 /healthz 与创建会话返回 503 cleanup_pending,避免旧残留与新会话并存。
  • srs.conf 的 HTTP 回调对整个 vhost 生效,网关会拒绝非专用 app 与未知流名的推流 / 播放,所以 SRS 需独占一个实例或 vhost,不要与其它直播业务共用。
  • 已经建立的 RTMP 连接不会因网关重启而重新鉴权;残留 Egress 停止后其推流断开,播放端随之断流。

7.2 无法自动回收时的手动恢复

LiveKit 长时间不可用、回收一直失败时,网关会持续返回 cleanup_pending,不会自行放行。恢复步骤:

  1. docker compose stop mp-gateway;
  2. 通过 LiveKit API(ListIngress)删除名称前缀为 yudao-mp-gw- 的 Ingress;
  3. 通过 ListEgress(active)停止输出地址以 MP_GATEWAY_SRS_PUBLISH_BASE_URL/ 开头的 Egress;
  4. docker compose restart srs,断开 SRS 上残留的推流与播放连接;
  5. docker compose start mp-gateway,确认 /healthz 返回 ok。
  6. 成本估算(LiveKit 官方 CPU 估值):每个小程序会话 1 路 RTMP Ingress 转码约 2 核,每个远端 1 路 Track Composite Egress 约 2 核。1 对 1 小程序通话约 4 核,Egress 超出 CPU 余量会被 LiveKit 拒绝并如实返回 failed。

8. 已知限制与待验证点

  • 延迟:RTMP 推流 + Ingress 转码 + Egress 转码 + RTMP 拉流,秒级延迟,不适合对实时性要求高的场景。
  • 回声:小程序 live-pusher 与 live-player 是两个独立组件,系统回声消除效果未验证,试验阶段建议戴耳机。
  • 静音状态:小程序关闭摄像头或麦克风时,Ingress 推流仍在,PC / App 看到的是黑屏或静音画面,而不是 LiveKit 的 muted 状态。
  • 后台 / 断网:小程序进入后台或 RTMP 网络断开(live-pusher 内部重连耗尽,状态码 -1307)时推流中断,Ingress 参与者离房,1 对 1 通话会由后端 webhook 结束;小程序端也会在推流丢失超过时限后结束本次通话。不支持无缝恢复,需要重新发起。
  • 屏幕共享转出:本地真实媒体验证中出现过屏幕共享的 Track Composite Egress 处于 EGRESS_ACTIVE 但始终未推到 SRS 的情况(摄像头转出正常),根因待排查;网关只做启动超时与 failed 如实返回,不静默回退摄像头。
  • 本地真实媒体验证(FFmpeg 模拟推流 + H5,非微信原生组件)已观察到 Egress 地址中的 ?secret= 原样传给 SRS 并通过回调校验。
  • 待真实运行验证:纯音频 Track Composite 推 RTMP;live-player 拉带 ?token= 的 RTMP 地址;Ingress 对 live-pusher RTC 模式码流的兼容性。

9. 真机要求

  • 小程序主体为企业,类目在 live-pusher / live-player 支持范围内,并在小程序后台「开发 → 开发管理 → 接口设置」开通「实时播放音视频流」「实时录制音视频流」。
  • 网关 API 部署在 HTTPS 域名后,并加入小程序「request 合法域名」。
  • 手机可达的地址:Ingress RTMP(MP_INGRESS_RTMP_PUBLIC_HOST:1935)、SRS(MP_SRS_PLAY_PUBLIC_HOST:1936,需把 srs.conf 的 listen 改为对外地址);PC / H5 / App 照常需要访问 LiveKit 信令与媒体端口(MP_LIVEKIT_BIND_ADDRESS、MP_LIVEKIT_NODE_IP)。
  • 开发者工具不支持 live-pusher 推流,必须真机调试。