当你问任何直播电视运营商他们希望在排程工具中看到什么时,他们几乎无一例外地会说:“让我像在文件夹中整理文件一样构建一个频道。拖入一个节目,放到我想要的位置,就完成了。”
问题在于,频道排程不是一个文件夹。它是一份具有严格约束的法律文件。两个节目不能同时播放。编码器不能以快于每五秒的速度切换输入。已经开始播放的节目不能编辑。而一部电影如果播放到午夜之后,必须作为一个节目处理,不能在日期边界处分割。
每一次拖放操作都可能导致潜在的约束违规。本文讲述了mStudio的排程器如何将操作员友好的拖放操作转化为AWS MediaLive上保证合法的频道时间线——包括操作员将一个节目直接拖放到另一个节目之上时的处理方式——以及为什么系统会自动解决这些冲突,而不是向操作员抛出错误并留下一个难题。
快速概览
| 方面 | 详情 |
|---|---|
| 领域 | AWS MediaLive 上实时 FAST 频道的拖放式排程 |
| 冲突解决 | 通过四规则对齐算法自动移位 |
| 最小相邻间隔 | 6秒(MediaLive的5秒最小间隔+1秒时钟漂移安全裕度) |
| 级联处理 | 首次触碰时记忆原始时间,因此链式移位为每个节目生成一个干净的清理调用 |
| 过时安全 | 两阶段防护——输入拒绝,加上静默的对齐后回滚 |
| 重置日安全 | 2分钟实时播放缓冲区,加上结转节目保留 |
| 状态 | 已投入生产 |
业务问题:一个不像日历的日历
频道排程看起来像日历软件。运营商希望它表现得像日历一样——将电影拖到晚上9点档,在时间线上向上滑动节目,批量添加一个系列并观看剧集连续排列。但实时频道排程具有日历所没有的约束:
- 节目不允许重叠。直播电视一次只播放一个内容。
- 编码器有最小动作间隔。AWS MediaLive不会以快于每5秒的速度切换输入。如果排程两个节目相隔4秒,部署将被拒绝。
- 过去的节目不能编辑。播出时间已经过去;比特流已在观众屏幕上。
- 跨午夜节目是一个单元。一部从23:30播放到01:15的电影必须作为一个单一节目处理,而不是在日期边界处分割成两个半节目。
两秒的“几乎重叠”不是操作员错误——它是拖动两个几乎但又不完全匹配的节目的自然结果。真正的工程挑战在于将“将电影拖到晚上9点”转化为“合法的频道排程”。如果处理不当,操作员要么在每次拖放时都遇到错误障碍,要么——更糟的是——在播出时才发现编码器静默地拒绝了排程的一部分。
AWS MediaLive 上的“合法”意味着什么
要在 MediaLive 上使排程合法,相邻节目必须是以下情况之一:
- 紧密相连——它们之间没有间隙,或者
- 至少间隔5秒——编码器的最小动作间隔。
陷阱在于两者之间的所有情况。1秒的间隔、3秒的间隔、4.9秒的间隔——所有这些在UI中看起来都完全正常,但它们在部署时都会被拒绝。更糟的是,这种拒绝不是一次干净的原子性失败;它可能导致部分部署的频道,其中一些排程动作成功部署到 MediaLive,而另一些则没有。
在应用服务器和 AWS 之间增加1秒的时钟漂移安全裕度,实际的下限变为6秒,而不是5秒。这个单一的数字——MIN_NEIGHBOUR_GAP_MS = 6000——是整个冲突解决系统所围绕的唯一常量。
为什么显而易见的方法会失败
在确定自动解决之前,曾考虑并拒绝了几种更显而易见的策略:
“拒绝任何不一致并要求操作员解决。”由于这一点,操作员必须在每次拖放时手动进行对齐计算。一个五秒钟的拖动变成了一个五分钟的难题,并且随着播放列表的增长,这个难题会变得越来越难。实际上,操作员会完全放弃拖放,转而使用电子表格。
“将所有内容对齐到5分钟边界,以防止任何重叠。”这通过破坏操作员的意图来解决技术问题。一个原定于21:03:15开始的节目不应悄无声息地跳到21:05:00。排程属于操作员,而不是一个四舍五入函数。
“在部署时而不是拖放时检测冲突。”这在UI中感觉更快,但它将失败推迟到了最糟糕的时刻。当操作员点击“部署”并看到“排程在节目47处被拒绝”时,他们已经从导致该问题的编辑操作中转移了注意力。
“允许UI中存在微小间隙,并让 MediaLive 拒绝它们。”这会将不透明的编码器错误直接推回给操作员,并可能使频道处于一种真正难以恢复的半部署状态。
真正有效的办法是:在拖放时,使用确定性规则自动解决冲突——并立即将修正后的时间线反馈给操作员。
解决方案:一个四规则对齐算法
在每次拖放时,对齐算法会检查受影响频道上每对相邻节目——新节目与新节目、新节目与现有节目,或因拖放而改变了间隔的现有节目与现有节目对——并根据它们之间的间隔应用四条规则中的一条:
- 间隔 = 0 → 无操作。紧密相连是合法的,几乎肯定是操作员的意图。
- 间隔 ≥ 6秒 → 无操作。操作员故意留出了空间,可能是为了插播片头或广告。
- 0 < 间隔 < 6秒 → 将第二个节目向后移位,使间隔缩短至零。
- 负间隔(重叠) → 将第二个节目向前移位,移位量等于重叠量。
至关重要的是,该算法通过对排序后的时间线进行单次正向遍历来处理相邻对。每个被移位的节目会立即成为下一个比较的“前一个”项目——因此,一个触发链式移位的拖放操作可以通过一次线性遍历解决,无需递归。
图1 · 对齐决策树

