ライブTVのオペレーターに、スケジューリングツールに何を求めるか尋ねると、ほとんどの場合「フォルダ内のファイルを整理するように、チャンネルを構築させてほしい。番組をドラッグして、好きな場所にドロップすれば完了だ」と答えるでしょう。
問題は、チャンネルのスケジュールはフォルダではないという点です。それは厳格な制約を持つ法的文書です。2つの番組が同時に再生されることはできません。エンコーダーは5秒よりも速く入力を切り替えることはできません。すでに開始された番組は編集できません。そして、深夜を過ぎて放送される映画は、日替わり境界で分割されるのではなく、1つの番組として扱われる必要があります。
あらゆるドラッグ&ドロップ操作は、発生を待つ潜在的な制約違反です。これは、mStudioのスケジューラーが、オペレーターに優しいドラッグ&ドロップを、オペレーターが別の番組の真上に番組をドロップする瞬間を含め、AWS MediaLive上で合法的なチャンネルタイムラインにどのように変えるか、そしてなぜシステムがオペレーターにエラーと解決すべきパズルを提示するのではなく、それらの競合を自動的に解決するのか、というエンジニアリングストーリーです。
クイック概要
| 項目 | 詳細 |
|---|---|
| ドメイン | AWS MediaLive上でのライブFASTチャンネル向けドラッグ&ドロップスケジューリング |
| 競合解決 | 4つのルールによるスナップアルゴリズムを介した自動シフト |
| 最小隣接間隔 | 6秒(MediaLiveの最小5秒 + 1秒のクロックドリフト安全マージン) |
| カスケード処理 | 初回タッチ時に元の時刻をメモ化することで、連鎖的なシフトでも番組ごとに1回のクリーンなクリーンアップ呼び出しを生成 |
| 過去日安全性 | 2段階のガード — 入力拒否、およびスナップ後のサイレントな復元 |
| リセット日安全性 | 2分間のライブ再生バッファ、および繰り越し保持 |
| ステータス | 本番稼働中 |
ビジネス上の問題:カレンダーではないカレンダー
チャンネルのスケジューリングはカレンダーソフトウェアのように見えます。オペレーターはそれをカレンダーのように動作することを期待します。映画を午後9時の枠にドラッグし、番組をタイムライン上で移動させ、シリーズを一括で追加してエピソードが連続して並ぶのを見る、といった具合です。しかし、ライブチャンネルのスケジュールには、カレンダーにはない制約があります。
- 番組の重複は許可されません。ライブTVは一度に厳密に一つのものを再生します。
- エンコーダーには最小アクション間隔があります。 AWS MediaLiveは5秒よりも速く入力を切り替えません。2つの番組を4秒間隔でスケジュールすると、デプロイは拒否されます。
- 過去の番組は編集できません。放送時間はすでに過ぎており、ビットはすでに視聴者の画面に表示されています。
- 深夜をまたぐ番組は1つの単位です。23:30から01:15まで放送される映画は、日替わり境界で分割された2つの半番組ではなく、1つの番組として扱われる必要があります。
2秒の「ほぼ重複」はオペレーターのエラーではありません。それは、ほぼぴったり合うものの、完全には合わない2つの番組をドラッグした自然な結果です。本当のエンジニアリングの課題は、「映画を午後9時にドラッグする」という操作を「合法的なチャンネルスケジュール」に変換することです。これを間違えると、オペレーターはドロップするたびにエラーに遭遇するか、さらに悪いことに、放送時にエンコーダーがスケジュールの一部をサイレントに拒否していたことを発見することになります。
AWS MediaLiveにおける「合法」の意味
MediaLive上でスケジュールが合法であるためには、隣接する番組は次のいずれかの状態である必要があります。
- 連続 — 間にギャップがない、または
- 少なくとも5秒以上の間隔 — エンコーダーの最小アクション間隔です。
落とし穴は、その間のあらゆるケースです。1秒のギャップ、3秒のギャップ、4.9秒のギャップ — これらすべてはUI上では全く問題ないように見えますが、デプロイ時にはすべて拒否されます。さらに悪いことに、この拒否はクリーンでアトミックな失敗ではなく、一部のスケジュールアクションがMediaLiveにデプロイされ、他がデプロイされなかった、部分的にデプロイされたチャンネルになる可能性があります。
アプリケーションサーバーとAWS間のクロックドリフトのために1秒の安全マージンを追加すると、実用的な最小値は5秒ではなく6秒になります。この単一の数値 — MIN_NEIGHBOUR_GAP_MS = 6000 — が、競合解決システム全体が構築されている唯一の定数です。
なぜ明白なアプローチは失敗するのか
自動解決に落ち着く前に、いくつかのより明白な戦略が検討され、却下されました。
「不一致を拒否し、オペレーターに解決を要求する。」これにより、オペレーターはドロップするたびに手動でスナップ計算を行う必要があります。5秒のドラッグが5分間のパズルになり、プレイリストが長くなるにつれてパズルは難しくなります。実際には、オペレーターはドラッグ&ドロップを完全に諦め、スプレッドシートに戻ってしまいます。
「すべてを5分単位の境界にスナップし、何も重複しないようにする。」これは、オペレーターの意図を破壊することで技術的な問題を解決します。21:03:15に開始される予定の番組が、サイレントに21:05:00にジャンプするべきではありません。スケジュールはオペレーターのものであり、丸め関数のものではありません。
「ドロップ時ではなく、デプロイ時に競合を検出する。」これはUI上では速く感じられますが、失敗を最悪のタイミングに移動させます。オペレーターがデプロイをクリックして「番組47でスケジュールが拒否されました」と表示される頃には、原因となった編集からはすでに意識が離れています。
「UIで微小なギャップを許可し、MediaLiveに拒否させる。」これは、不透明なエンコーダーエラーをオペレーターに直接押し戻し、チャンネルを回復が非常に困難な半デプロイ状態のままにする可能性があります。
実際に機能した手段は、確定的なルールを使用して、ドロップ時に競合を自動的に解決し、修正されたタイムラインを即座にオペレーターに反映させることでした。
解決策:4つのルールによるスナップアルゴリズム
ドロップが発生するたびに、スナップアルゴリズムは、影響を受けるチャンネル上の隣接する番組のペア(新規-新規、新規-既存、またはドロップによってギャップが変更された既存-既存のペア)を検査し、それらの間のギャップに基づいて4つのルールのうちの1つだけを適用します。
- ギャップ = 0 → 何もしない。連続は合法であり、ほとんどの場合オペレーターの意図通りです。
- ギャップ ≥ 6秒 → 何もしない。オペレーターは意図的にスペースを残しており、おそらくスレートまたは広告ポッドのためでしょう。
- 0 < ギャップ < 6秒 → 2番目の番組を後方にシフトして、ギャップをゼロにする。
- 負のギャップ(重複) → 2番目の番組を重複量だけ前方にシフトする。
重要なのは、アルゴリズムがソートされたタイムライン上で単一の順方向パスで隣接するペアを処理することです。シフトされた各番組は、次の比較のためにすぐに「前の」項目となるため、連鎖的なシフトを引き起こすドロップは、再帰を必要とせず、1回の線形ウォークで解決されます。
図1 · スナップ決定ツリー

