FAST 频道通过商业广告时段盈利。整个商业模式建立在一个假设之上:当频道发出“切换到广告”的指令时,所有下游监听系统——广告服务器、SSAI 拼接器、智能电视播放器——都能在同一时刻,以完全相同的持续时间接收到该指令。SCTE-35 是承载此消息的标准协议。如果悄无声息地出错——而静默正是默认的故障模式——广告将无法渲染,在错误的帧触发,或持续时间不正确。观众会看到一个广告片头,收入也就随之流失。
这讲述了 mStudio 的 FAST 频道如何从操作员友好的输入中发出符合标准的 SCTE-35 提示,以及我们为何将其构建为每个广告时段三个提示而非一个。
快速概览
| 方面 | 详情 |
|---|---|
| 领域 | 针对 AWS MediaLive FAST 频道的 SCTE-35 广告时段信令 |
| 操作员工作流程 | 每节目广告时段编辑器 — 位置(秒),持续时间(秒) |
| 每个广告时段发出的提示 | 三个 — 广告开始 TimeSignal,SpliceInsert,广告结束 TimeSignal |
| 持续时间限制 | 10-120 秒,在数据层进行验证 |
| 下游消费者 | 播放器、广告服务器和 AWS MediaTailor(已设计但尚未集成)都是示例。 |
| 状态 | SCTE-35 提示已投入生产;SSAI 集成正在设计中 |
业务问题
FAST 频道上的每一个商业广告时段都是一个收入事件。频道会发出一个标记,提示“这里将插入广告,持续 30 秒,请准备。”下游系统——Google Ad Manager、AWS MediaTailor、区域广告服务器、智能电视播放器 SDK——会读取该标记并决定插入什么内容。
当标记正确时,广告时段流畅播放,曝光量计入,操作员获得收益。当标记错误时——持续时间错误、格式不正确——广告系统要么拒绝它(不投放广告),要么错误地接受它(广告长度不正确,或超出到下一个节目)。这两种结果都会造成实际的金钱损失,并且都是静默失败。频道持续播放,观众看到广告片头或不自然的切换。变现流程只会产生较低的数字,并且没有错误消息可供追踪。
工程目标并非“支持广告”,而是:操作员定义的每一个广告时段都必须作为符合标准的 SCTE-35 信号,在操作员选择的精确帧时刻,每次都送达每个下游消费者。
SCTE-35 的真实面貌(90 秒版本)
SCTE-35 是一种将提示消息插入视频流的标准。提示不承载广告内容;它们承载的是信号——“广告时段在此开始”、“广告时段在此结束”、“此片段时长 N 秒”。下游系统读取这些信号并据此操作:SSAI 层将实际广告创意拼接进 HLS manifest,智能电视播放器触发叠加层,广告服务器记录一个广告位机会。
两种提示格式对我们的用例很重要:
- SpliceInsert — 最初的 SCTE-35 提示。包含 SpliceEventId、以秒为单位的持续时间,以及一个“out of network”标志。大多数经典广告服务器读取此格式。
- TimeSignal — 更新、更具表现力的提示。包含一个 SegmentationDescriptor,其中有 SegmentationTypeId(52 = Provider Ad Start,53 = Provider Ad End)和以90,000 刻度为单位的 SegmentationDuration。SSAI 系统和现代播放器更倾向于此格式,因为它能在开始和结束之间进行干净的配对。
两种格式都是正确的 SCTE-35。不同的下游消费者偏好不同的格式。只发出一种格式会损失潜在收益。
为何这很重要。 一个简单的实现会为每个广告时段发出一个 SpliceInsert 便认为大功告成。广告时段在传统播放器上能工作,但在 SSAI 上会静默失效。你的广告库存一半能变现,一半不能。你只有在营收数字偏低时才会知道具体是哪一半。
简单方法为何会失败
捷径很诱人,因为它们都几乎能奏效。
- “在编码时,将广告插入原始视频。” 这种方式没有灵活性。操作员无法进行 A/B 测试广告位,无法在部署后更改广告,无法投放区域性或受众定向广告。FAST 频道存在的全部原因是为了通过多种方式将相同内容变现——在编码时将广告烧录进去就杜绝了这种可能性。
- “利用客户端广告插入。” 广告屏蔽简单。每个设备有不同的代码路径。没有服务器端个性化。并且它完全绕过了 SSAI,这意味着更低的 CPM。
- “每个广告时段只发出一个 SpliceInsert。” 最常见的生产错误。在某些播放器上可以工作,但会被需要 TimeSignal 提示进行开始/结束配对的 SSAI 系统静默忽略。部分变现,且没有错误可供调查。
- “因为规范中提到了 90 kHz 刻度,就用它来表示所有内容。” 规范比这更恼人。SpliceInsert.Duration 以秒为单位。SegmentationDuration 以90 kHz 刻度为单位。将它们混淆会产生通过模式验证并发送出去的提示,然后因持续时间格式错误而在播放器端失败。编码器不会捕获到它。播放器只会跳过广告时段。
- “让 MediaLive 确认。” 它不会。MediaLive 接受重叠的广告时段、零持续时间的广告时段以及不可能的片段持续时间,而不会发出抱怨。确认必须在 MediaLive 看到动作之前完成。
我们拥有的杠杆是操作员 UI(其中广告时段只是 {position, duration} 对)与 MediaLive 调度(其中每个提示都必须完美无缺)之间的边界。
我们的解决方案
将广告时段作为端到端的一等数据对象处理。操作员以最简单的术语定义它。后端在模式层进行一次验证。Lambda 在部署时将其转换为一个三提示堆栈,每个提示都以其自身规范要求的时间基准表示。堆栈中的其他任何东西都不需要了解 SCTE-35 — 翻译功能位于一个函数中,与拥有所有其他调度操作的同一个 Lambda 中。
图 1 · 端到端广告标记管道

