一个输入,数百个节目——在 MediaLive 上运行 24/7 FAST 频道
一个 24/7 FAST 频道每天、每天、永远播放数百个独一无二的节目。AWS MediaLive 将一个频道限制为 20 个输入附件。朴素的设计——每个视频一个输入——在午餐前就用完了插槽,并强制重新启动频道,导致直播流下线。我们通过一个动态输入解决了这个问题,该输入服务于频道将播放的每个节目,并在其之上添加了一个节目级别的编排层,使时间线在部署、部分故障和操作员编辑中保持自愈。
这就是该编排如何实际运作的工程故事。
快速概览
| 方面 | 详情 |
|---|---|
| 领域 | AWS MediaLive 上的 24/7 FAST 频道编排 |
| 每个频道的输入附件 | 1 个动态 + 1 个垫片(+ 可选 SRT)—— 远低于 20 个输入的上限 |
| 每个节目的身份 | 嵌入在每个操作名称中的 8 字节十六进制 programId |
| 垫片填充阈值 | 间隔 ≥ 6 秒时会触发一个 slate-switch 操作 |
| 调度容量 | 每个频道 1500 个操作(AWS 硬性限制,预检) |
| 自愈 | 每次部署后运行孤立操作清除 |
| 状态 | 已投产 |
业务问题
FAST 频道是一种 24/7 服务。运营商提前调度一整周或一个月的独特视频,频道必须在正确的时刻播放每一个视频——干净地切换源,保持水印稳定,发出广告插播提示,并用带有台标的垫片填充任何空白。观众绝不应看到黑帧、卡住的标志或上周节目循环播放。
这种编排必须经受住操作员对其所做的一切:编辑明天的节目单,在周中添加广告插播,在单个节目失败后重新部署,甚至在两次快速点击 Deploy 的情况下。系统要么将每个更改原子地应用到直播频道,要么干净地恢复。“频道现在处于无人理解的状态” 对于正在直播的基础设施来说是不可接受的结果。
MediaLive 60 秒入门
AWS MediaLive 是一个长时间运行的云编码器。您为其提供一个或多个 inputs(源流或文件 URL)以及一个定时 actions 的 schedule——切换到此输入、打开此叠加层、插入此提示。编码器永久运行,在预定的时间戳执行操作,并生成观众播放器消耗的 HLS manifest。
两个 AWS 限制影响着所有下游:
- 每个频道 20 个输入附件。硬性限制。一个“20 部热门电影”频道,如果天真地设计为每个视频一个输入,在第 21 部电影时就会达到上限。
- 每个频道 1500 个调度操作。同样是硬性限制。每个节目包含多个操作(输入切换、每个渲染的水印、广告提示、垫片切换),因此实际的上限更接近于在任何给定时间可容纳数百个节目。
对于一个预期无限期播放独特内容的 24/7 频道,这两个限制都至关重要。
为什么朴素方法会失败
显而易见的途径都以不同的方式失效:
- “每个节目一个输入。” 第一天就会达到 20 个附件的上限。添加更多需要重新创建频道——而频道重新创建需要 60-90 秒,在此期间直播流会中断。这对于 24/7 服务来说是不可接受的。
- “每次部署都重新创建频道。” 同样的问题,每次部署都会发生。每次节目更改时,观众都会经历一次中断。这对于一个本应是直播的频道来说,不是一个真正的选择。
- “将整个星期预编码成一个巨大的循环文件。” 扼杀了编辑模型。想重新安排明天的节目?重新编码整个星期。想插入广告?重新编码。FAST 频道之所以能作为一项业务运作,其根本原因是动态节目编排和按插播插入广告——将所有内容烧录到一个文件中会阻止这两点。
- “并行运行多个频道。” 需要播放器在它们之间切换,增加了 AWS 的成本,并中断了音频、MediaPackage 和 CDN 管道。与其说是解决了问题,不如说是将其复杂化了。
我们拥有的杠杆是 AWS 文档中的一个单一功能——动态输入上的 $urlPath$ 占位符——以及在此之上构建任何我们想要的编排的自由。
为什么这很重要。诀窍不是使用 $urlPath$。AWS 对此有文档说明。诀窍是在其之上构建一个自愈的编排层,该层能够经受住部分部署、操作员编辑和并发重试——而不会让直播频道陷入 UI 无法描述的状态。
我们的解决方案
一个 MediaLive 频道。一个动态输入链接到精确的 URL $urlPath$。一个垫片输入用于填充空隙。在每个节目边界,一个 InputSwitchScheduleActionSettings 操作会用该节目实际的 S3 路径覆盖动态输入的 URL。频道永远不需要新的输入附件,永远不需要重启,也永远不会下线。
有趣的工作是运行在该输入之上的编排层——用 program ID 命名每个操作,以便在失败后清除孤立项,用垫片填充 ≥ 6 秒的空隙,使观众永远不会看到停顿帧,在每次输入切换后经过精确测量的时间激活水印,以避免叠加层在缓冲帧中闪烁,并对 1500 个操作的上限进行预检,以便部署在 UI 中失败而不是在 AWS 上飞行中失败。
图 1 · 频道架构
架构
- 垫片输入——附加到频道上的静态资产,用于填充节目之间的空隙。
- 动态输入——使用 Sources: [{ Url: "$urlPath$" }] 和 Type: MP4_FILE 创建一次。URL 是一个占位符;实际路径在调度时为每个节目提供。
- Lambda 编排器 (fastChannel-lambda-fun/index.js)——负责频道上出现的每个活动。为每个节目生成一个 8 字节的十六进制 programId,并用它标记每个相关操作。
- MediaLive 调度——一个单一的有序操作列表,所有操作都通过一个 BatchUpdateScheduleCommand。AWS 将其限制为 1500 个操作。
- 后端预检 (schedule.service.ts)——在每次部署前调用 Lambda 的 GET_SCHEDULE_COUNT,如果新操作将超出上限,则拒绝继续。
孤立项清除 (sweepIncompleteProgramGroups)——在每次部署后运行。按 programId 对操作进行分组,删除任何缺少其锚定 input-switch 操作的分组。
关键工程决策
1. 每个节目一个动态输入,一个 URL 覆盖
动态输入是使用 Sources: [{ Url: "$urlPath$" }] 创建的。在每个节目边界,Lambda 会发出一个 InputSwitchScheduleActionSettings 操作,提供 UrlPath: [program.videoUrl]——即该节目 MP4 文件的实际 S3 URL。MediaLive 在执行时替换占位符并从真实源拉取数据。
现在,一个输入附件就能服务于频道将播放的所有节目——在频道的整个生命周期中,有效实现无限量的独特视频,其限制仅受限于任何单个时间点的每次部署调度操作上限。20 个输入的上限不再是一个需要我们考虑的约束,频道也永远不需要重启来添加新内容。
图 2 · 动态输入 URL 覆盖