システムアーキテクチャ
スケジューラーは、小規模で特化したコンポーネントセットを中心に構築されています。
- NestJSバックエンド (
schedule.service.ts) がスナップウォーク、カスケードメモ化、およびMediaLiveとの書き込み契約を所有しています。 - 時間範囲重複クエリ。ドロップが到着すると、バックエンドは新しいバッチの範囲に触れるすべての既存番組のタイムウィンドウと、両側に6秒のバッファを取得します。このクエリはカレンダーの日付ではなく時間範囲で動作するため、深夜をまたぐ番組は他のどの番組とも同様に処理されます。システム内のどこにも特別な日付境界ロジックは存在しません。
- マージされたタイムライン。新しい番組のDTOとクエリされた既存の番組は、単一のソートされたリストにマージされます。スナップウォークはこの結合されたタイムラインに対して実行されます。
shiftedExistingsマップ。シフトによって触れられた既存の番組について、このマップは初回タッチ時にその元の開始時刻と終了時刻を捕捉し、その後のタッチでそれらを上書きすることはありません。この単一のデータ構造が多段階のカスケードを安全にします。- Lambda
DELETE_PROGRAM。MediaLiveにすでにデプロイされていたシフトされた番組については、その元の時刻がLambda関数に送られ、MongoDBが更新される前にクリーンアップされます。 MIN_NEIGHBOUR_GAP_MS = 6000— すべてのルール、すべての重複クエリ、およびすべての安全バッファが参照する単一の定数。
主要なエンジニアリング決定
1. 4つのルール、1回のウォーク、特殊ケースなし。同じ4つのルールが、オペレーターが作成しうるすべてのシナリオをカバーします。既存の2つの番組の間にドロップされた新しい番組、互いに競合する2つの新しい番組、または同じドロップ内で以前のシフトによって重複に押し込まれた既存の番組などです。これらのどれにも個別のコードパスはなく、すべてのケースは「隣接する番組間のギャップを調べてルールを適用する」に集約されます。
2. 5秒ではなく6秒。MediaLiveはスケジュールアクション間に5秒の最小間隔を強制します。2つの入力切り替えを4.9秒間隔でスケジュールすると、デプロイが拒否されます。システムは6秒を強制します。これは、バックエンドのクロックとAWSのクロック間のクロックドリフトに対する1秒の安全マージンです。エンコーダーのクロックが4.997秒と読み取る状況で、正確に5.000秒でアクションを送信すると、ネットワーク障害のように見え、再現不可能なバグのように感じる断続的な拒否が発生します。追加の1秒は、断続的な障害モードを、単に発生しないモードに変換します。
これには意図的なトレードオフが伴います。6秒という最小値は、3秒または4秒といった短い連続したギャップが保持されるのではなく、ゼロにスナップされて閉じられることを意味します。このトレードオフは意図的に受け入れられました。MediaLiveでは連続したトランジションはクリーンであり、目に見える3秒のギャップは視聴者にとってはグリッチのように見える傾向があるからです。
3. 元の時刻のメモ化によりカスケードが安全になる。単一のドロップがシフトの連鎖を引き起こすことがあります。プログラムAがBをシフトさせ、BがCをシフトさせ、CがDをシフトさせる、といった具合です。MediaLiveへのクリーンアップ呼び出しは、各番組の元のデプロイ時刻をターゲットにする必要があり、カスケードシフトされた時刻ではありません。誤った時刻を使用すると、MediaLiveは「no action found」と応答し、クリーンアップがサイレントに失敗します。ウォークは、各番組が最初に触れられたときにキャプチャされたprogramId → {oldStartTime, oldEndTime}のマップを維持します。後続のカスケードシフトはメモリ上のタイムラインのみを更新し、メモ化された元の時刻は手付かずのままであり、クリーンアップは常にMediaLiveが実際に記録しているものとまったく同じものを使用します。
図2 · カスケードの例

