EP18:录像与回放:给直播加上 VOD¶
| 上集回顾 | EP17《从零部署:15 分钟跑起你自己的 CDN》 |
|---|---|
| 下集预告 | EP19《压力测试方法论:从单机到全球压测》 |
0. 本集目标¶
看完这一集,观众应该能做到四件事:
- 分清录像里的两套系统:ppcenter 的"场次事件"只是审计,真正决定录像文件切分和上传的是 recorder 节点的 split-rec 接口。
- 讲清楚两阶段切分协议:为什么录像不能"一直录着",而必须由业务后端显式地喊"开始"和"结束"。
- 走一遍从直播流到 VOD 的完整链路:fMP4 分片 → recorder 节点自己直传对象存储 → 由对象 ACL 和
recordPublic开关共同决定的回放链接。 - 知道实现选择与通用要求的区别:拿到
200不等于上传成功,而且失败是静默的;磁盘是硬约束;fMP4 与「只录最高层」都是实现/成本取舍,不是录像系统的必然规则。
1. 开场钩子(逐字稿要点)¶
前面几集我们一直在讲"怎么把画面实时送到观众眼前"。但真实业务里还有一个反向的需求:把这一场留下来。真人视讯要留存审计,竞拍要回放出价过程,在线课堂要给学生补课。这一集讲这套系统怎么给直播加上 VOD。先说一个容易被忽略的关键点:录像不是"打开录制开关一直录"这么简单——因为"一场"什么时候开始、什么时候结束,只有业务的系统知道,服务端并不知道"这局游戏打完了"。所以真正决定录像边界的,是一套由业务后端主动触发两阶段协议。这一集就把它拆开。
2. 先分清:录像里其实有两套独立的系统¶
这是接入时最容易混淆的地方,必须一开始就说清楚:
| 系统 A:场次事件(审计) | 系统 B:split-rec(真录像) | |
|---|---|---|
| 谁提供 | ppcenter | mmx-recorder 节点 |
| 接口 | POST /v1/records/round-events |
POST /api/record/split |
| 干什么 | 记录"某场次开始/结束了"这个事实,供后台查询、审计 | 真正对媒体流切分,并在结束时上传录像文件 |
| 影响录像文件吗 | 不影响 | 影响 |
两者字段名很像(都有 gameId/gameRound/roundId 这类),但语义完全独立。只调 A 不会产生任何录像文件,只调 B 才真正切分上传。本集重点讲的是 B,因为"给直播加上 VOD"落到实处的就是它。
3. Recorder 是第三种节点角色¶
前面几集讲的媒体节点只有两种:Origin 和 Edge。录像引入了第三种角色——Recorder。
这里有一个架构上的演进值得点出来:录像最初的设计是跑在 Origin 进程内的“内置能力”,后来改成与 Origin/Edge 平级的第三种节点角色(NODE_ROLE_RECORDER)。要区分两个能力:注册与监控表示控制面知道 recorder 在线、角色和容量;自动调度则要求控制面能按负载选节点、分配录制任务并处理失败重调度。前者存在不等于后者已经完整实现。
- 职责不同:Origin 关心的是"低延迟地把流转发出去",Recorder 关心的是"完整、可靠地把流落盘"——一个对延迟敏感、一个对完整性敏感,混在一个进程里,两边的资源会互相挤占。
- 资源画像不同:Recorder 吃的是磁盘和上传带宽,Origin 吃的是转发带宽和会话数。分开之后可以各自按需扩容,也不会因为录像把盘写满而拖垮推流入口(EP15 讲的"看清每一层加机器会把负载推给谁",在这里同样成立)。
- 控制面可识别该角色:ppcenter 可以按 recorder 角色注册、心跳和统计;这提供监控与后续调度基础,但具体录制请求是否自动选点,仍要看调用链是否接入选择、租约与故障重试,不能由注册状态反推。
PPCDN 案例中的 Recorder 配置为 record: yes、录像格式 fmp4,并像其它节点一样注册、上报心跳和容量。节点池和容量指标可用于观察 recorder 候选,但是否已自动分配录制任务应单独验证,不能笼统称为“独立自动调度”。
4. 两阶段切分协议:业务后端喊"开始"和"结束"¶
录像的核心接口是 POST /api/record/split,它靠 gameRound 字段是否为空,区分两个阶段:
| 阶段 | gameRound |
效果 |
|---|---|---|
| 场次开始(round-start) | 留空 | 把该桌台当前每个在线 view 的流"切一刀",从这里开始累积新分段 |
| 场次结束(round-end) | 传具体值 | 校验调用方是当初开始这场次的 owner,再切一刀、重命名为最终文件,触发异步上传 |
为什么必须两阶段?因为只有业务的系统知道"一场"的边界。一场拍卖从哪一刻开始、一局游戏到哪一刻结束,是业务语义,媒体服务端无法自动判断。所以协议把这件事实交给业务后端显式表达:你来喊开始,你来喊结束,我负责在两次调用之间把这段流切出来、存好。
几个设计细节,都是实战里会被踩到的:
- 签名用调用方 appId 自己的
appSecret:不是某个部署级共享密钥。appId本身也参与签名,防止被篡改成别的 app。这跟 EP06 讲的"短期签名 + 资源绑定"是同一种安全思路。 - 两种鉴权模式:默认
simple(md5(appSecret + time + ...),time是签名时刻,容忍 ±30s);更严格的advance(HMAC-SHA256 + nonce 防重放,容忍 ±5min)。要防重放就上 advance。 - 静默 no-op:对一个从没开始过的场次调用 round-end,返回 200 成功,不报错——因为结束信号是无条件发出的,不该因为没匹配到开始就告警。
- 冲突语义很直白:同一
(appId, tableId)同时只能被一个 owner 持有;别的 owner 来抢,返回 500(带具体文本,不是 409)——接入时要按msg判断,不能只看状态码。 - 明确区分"没有推流"和"其它失败":如果这个桌台当前没有任何 view 在线,返回
code=50001,和真正的服务端异常分得开,接入方不用去猜文本。 - 多机位自动识别:一个
tableId下多个 view(多机位)不需要事先配置——只要各 view 的流在线,round-start 会自动对每个 view 同时录。这比"每个机位单独配置、单独调用"省了一大截运维。 - 限流:单 IP 每秒最多 500 次请求。
一个务实的提醒:"开始"之后别立刻"结束"。两份调用之间如果短于一个分段时长,还没有新数据写进当前分段,结束会以"nothing to finalize"失败。正常场次远长于这个量级,只有联调/压测脚本容易踩到。
5. 从直播流到 VOD:fMP4 分片 + 节点自行直传¶
切分只是第一步,真正把录像变成可回放的 VOD,还要走完"落盘 → remux → 上传 → 登记"这条链路——这条链路除了最后"登记"一步,全程都在 recorder 节点自己的进程里完成,ppcenter 只负责收一条事后上报。
① PPCDN 选择 fMP4(fragmented MP4)持续写盘。 Recorder 按 fMP4 持续写盘,split-rec 在业务边界形成独立录像段。上传前会先把这段 fMP4(moov 在前、数据分散在多个 moof/mdat)重新封装(remux,不重新编码)成传统单 moov、支持 fast-start 的 MP4——这是为了让 <video src> 或下载后双击播放不用等下载完就能起播;重封装失败时退化为直接上传原始 fMP4 文件,不会因此丢失这段录像。fMP4 本身便于与现代浏览器/MSE、CMAF 工作流衔接,但不是普遍优于 MPEG-TS:TS 对中断容忍和传统 HLS 工具链更成熟,普通 MP4 也可能更适合单文件下载。格式应按播放端兼容性、故障恢复、封装开销和转封装成本选择;PPCDN 的 fMP4 是实现取舍。
② round-end 触发异步、尽力而为的上传——上传本身在 recorder 节点自己的进程里完成,不经过 ppcenter。 这里有一条极其重要的契约:上传失败不影响 /api/record/split 已经返回的 200。也就是说,业务后端拿到成功响应,只代表「切分和登记完成了」,不代表上传已经完成、或一定会成功。recorder 节点直接用对象存储的官方 SDK 发起上传(AWS S3 SDK,OVH net-storage 等 S3 兼容服务走这条路;没配 S3 时退化到 MinIO SDK),凭证是节点自己 .env 里的访问密钥——不是"预签名 URL 的 HTTP PUT"这种更松耦合的方式,节点上确实绑定了具体的对象存储 SDK。
③ 重试在节点本地完成,而且失败是静默的——ppcenter 可能永远不知道。 上传失败由 recorder 节点自己按指数退避重试(1s→2s→4s...上限 60s),最多 10 次;只有某一次成功了,节点才会把文件信息(objectKey/playbackUrl/时长/大小)上报给 ppcenter(POST /internal/mmx/v1/records/split-rec-files)。如果 10 次全部失败,ppcenter 不会被告知——没有「pending/failed」这种状态可查,这场录像就单纯不会出现在 §6 的录像列表里,线索只留在 recorder 节点自己的日志里(一条 WARN)。业务后端如果要确认一场录像到底传成功没有,唯一办法是定期查 /v1/apps/{id}/recordings 看这一行有没有出现——没出现,可能是还没传完,也可能是 10 次重试后彻底失败,接口层面无法区分两者。
文档里另一套录像系统,别搞混
ppcenter 另外还跑着一套完全独立的"持续录像任务"机制(POST /v1/records/tasks,按 appId + streamPath 维度,而不是本集的 gameRound/tableId)。那一套确实有 ppcenter 侧的 pending → uploading → uploaded / failed 分片状态机,以及 /internal/mmx/v1/records/segments/retryable/claim 这种"节点认领重试"协议,服务的是长时间不间断录制的场景。这是另一条产品线,和本集讲的两阶段 split-rec 并行存在、互不影响——查 API 文档时容易把两者的重试模型看混,这里特别提一句。
④ ppcenter 不持有桶凭证。 这是一个刻意的权限边界:只有 recorder 节点的 .env 里有对象存储的读写凭证,ppcenter 没有。所以 ppcenter 自己删不掉对象存储里的文件——录像过期要清理时,它是往一张删除队列表(record_object_deletions)写一行,由 recorder 节点来认领并执行删除,再把结果报回来。这样"能删桶数据"的凭证只存在于最需要它的那一类节点上,控制面被攻破也不会直接导致录像被删。删除失败重试次数很低(5 次)——反复删不掉说明是权限/配置问题,应该暴露给人,而不是无限重试掩盖成存储泄漏。
⑤ 文件命名与多环境隔离。 最终对象名是 <tableId>-<viewName>-<roundId>.mp4(gameId 只用于上报/审计,不进文件名)。如果请求显式传了 appEnv,就在对象名前加一层 <appEnv>/ 前缀——但不管传什么,都落在同一个桶里。这是对象存储的约束:多环境只能靠前缀区分,不是桶级权限隔离。联调时传 appEnv=test,就能让测试文件落在独立前缀里,不与生产录像混在一起。
6. 回放:节点自己算好的一个公开链接¶
录像传到对象存储后,「回放」就有了数据基础。但这里有一个容易想错的地方:split-rec 的回放链接不是 ppcenter 现场签发、带有效期的短期凭证(EP06 讲的 txTime/txSecret 那种签名模式)——它是 recorder 节点在上传成功那一刻,自己用配置好的域名 + 桶名 + objectKey 拼出来的一个普通公开 URL(形如 https://<存储域名>/<桶名>/<objectKey>,域名本身已内嵌桶名时省掉这一段),不带任何签名、没有过期时间;节点上传成功后把这个字符串原样上报给 ppcenter 存起来。
这个链接能不能被任何拿到它的人一直访问,完全取决于对象自己的 ACL:如果上传时配置了 S3_ACL=public-read,这个链接就是永久公开的;如果用的是桶默认 ACL 且默认私有,这个链接实际上可能谁都打不开。这是和 EP06「短期签名 + 资源绑定」完全不同的安全模型——没有 TTL 意味着它不会像签名 URL 一样自动失效,一旦公开就没有时间窗口帮你兜底。
① 控制台录像列表:GET /v1/apps/{id}/recordings 返回这个 app 的录像清单(tableId/gameId/gameRound/fileName/playbackUrl/durationSeconds/sizeBytes/创建时间)。有一个开关要注意:「是否公开」(recordPublic)只控制这个接口要不要把 playbackUrl 吐出来,不改变对象存储里文件本身的访问权限——那是对象/桶自己的 ACL 决定的,和这个开关无关。关掉 recordPublic,只是让 ppcenter 自己不往外吐这个链接;如果对象本身是 public-read,知道链接的人仍然能直接访问。别把「关掉公开」误当成「文件不可访问」。
另一个长得很像的接口不是这套系统的
ppcenter 确实另外提供一个 POST /v1/records/rounds/manifest 接口,会现场签发带 txTime/txSecret、默认 5 分钟有效期的分片直链,用的是和 EP06 一样的签名函数。但它服务的是 §5 提到的「持续录像任务」系统(按 appId/streamPath/roundId 查分片),不是本集的 split-rec 流程——对 split-rec 产生的录像调用这个接口,查不到任何分片。写集成代码或读 API 文档时注意别把两者当成同一个回放机制。
② 存储也在计费:录像会按「存量」计费(占用多少 GB·天),和按流量计费是不同的口径。record_storage_daily 每天给每个 app 记一行存量快照,保留期最长 90 天(MaxRecordRetentionDays)。这也是为什么「录像」不只是个技术功能,而是一个要算账的产品能力(EP19/EP20 还会回到成本这个话题)。
7. 诚实的边界¶
200≠ 上传成功,而且失败是静默的。上传是异步、尽力而为的,重试完全在 recorder 节点本地完成(最多 10 次);拿到成功响应只代表切分和登记完成。split-rec 没有专门的"上传状态"查询接口——要确认一场录像有没有传成功,只能去查/v1/apps/{id}/recordings里有没有出现这一行,查不到既可能是"还没传完"也可能是"重试 10 次后彻底失败",两者在接口层面无法区分。- 磁盘是硬约束。Recorder 是独立节点、本地先落盘、结束才上传,所以磁盘容量直接决定它同时能录多少路、能撑多久。生产上靠
recordDeleteAfter(固定时长清理)和recordMinFreeSpace兜底。这里有一个真实教训(纯磁盘容量估算,不是实测,且当时标注为"待复核、未实机验证"):$12档机器(约 60GB 盘、约 52GB 可用)在mmxNodeCapacity=48路并发全程落盘的配置下,按 5Mbps/路估算只能撑约 24~30 分钟;反推 1/4/24 小时缓冲对应的安全上限大致是 ~20/~5~6/~1 路——单机录像容量必须按实测校准,不能直接拿准入阈值当安全值。 - PPCDN 当前选择只录最高层。这降低存储、上传和索引成本,但会失去多档回放、低带宽适配和最高层缺失时的冗余。多层并非原理上“无法同时落盘”:可以分别封装并对齐时间线,代价是更多存储、处理与 manifest 复杂度。应把“只录最高层(且优先 H.264)”表述为当前实现和成本取舍。
- app 凭证有同步延迟。recorder 每 30 秒从 ppcenter 同步一次有效 app 列表(含
appSecret),所以新建 app、欠费恢复、或改了appSecret,最多有 30 秒才在录像侧生效。 - 集成测试只覆盖了 record 角色的录像面(
integrationTest/record:编译真实 mmx 二进制、真 ffmpeg RTMP 推流触发 fMP4 落盘、真实覆盖 split-rec 与鉴权分支)。而且这套本地测试只验证"上传不阻塞响应"这个契约,不验证真实上传成功——因为本地没有对象存储可写。真实上传要对着生产存储环境单独验。
8. 框图¶
8.1 一次录像的完整链路¶
客户业务后端
│ ① round-start(切一刀,开始累积) ┌─ 可选:round-events 上报 ppcenter(审计)
├───────────────────────────────────────►│
│ │
┌──┴──────────────────────┐ │
│ mmx-recorder 节点 │ │
│ · 对每个在线 view 切分 │ │
│ · 持续写 fMP4 分片 │ │
└──┬──────────────────────┘ │
│ ② round-end(再切一刀 + 重命名 + faststart remux)
│ ▼
│ 文件:<tableId>-<viewName>-<roundId>.mp4
│
│ ③ 节点自己用 S3/MinIO SDK 直传,最多重试 10 次
▼ (失败只写本地日志,ppcenter 不会知道)
┌─────────────────┐
│ 对象存储(S3兼容)│◀─────────────────────────────┐
│ OVH net-storage │ │ ⑤ 用 playbackUrl 直接访问
└────────┬────────┘ │ (普通公开链接,无签名/
│ │ 无 TTL,能否打开看对象 ACL)
│ ④ 上传成功才上报一次: │
│ objectKey + playbackUrl │
▼ │
┌──────────────┐ │
│ ppcenter │ GET /v1/apps/{id}/recordings │
│ 存 file 元数据 │────────────────────────────────┘
│ (无桶凭证) │ (recordPublic=true 才带 playbackUrl)
└──────────────┘
8.2 权限边界:为什么删除要"绕一圈"¶
ppcenter:持有分片/回放元数据,但没有对象存储读写凭证
│
│ 录像过期 → 写一行删除任务(pending)
▼
record_object_deletions 队列
│
│ recorder 节点认领(它有桶凭证)
▼
recorder 删对象 → 回报 done / failed(≤5 次后转人工)
9. 结尾与下集预告(逐字稿要点)¶
这一集给直播加上了 VOD:业务后端用两阶段协议显式划出场次,Recorder 节点自己完成写盘、remux、上传与重试,再把一个由对象 ACL 决定能不能公开访问的链接报给 ppcenter 供控制台展示。注册监控不能与自动调度画等号;fMP4 和只录最高层是 PPCDN 的实现/成本取舍;回放链接是否公开取决于对象存储 ACL 和
recordPublic开关,不是像 EP06 那样的短期签名凭证。200不代表上传完成,而且上传失败是静默的;磁盘、链接可见性和节点侧凭证治理都必须单独设计。到这里,Module 4 的"把环境跑起来"和"加上录像闭环"都完成了。下一集,我们做实战里最容易被跳过、也最不能跳过的一步:压力测试——怎么从单机测到全球,以及为什么单元测试通过不等于能扛住生产流量。