我们故意做出的权衡。动态输入不会预先验证 URL——MediaLive 仅在切换时解析占位符,因此 404 错误会作为流端错误而不是部署时拒绝出现。我们接受这种成本,以换取一个输入的架构简洁性。后端的独立 ffprobe 验证会在部署前捕获格式错误源。
2. 带有节目标签的操作名称实现自愈
Lambda 发出的每个操作都在其名称中包含节目的 8 字节十六进制 programId:input-switch-${programId}、watermark-on-${programId}-${rendition}、ad-break-start-${programId}-${i}、program-end-${programId}、slate-switch-${programId}。
每次部署后,sweepIncompleteProgramGroups 会列出频道上当前的所有操作,按其嵌入的 programId 进行分组,并删除任何缺少其锚定 input-switch 操作的分组。这是针对失败的部分部署、竞争性编辑以及任何可能导致频道留下半个节目操作的条件的清理路径。
操作名称编码是整个身份机制。MediaLive 本身没有“program”的概念——编排层通过命名约定将其投射到 MediaLive 上。
为什么这很重要。如果每个操作名称中没有嵌入 program ID,孤立项清除将无法知道哪些操作属于一起。操作级别的清理要么删除过多(整个频道重置),要么删除过少(永远不会关闭的孤立水印)。命名约定就是数据模型。
3. 6 秒垫片规则
节目很少完美衔接——通常在一个 MP4 结束与下一个预定节目开始之间总有几秒钟的间隔。当该间隔 ≥ 6 秒时 (MIN_SLATE_GAP_MS = 6000),编排会发出一个 slate-switch 操作。
该阈值并非随意设定。MediaLive 强制规定任何两个调度操作之间至少间隔 5 秒;如果 slate-switch 操作与下一个节目的 input-switch 操作间隔小于此值,则会导致拒绝。6 秒规则为 MediaLive 提供了所需的间隔,并为编排层留出了一秒的时钟漂移安全裕度。如果小于 6 秒,我们会让前一个节目的最后一帧短暂冻结,而不是冒部署被拒绝的风险。
4. 带测量激活延迟的每渲染水印
水印是一个 StaticImageOutputActivate 操作,为每个输出渲染(1080p, 720p, 480p, 360p)发出——每个节目四个操作。每个操作在该节目 input-switch 1,500 毫秒后触发。
存在延迟是因为输入切换后的第一帧仍在缓冲;在精确切换时刻激活叠加层可能会产生短暂的闪烁,因为叠加层绘制在一个尚未完全渲染的帧上。1,500 毫秒是在测试中所有四个渲染版本上始终产生清晰激活的值。它是一个经过测量的常数,而不是 MediaLive 文档中记载的参数——它存在于代码的一个地方,因此未来的调整只需更改一行代码。
我们故意做出的权衡。每个渲染的叠加层操作的动作计数是单个全局叠加层的 4 倍。我们接受这个成本,因为每渲染路径允许每个输出获得一个精确匹配其像素尺寸的水印,而不是让 MediaLive 将单个叠加层缩小到所有四个渲染版本。结果是在 SD 输出上徽标明显更清晰——并且在达到 1500 上限之前,为数百个节目留下了足够的动作预算。
5. UI 中预检的 1500 个操作上限
AWS 将 MediaLive 频道的调度操作硬性限制为 1500 个。每个节目约有 7-8 个操作(输入切换 + 4 个水印 + 2 个广告提示 + 偶尔的垫片),因此频道大约可以容纳 180-200 个活跃节目,具体取决于广告密度和垫片频率。这对于长期部署来说是一个实际的上限,具体数量取决于每个节目的复杂性。
在每次部署之前,后端会调用 Lambda 的 GET_SCHEDULE_COUNT,该函数通过 DescribeScheduleCommand 计算频道上的实时操作,并返回 { liveCount, capacity: 1500 }。如果 liveCount + (newPrograms × 8) 将超过 1500,后端会抛出 SCHEDULE_ACTION_CAP_EXCEEDED 错误,并附带确切的剩余容量——在向 MediaLive 提交任何内容之前。操作员在 UI 中看到限制,并收到先清除过去节目的指导。部署永远不会中途撞墙失败。
图 3 · 单个节目的操作时间线

结果
- 一个 MediaLive 频道通过一个动态输入附件,在其生命周期内可提供有效无限量的独特节目。20 个输入的上限不再是我们必须围绕其进行规划的约束——任何时候的容量都由调度操作限制而非输入限制决定。
- 频道编排是自愈的:每次部署都以孤立项清除结束,因此失败的部分部署不会使调度处于不一致的状态。
- 调度容量是有限且可见的。操作员在点击之前就能在 UI 中看到 1500 个操作的上限,而不是在部署中途遇到不透明的 AWS 拒绝。
- program ID 命名约定是整个身份层——而且它是一个字符串。没有新的基础设施,没有额外的存储,没有依赖项。这是将“program”投影到 MediaLive 扁平操作列表上的最简单方式。
技术栈: AWS MediaLive · AWS Lambda · NestJS · MongoDB · TypeScript · Node 18 · AWS SDK v3 (@aws-sdk/client-medialive)