4. 過去日付ガード、2つの異なるフェーズで強制。過去の日付の編集は、意図的に2回ブロックされます。
- フェーズ0、スナップウォークが実行される前: 「現在」よりも早い開始時刻を持つ新しい番組は、明確なエラーとともにバッチ全体を即座に拒否します。スナップウォークは不可能な入力に対しては実行すらされません。
- フェーズ4、スナップウォーク後: 2つの異なるサブケースが別々に処理されます。スナップによって誤って過去に引き込まれた新しい番組(稀ですが、リクエスト時のクロック境界では可能です)は、元のスナップ前の時刻にサイレントに復元されます。オペレーターの意図は保持され、スナップは単に適用されません。シフトによって過去に押し込まれる既存の番組は、代わりにバッチ全体を拒否します。すでに放送が開始された番組に手を加えることは、システムがサイレントに吸収することはありません。
5. Lambda優先、データベース後書き込み契約。スナップがMediaLiveにすでにデプロイされている番組をシフトする場合、MongoDBとMediaLiveは一時的に同期が取れなくなり、調整の順序が重要になります。契約は、Lambdaが最初、MongoDBが次です。バックエンドは、番組の元の時刻を使用してLambdaのDELETE_PROGRAMを呼び出します。いずれかの呼び出しが失敗した場合、バックエンドはデータベース書き込みが発生する前にエラーをスローします。すべての削除呼び出しが成功した場合にのみ、単一のbulkWriteがMongoDBを新しい時刻で更新し、isDeployed: falseをリセットします。
これにより、1つのクリーンな不変条件が生成されます。MongoDBが番組を新しい時刻で表示している場合、MediaLiveはその移動をすでに受け入れています。オペレーターがエラーを見た場合、どちらのシステムも触られていません。MongoDBとMediaLiveが番組の時刻についてサイレントに不一致になる状態はあり得ません。
6. リセット日には独自のセーフティネットがあります。「リセット日」は、特定のカレンダー日のチャンネル上のすべての番組を削除する、システム内で最も破壊的な操作であるため、2つの特定の保護を備えています。
- 2分間のバッファ (
SAFETY_BUFFER_MS = 120000) は、今後2分以内に開始する番組を除外します。これにより、ライブ再生に猶予期間が与えられ、リセットが放送開始直前の番組と競合することはありません。 - 繰り越し保持は、前日に開始されて今日にまたがる番組を除外します。これらは今日のスケジュールではなく、昨日のスケジュールに属します。
また、優雅なフォールバックも利用できます。チャンネルがMediaLiveに全くデプロイされたことがない場合、Lambdaはバックエンドが理解し、no_infrastructureとしてログに記録する特定のエラーステートメントを返した後、MongoDBのみのソフトデリートを実行します。リセットは引き続き成功し、AWSのステップは単に何もしない操作になります。
これらの設計選択の組み合わせの理由
| 決定 | 理由 | 検討された代替案 | 受け入れられたトレードオフ |
|---|---|---|---|
| ドロップ時自動解決 vs. 拒否して質問 | 大規模なドラッグ&ドロップを使いやすくする。手動のスナップ計算はプレイリストの増加に対応できない | 競合時に拒否し、オペレーターに修正を依頼 | 正確性を保証するのはシステムであり、オペレーターではないことが求められる |
| 6秒の最小値 vs. MediaLiveの公称5秒最小値 | バックエンドとAWS間のクロックドリフトを吸収し、断続的なデプロイ失敗を防ぐ | 厳密に5秒を強制 | 意図的な短いギャップ(3~4秒)は保持されずゼロにスナップされる |
| 単一の順方向パスウォーク vs. 再帰的な競合解決 | 再帰深度の懸念なく、カスケードを決定論的に解決 | 再帰的なシフトと再確認 | 事前にソートされたタイムラインの慎重な順序付けが必要 |
| Lambda優先 / DB後 vs. DB優先 / Lambda後 | MongoDBとMediaLiveがサイレントに不一致になることがないことを保証 | MongoDBを楽観的に更新し、MediaLiveを後で同期 | シフトおよびデプロイされた番組あたりのレイテンシがわずかに高くなるが、ドリフトリスクはゼロになる |
引き続き監視中の課題
誠実なエンジニアリングとは、解決された問題だけでなく、残されているギャップも明確にすることです。
- 同一チャンネルでの同時編集。2人のオペレーターが数百ミリ秒以内に同じチャンネルでデプロイをクリックした場合、両者とも同じスナップショットを読み込み、独立してスナップウォークを実行し、両者ともMongoDBに書き込みます。現在、チャンネルごとのロックや楽観的バージョンチェックはありません。現在の緩和策は運用上のものであり、一度に1人のオペレーターが1つのチャンネルを所有するというものですが、書き込み時にチェックされるチャンネルドキュメントのバージョンフィールドという技術的な修正がロードマップにあります。
- スナップされた内容に関するUI内フィードバックの欠如。ウォークが番組を3秒シフトした場合、オペレーターのビューは修正された状態に更新されますが、何がどのように移動したのかはまだ表示されません。データはすでにレスポンスペイロードに存在しており、トースト、サイドバー、または差分ビューがスケジューラーUIの次期イテレーションで計画されています。
成果
- オペレーターはタイムライン上のどこにでも番組をドロップでき、システムは結果のスケジュールを1回の確定的なパスで合法にします。競合モーダル、エラーの壁、手動でのスナップ計算は不要です。
- 単一の4ルールアルゴリズムが、新規-新規、新規-既存、および日境界を越えるカスケードシフトを含むすべてのケースを、特殊ケースとして扱うことなくカバーします。
- 深夜をまたぐ番組やDST移行は、他のケースと同様に同じ時間範囲重複クエリを通じて処理されます。維持すべき個別の「深夜コードパス」はありません。
- リセット日に誤ってライブ番組が放送停止されることはありません。2分間のバッファと繰り越し保持は、すべてのチャンネルに常に適用されます。
- Lambda優先 / データベース後契約により、サイレントなスケジュールドリフトは不可能です。MongoDBとMediaLiveは常に一致することが保証されるか、オペレーターは明示的なエラーを見ることになります。
MIN_NEIGHBOUR_GAP_MSは調整可能な唯一のつまみです。すべての安全マージン、すべてのスナップルール、およびすべての重複ウィンドウがこれを参照するため、プラットフォームの「合法」の定義を調整するのは1行の変更で済みます。
最後に
このシステムで最も興味深いのは、個々のルールではなく、必要とされたルールの少なさです。ギャップ値に対する4つの条件を1回の順方向パスで適用することで、深夜境界をまたぐ多段階カスケードを含む、オペレーターが作成しうるあらゆる競合をカバーします。これは意図的な設計の結果です。複雑さは、増え続ける特殊ケースのリストを処理するのではなく、ルールを一度正しくすることに集約されました。
より広範な教訓は、スケジューリングソフトウェアを超えて一般化できます。システムが厳格な外部制約(エンコーダーの最小間隔、データベースの整合性保証、放送を停止できないライブ放送など)を持つ場合、それらの制約を強制する最も安全な場所は、コードベース全体に散らばったアドホックな処理ではなく、一貫して適用される少数の決定論的なルールの中にあります。また、2つの記録システム(ここではMongoDBとMediaLive)が同期を保つ必要がある場合、障害が発生しても常に既知の合意状態になるように書き込み順序を定めることは、追加のレイテンシを伴っても価値があります。
MicrocosmWorksについて
MicrocosmWorksでは、複雑なエンジニアリング問題を解決する組織向けに、本番環境レベルのソフトウェアを構築しています。
当社の専門知識には、AIアプリケーション、SaaSプラットフォーム、エンタープライズソフトウェア、クラウドネイティブシステム、メディアテクノロジー、カスタムバックエンドアーキテクチャが含まれます。
私たちのエンジニアリングブログを通じて、実際のプロダクションシステムを設計・運用する中で得られた実践的な教訓を共有しています。
続きをお読みください
この記事を楽しんでいただけたなら、以下のトピックも役立つかもしれません。