架构
- 前端广告时段编辑器 — 操作员通过指定位置偏移(节目开始后的秒数)和持续时间(秒)为每个节目添加广告时段。片头片尾广告位是数据模型的一部分,并已准备好用于播放层。
- AdMarker 集合 (MongoDB) — 每个包含广告时段的节目对应一个文档,引用视频。adBreaks[] 数组存储 {position, duration},Mongoose 在保存时强制执行 10 ≤ duration ≤ 120。片头片尾广告带有 adBreakId 引用,用于下游配对。
- NestJS 后端 (schedule.service.ts) — 在部署时,将每个调度与其 AdMarker 连接起来,并将字段重命名为 Lambda 的契约(position → offsetSeconds,duration → durationSeconds)。一次批量获取,没有 N+1 问题。
- Lambda 协调器 (fastChannel-lambda-fun/index.js) — 负责 SCTE-35 翻译。对于每个广告时段,将三提示堆栈发出到与承载输入切换和水印的同一个 BatchUpdateScheduleCommand 中。操作名称按节目进行版本控制,以便在部分部署后,孤立清除可以将其配对。
- AWS MediaLive — 接收操作,在操作员选择的秒数时,在 HLS manifest 中发出 #EXT-SCTE35 标记。
关键工程决策
1. 每个广告时段三个提示,而非一个
每个广告时段针对节目中相同的逻辑时刻发出三个独立的动作。
图 2 · 单个广告时段生命周期