系统架构
该排程器围绕一组小巧、专注的组件构建:
- NestJS 后端(
schedule.service.ts)负责对齐遍历、级联记忆以及与 MediaLive 的写入契约。 - 时间范围重叠查询。当收到拖放操作时,后端会拉取所有时间窗口触及新批次范围(包括两边各6秒缓冲区)的现有节目。由于此查询操作基于时间范围而非日历日期,跨午夜节目与其他任何节目处理方式相同——系统中不存在特殊的日期边界逻辑。
- 合并时间线。新的节目 DTOs 和查询到的现有节目被合并成一个排序列表。对齐遍历在此合并后的时间线上运行。
shiftedExistings映射。对于任何因移位而被触及的现有节目,此映射会在首次触及时捕获其原始开始和结束时间——并且在后续触及时绝不覆盖。正是这种数据结构保证了多步骤级联的安全性。- Lambda
DELETE_PROGRAM。对于任何已部署到 MediaLive 的移位节目,其原始时间会发送给 Lambda 函数进行清理,然后才更新 MongoDB。 MIN_NEIGHBOUR_GAP_MS = 6000— 每一个规则、每一个重叠查询和每一个安全缓冲区都引用的单一常量。
关键工程决策
1. 四条规则,一次遍历,无特殊情况。同样的四条规则涵盖了操作员可能创建的每种场景:一个新节目被拖放到两个现有节目之间,两个新节目相互冲突,或者一个现有节目被同一次拖放中的早期移位推入重叠状态。所有这些都没有单独的代码路径——每个情况都归结为“检查相邻节目之间的间隔并应用规则”。
2. 六秒,而非五秒。MediaLive 强制执行节目排程动作之间至少5秒的间隔;如果排程两个输入切换相隔4.9秒,将导致部署被拒绝。系统强制执行6秒——这是后端时钟与 AWS 时钟之间时钟漂移的一秒安全裕度。当编码器时钟读取为4.997秒时,如果精确地在5.000秒提交一个动作,会产生间歇性的拒绝,这看起来像网络故障,感觉像无法重现的错误。额外的一秒将间歇性故障模式转化为一种根本不会触发的模式。
这伴随着一个深思熟虑的权衡:6秒的下限意味着3或4秒的小间隔会被对齐为零而不是被保留。这种权衡是故意接受的——MediaLive 上的紧密相连过渡是干净的,而且无论如何,一个可见的3秒间隔在观众看来都像是一个故障。
3. 原始时间记忆使级联安全。一次拖放可以触发一系列移位——节目A移位B,B移位C,C移位D。对 MediaLive 的清理调用必须针对每个节目的原始部署时间,而不是其级联移位后的时间;使用错误的时间会导致 MediaLive 响应“未找到动作”,从而静默地使清理失败。遍历会维护一个 programId → {oldStartTime, oldEndTime} 的映射,在每个节目首次被触及时捕获。后续的级联移位仅更新内存中的时间线;记忆的原始时间保持不变,清理始终使用 MediaLive 实际记录的内容。
图2 · 级联示例