开始和结束 TimeSignal 提示共享一个 SegmentationUpid,以便下游系统可以确定性地配对它们。SpliceInsert 携带一个唯一的 SpliceEventId 用于播放器级别的去重。不同的消费者读取不同的提示。发出所有三个提示涵盖了我们今天关注的所有契约以及未来所有可能的契约。
常见的生产错误。 发出一个 SpliceInsert,并假设生态系统的其他部分会自行解决。SSAI 系统会悄悄地丢弃缺少匹配 TimeSignal 提示的广告时段;经典广告服务器会忽略仅包含 TimeSignal 的信号。没有这三者,每个广告时段都只会被你的下游堆栈中的一部分变现,而其余部分则会错过——你将从营收报告中而非日志中发现这个问题。
2. 两个时间基准,在一个函数中协调
SpliceInsert.Duration 以秒为单位。SegmentationDuration 描述符内的 TimeSignal 以 90,000 刻度单位表示。相同的逻辑持续时间,两种编码——野外最常见的 SCTE-35 错误就是将它们混淆。
图 3 · 时间转换逻辑

转换逻辑位于 buildProgramActions 中,且仅在那里。当提示持续时间出错时,只有一个规范的地方可以查看;如果规范发生演变,也只有一个地方可以更改。
我们有意做出的权衡。 我们本可以将两种时间基准都存储在 AdMarker 文档中,并让 Lambda 进行复制。我们有意没有这样做:只存储秒数值意味着操作员只需设置一个数字,只需验证一个数字,也只有一个数字可能出错。90 kHz 值是派生的,从不存储。派生数据不会发生漂移。
3. 位置是相对的,触发时间是绝对的
操作员说“节目开始 720 秒时插入广告时段”。Lambda 将实际的 UTC 触发时间计算为 programStartTime + offsetSeconds × 1000,并将其标记到操作上。所有其他调度操作——输入切换、水印开启、节目结束——都使用相同的偏移量到时间戳的管道。广告提示与其他所有内容一样落在相同的帧边界上,对哪个时钟拥有时间线没有任何歧义。
4. 在数据层进行验证,而非传输层
的 AdMarker 模式在 Mongoose 保存时强制执行 duration ∈ [10, 120] 秒。超出范围的广告时段永远不会到达 Lambda。那些会超出节目结束时间的广告时段在构建时会被剪切而不是拒绝——操作员不会因为一个错误的广告时段而失去工作。不能发生的错误类别比能发生的错误类别更有趣。
5. 提示发出与 SSAI 解耦
我们今天发布的是信令层。通过 AWS MediaTailor 进行服务器端广告插入——它将消耗这些提示,将真实的广告创意拼接进 HLS manifest——已完全设计好,但尚未集成。有意为之的顺序是:在启用 SSAI 之前,首先确保提示层在生产环境中正确无误,并被真实频道使用。当集成上线时,提示已经存在。下游系统从第一天起就能获得清晰的契约进行消费。
为何此顺序很重要。 当你的提示出错时,SSAI 调试是残酷的,因为即使错误出在提示上,每次失败看起来都像是 SSAI 失败。通过首先独立发布提示层并针对真实播放器和广告服务器进行验证,我们在集成混乱发生之前消除了整整一类问题。
成果
- 操作员定义的每个广告时段都会在频道 HLS manifest 中发出一个符合标准的三提示 SCTE-35 堆栈——在正确的秒数,以正确的时间基准。
- 翻译层位于一个函数中。规范更改或新的下游消费者只需更改单个文件,而无需在整个代码库中搜寻。
- 持续时间验证在数据层运行,在任何提示到达编码器之前。操作员的错误会在 UI 中显现,而不是在播出时静默发生——格式错误的提示无法到达 MediaLive。
- 提示生成是确定性的:相同的操作员输入会产生相同的 SCTE-35 动作,因此相同的广告时段在每次部署和每个频道中行为相同。
- 信令层已发布并上线。SSAI 消费者 (MediaTailor) 已设计好并准备好集成——届时,每个现有频道的提示都将已在那里等待它。
技术栈: AWS MediaLive · AWS Lambda · NestJS · MongoDB · TypeScript · Node 18 · SCTE-35 (Scte35TimeSignalSettings, Scte35SpliceInsertSettings) · AWS SDK v3