4. 过时防护,分两个独立阶段强制执行。过时编辑被故意阻止两次:
- 阶段0,对齐遍历运行之前:任何开始时间早于“现在”的新节目会立即拒绝整个批次,并伴随明确错误。对齐遍历甚至不会对不可能的输入运行。
- 阶段4,对齐遍历之后:两种不同的子情况被区别对待。一个新节目如果被对齐意外地拉到过去(这种情况很少见,但在请求时钟边界处可能发生),则会静默回滚到其原始的、对齐前的时间——操作员的意图得以保留,对齐简单地不被应用。而一个移位会将现有节目推到过去的情况,则会拒绝整个批次——触及一个已经开始播出的节目绝不是系统会静默吸收的。
5. Lambda 优先,数据库其次的写入契约。当对齐移位了一个已部署到 MediaLive 的节目时,MongoDB 和 MediaLive 会短暂地不同步,并且协调的顺序很重要。契约是:Lambda 优先,MongoDB 其次。后端使用节目的原始时间在 Lambda 上调用 DELETE_PROGRAM;如果任何调用失败,后端会在任何数据库写入发生之前抛出错误。只有当每个删除调用都成功后,才会进行一次 bulkWrite 更新 MongoDB 中的新时间并重置 isDeployed: false。
这产生了一个清晰的不变式:如果 MongoDB 显示节目在新时间,MediaLive 已经接受了该移动。如果操作员看到错误,则两个系统都没有被触及。不可能存在 MongoDB 和 MediaLive 在节目时间上静默不一致的状态。
6. 重置日有其专属安全网。“重置日”会删除指定日历日上频道的所有节目——这是系统中最具破坏性的单一操作——因此它带有两项特殊保护。
- 一个 2分钟缓冲区(
SAFETY_BUFFER_MS = 120000)豁免任何在未来两分钟内开始的节目,为实时播放提供了一个宽限窗口,因此重置永远不会与即将播出的节目竞争。 - 结转节目保留排除在前一天开始但溢出到今天的节目——这些节目属于昨天的排程,而不是今天的。
还提供了一种优雅的回退机制:如果一个频道从未部署到 MediaLive,Lambda 会返回一个后端能识别的特定错误字符串,后端会将其记录为 no_infrastructure,然后只执行 MongoDB 的软删除。重置仍然成功;AWS 步骤只是一个空操作。
为何选择这种设计组合
| 决策 | 原因 | 考虑的替代方案 | 接受的权衡 |
|---|---|---|---|
| 在拖放时自动解决 vs. 拒绝并询问 | 使拖放在大规模使用时可用;手动对齐计算无法应对不断增长的播放列表 | 冲突时拒绝,要求操作员修复 | 要求系统而非操作员来保证正确性 |
| 6秒下限 vs. MediaLive 规定的5秒最小值 | 吸收后端与 AWS 之间的时钟漂移,防止间歇性部署失败 | 严格执行5秒 | 小的(3-4秒)有意留出的间隔会被对齐为零而不是被保留 |
| 单次正向遍历 vs. 递归冲突解决 | 级联确定性地解决,无需担心递归深度 | 递归移位和重新检查 | 需要预先仔细排序时间线 |
| Lambda 优先 / 数据库其次 vs. 数据库优先 / Lambda 其次 | 保证 MongoDB 和 MediaLive 永远不会静默不一致 | 乐观地更新 MongoDB,之后再同步 MediaLive | 每个移位并部署的节目略高的延迟,以换取零漂移风险 |
仍在观察中的问题
诚实的工程实践意味着指出仍然存在的问题,而不仅仅是已解决的问题。
- 同一频道上的并发编辑。如果两名操作员在几百毫秒内点击同一频道上的“部署”,两者都将加载相同的快照,都将独立运行对齐遍历,并且都将写入 MongoDB。目前没有按频道锁或乐观版本检查。当前的缓解措施是操作层面的——一次一名操作员拥有一个频道——而技术修复方案,即在写入时检查频道文档上的版本字段,已在规划中。
- UI 中没有关于对齐结果的反馈。当遍历将一个节目移位三秒时,操作员的视图会刷新到修正后的状态,但尚未显示什么移动了以及为什么。数据已存在于响应有效负载中;计划在排程器 UI 的下一个迭代中添加 toast 通知、侧边栏或差异视图。
成果
- 操作员可以将节目拖放到时间线上的任何位置,系统会在一次确定性遍历中使生成的排程合法——没有冲突模态框,没有错误障碍,无需手动对齐计算。
- 一个单一的四规则算法涵盖了所有情况——新节目与新节目、新节目与现有节目,以及跨日期边界的级联移位——无需对任何一种情况进行特殊处理。
- 跨午夜节目和夏令时(DST)转换与其他任何情况一样,都通过相同的时间范围重叠查询进行处理;不存在需要维护的单独“午夜代码路径”。
- 重置日不会意外地将直播节目中断——2分钟缓冲区和结转节目保留适用于每个频道,在任何时候。
- Lambda 优先 / 数据库其次的契约使得静默排程漂移不可能发生:MongoDB 和 MediaLive 保证一致,否则操作员会看到明确的错误。
MIN_NEIGHBOUR_GAP_MS是唯一的调节旋钮。每个安全裕度、每个对齐规则和每个重叠窗口都引用它,因此调整平台对“合法”的定义只需修改一行代码。
最终思考
这个系统最有趣的地方不是任何单一规则,而是它所需的规则之少。对间隔值的四个条件,通过一次正向遍历应用,涵盖了操作员可能创建的每一个冲突,包括跨午夜边界的多步骤级联。这是一个深思熟虑的设计结果:复杂性被推向一次性地把规则制定正确,而不是处理一个不断增长的特殊情况列表。
更广泛的教训超越了排程软件:当一个系统具有严格的外部约束时——编码器的最小间隔、数据库的一致性保证、无法取消播出的直播——最安全的做法是将这些约束强制实施在一小组确定性且一致应用的规则中,而不是分散在代码库中的临时处理。当两个记录系统(此处是 MongoDB 和 MediaLive)必须保持同步时,精心安排写入顺序,使失败始终能让它们处于已知且一致的状态,是值得为此付出额外延迟的。
关于 MicrocosmWorks
在 MicrocosmWorks,我们为解决复杂工程问题的组织构建生产级软件。
我们的专长包括 AI 应用、SaaS 平台、企业软件、云原生系统、媒体技术和定制后端架构。
通过我们的工程博客,我们分享从设计和运营真实生产系统中获得的实践经验。
继续阅读
如果你喜欢这篇文章,你可能还会发现以下主题有用:

