Якщо ви запитаєте будь-якого оператора прямого ефіру, що б вони хотіли бачити в інструменті планування, вони майже незмінно скажуть: «Дозвольте мені створювати канал так само, як я організовую файли в папці. Перетягнув програму, кинув її туди, куди хочу, і готово».
Проблема в тому, що розклад каналу — це не папка. Це юридичний документ із жорсткими обмеженнями. Дві програми не можуть відтворюватися одночасно. Кодер не може перемикати входи швидше ніж кожні п'ять секунд. Програму, яка вже почалася, не можна редагувати. А фільм, який триває після опівночі, має розглядатися як одна програма, а не розрізана на межі дня.
Кожна дія перетягування є потенційним порушенням обмежень, яке чекає свого часу. Це інженерна історія про те, як планувальник mStudio перетворює зручне для оператора перетягування на гарантовано легальний часовий графік каналу на AWS MediaLive — включно з моментом, коли оператор кидає програму прямо поверх іншої — і чому система вирішує ці конфлікти автоматично, замість того щоб видавати оператору помилку та головоломку для вирішення.
Короткий огляд
| Аспект | Деталі |
|---|---|
| Домен | Планування за допомогою перетягування для живих FAST каналів на AWS MediaLive |
| Вирішення конфліктів | Автоматичне зміщення за допомогою 4-правильного алгоритму прив'язки |
| Мінімальний проміжок між сусідами | 6 секунд (мінімум MediaLive 5с + 1с безпечний запас на дрейф годинника) |
| Обробка каскадів | Оригінальний час запам'ятовується при першому дотику, тому ланцюгові зміщення створюють один чистий виклик очищення для кожної програми |
| Безпека від минулих дат | Двофазний захист — відхилення вхідних даних, плюс безмовне повернення після прив'язки |
| Безпека скидання дня | 2-хвилинний буфер живого відтворення, плюс збереження перенесення |
| Статус | У виробництві |
Бізнес-проблема: Календар, який не є календарем
Планування каналів виглядає як календарне програмне забезпечення. Оператори очікують, що воно буде поводитися як таке — перетягнути фільм у слот 21:00, посунути програми вгору по часовій шкалі, пакетно додати серіал і спостерігати, як епізоди розташовуються один за одним. Але розклад прямого ефіру має обмеження, яких просто немає у календаря:
- Програми не можуть перетинатися. Прямий ефір відтворює лише одну річ одночасно.
- Кодер має мінімальний інтервал між діями. AWS MediaLive не перемикатиме входи швидше ніж кожні 5 секунд. Заплануйте дві програми з інтервалом 4 секунди, і розгортання буде відхилено.
- Минулі програми не можна редагувати. Час ефіру вже минув; біти вже на екранах глядачів.
- Програми, що тривають через опівніч, є однією одиницею. Фільм, що триває з 23:30 до 01:15, має оброблятися як одна програма, а не дві напівпрограми, розділені на межі дня.
«Майже перекриття» в дві секунди — це не помилка оператора, це природний результат перетягування двох програм, які майже, але не зовсім, підходять одна до одної. Справжня інженерна проблема полягає в перетворенні «перетягнути фільм на 21:00» на «легальний розклад каналу». Якщо це зробити неправильно, оператор або натрапляє на стіну помилок при кожному перетягуванні, або — що гірше — виявляє під час ефіру, що кодер тихо відхилив частину розкладу.
Що означає «легальний» на AWS MediaLive
Щоб розклад був легальним на MediaLive, сусідні програми повинні бути або:
- Одна за одною — нульовий проміжок між ними, або
- Розділені щонайменше 5 секундами — мінімальний інтервал між діями кодера.
Пастка — це все, що між ними. 1-секундний проміжок, 3-секундний проміжок, 4,9-секундний проміжок — усі вони виглядають абсолютно нормально в UI, і всі вони відхиляються під час розгортання. Гірше того, відхилення не є чистим, атомарним збоєм; воно може призвести до частково розгорнутого каналу, де деякі дії розкладу були застосовані на MediaLive, а інші — ні.
Додайте 1-секундний запас безпеки для дрейфу годинника між серверами додатків і AWS, і практична нижня межа стає 6 секунд, а не 5. Це єдине число — MIN_NEIGHBOUR_GAP_MS = 6000 — є сталою величиною, навколо якої побудована вся система вирішення конфліктів.
Чому очевидні підходи зазнають невдачі
Перш ніж зупинитися на автоматичному вирішенні, було розглянуто та відхилено кілька більш очевидних стратегій:
«Відхиляти будь-яку невідповідність і вимагати від оператора її вирішення». Через це операторам доводиться вручну виконувати математичні розрахунки прив'язки при кожному перетягуванні. П'ятисекундне перетягування перетворюється на п'ятихвилинну головоломку, і головоломка ускладнюється зі зростанням плейлиста. На практиці оператори повністю відмовляються від перетягування та повертаються до електронних таблиць.
«Прив'язати все до 5-хвилинних меж, щоб ніколи нічого не перекривалося». Це вирішує технічну проблему, руйнуючи намір оператора. Програма, яка мала початися о 21:03:15, не повинна тихо перестрибувати на 21:05:00. Розклад належить оператору, а не функції округлення.
«Виявляти конфлікти під час розгортання, а не під час перетягування». Це здається швидшим в UI, але переносить збій на найгірший можливий момент. До того часу, як оператор натискає «Розгорнути» і бачить «розклад відхилено на програмі 47», він вже подумки відійшов від редагування, яке це спричинило.
«Дозволити мікро-проміжки в UI і дозволити MediaLive їх відхиляти». Це повертає непрозорі помилки кодера прямо оператору і може залишити канал у напіврозгорнутому стані, з якого дійсно важко відновитися.
Важелем, який дійсно спрацював, було автоматичне вирішення конфліктів під час перетягування за допомогою детермінованих правил — і негайне відображення виправленої часової шкали оператору.
Рішення: Чотириправильний алгоритм прив'язки
При кожному перетягуванні алгоритм прив'язки перевіряє кожну пару сусідніх програм на відповідному каналі — нові-до-нових, нові-до-існуючих або існуючі-до-існуючих пари, чий проміжок змінився через перетягування — і застосовує лише одне з чотирьох правил на основі проміжку між ними:
- Проміжок = 0 → без дії. Програма за програмою є легальним, і майже напевно це те, що мав на увазі оператор.
- Проміжок ≥ 6 секунд → без дії. Оператор навмисно залишив місце, ймовірно, для заставки або рекламного блоку.
- 0 < проміжок < 6 секунд → змістити другу програму назад, щоб закрити проміжок до нуля.
- Негативний проміжок (перекриття) → змістити другу програму вперед на величину перекриття.
Важливо, що алгоритм обробляє сусідні пари одним прямим проходом по відсортованій часовій шкалі. Кожна зміщена програма негайно стає «попереднім» елементом для наступного порівняння — таким чином, перетягування, яке викликає ланцюгову реакцію зміщень, вирішується за один лінійний прохід, без необхідності рекурсії.
Діаграма 1 · Дерево рішень прив'язки

Архітектура системи
Планувальник побудований навколо невеликого, цілеспрямованого набору компонентів:
- NestJS backend (
schedule.service.ts) керує проходом прив'язки, кешуванням каскадів та контрактом запису з MediaLive. - Запит на перекриття часових діапазонів. Коли надходить перетягування, backend витягує всі існуючі програми, чиє часове вікно торкається діапазону нового пакета, плюс 6-секундний буфер з кожного боку. Оскільки цей запит працює з часовими діапазонами, а не календарними датами, програми, що тривають через опівніч, обробляються ідентично будь-яким іншим програмам — у системі немає спеціальної логіки для граничних дат.
- Об'єднана часова шкала. Нові DTO програм та запитані існуючі програми об'єднуються в один відсортований список. Прохід прив'язки виконується по цій об'єднаній часовій шкалі.
- Карта
shiftedExistings. Для будь-якої існуючої програми, до якої доторкнулося зміщення, ця карта захоплює її початковий час початку та кінця при першому дотику — і ніколи не перезаписує їх при наступних дотиках. Ця єдина структура даних робить багатоступеневі каскади безпечними. - Lambda
DELETE_PROGRAM. Для будь-якої зміщеної програми, яка вже була розгорнута в MediaLive, її оригінальні часи надсилаються функції Lambda для очищення, перш ніж MongoDB буде оновлено. MIN_NEIGHBOUR_GAP_MS = 6000— єдина константа, на яку посилається кожне правило, кожен запит на перекриття та кожен буфер безпеки.
Ключові інженерні рішення
1. Чотири правила, один прохід, без особливих випадків. Ті самі чотири правила охоплюють кожен сценарій, який може створити оператор: нова програма, скинута між двома існуючими, дві нові програми, які конфліктують між собою, або існуюча програма, яка виштовхується в перекриття попереднім зсувом в тому ж перетягуванні. Немає окремого коду для будь-якого з цих випадків — кожен випадок зводиться до «вивчити проміжок між сусідніми програмами та застосувати правило».
2. Шість секунд, а не п'ять. MediaLive вимагає мінімум 5 секунд інтервалу між діями за розкладом; планування двох перемикань вхідних даних з інтервалом 4,9 секунди призводить до відхилення розгортання. Система вимагає 6 секунд — одностережний запас безпеки для дрейфу годинника між годинником backend'а та AWS. Надсилання дії рівно через 5,000 секунд, коли годинник кодера показує 4,997 секунд, призводить до періодичних відхилень, які виглядають як мережеві збої та відчуваються як неповторні помилки. Додаткова секунда перетворює режим періодичних збоїв на той, який просто ніколи не спрацьовує.
Це має свідомий компроміс: 6-секундна нижня межа означає, що невеликі проміжки (3 або 4 секунди) між програмами закриваються до нуля замість збереження. Цей компроміс був прийнятий свідомо — переходи без проміжків є чистими на MediaLive, а видимий 3-секундний проміжок, як правило, виглядає для глядачів як збій незалежно від обставин.
3. Кешування оригінального часу робить каскади безпечними. Одне перетягування може викликати ланцюг зміщень — програма А зміщує В, В зміщує С, С зміщує D. Виклик очищення до MediaLive повинен стосуватися оригінального розгорнутого часу кожної програми, а не її каскадно-зміщеного часу; використання неправильного часу призводить до того, що MediaLive відповідає «дія не знайдена», безмовно скасовуючи очищення. Прохід підтримує карту programId → {oldStartTime, oldEndTime}, захоплену при першому дотику до кожної програми. Пізніші каскадні зміщення оновлюють лише часову шкалу в пам'яті; кешовані оригінали залишаються недоторканими, і очищення завжди використовує саме те, що MediaLive насправді має в записі.
Діаграма 2 · Приклад каскаду

4. Захист від минулих дат, застосовується в двох окремих фазах. Редагування минулих дат блокується двічі, свідомо:
- Фаза 0, до запуску проходу прив'язки: будь-яка нова програма з часом початку раніше, ніж «зараз», повністю відхиляє весь пакет, з чіткою помилкою. Прохід прив'язки навіть не запускається для неможливих вхідних даних.
- Фаза 4, після проходу прив'язки: два окремі під-випадки обробляються по-різному. Нова програма, яку прив'язка випадково перетягнула в минуле (рідко, але можливо на межах годинника запиту), безмовно повертається до свого оригінального, до-прив'язкового часу — намір оператора зберігається, і прив'язка просто не застосовується. Існуюча програма, яку зсув штовхнув би в минуле, натомість відхиляє весь пакет — торкатися програми, яка вже почала транслюватися, система ніколи не буде безмовно поглинати.
5. Контракт запису: спочатку Lambda, потім база даних. Коли прив'язка зміщує програму, яка вже була розгорнута в MediaLive, MongoDB і MediaLive короткочасно не синхронізовані, і порядок узгодження має значення. Контракт: спочатку Lambda, потім MongoDB. Backend викликає DELETE_PROGRAM на Lambda, використовуючи оригінальні часи програми; якщо будь-який виклик завершується невдачею, backend видає помилку до того, як відбудеться будь-який запис у базу даних. Лише після успішного виконання кожного виклику видалення, один bulkWrite оновлює MongoDB новими часами та скидає isDeployed: false.
Це дає один чистий інваріант: якщо MongoDB показує програму в новий час, MediaLive вже прийняв цей крок. Якщо оператор бачить помилку, жодна система не була змінена. Не існує можливого стану, коли MongoDB та MediaLive безмовно не погоджуються щодо часу програми.
6. День скидання має свою власну спеціальну мережу безпеки. «День скидання» видаляє всі програми на каналі за заданий календарний день — це найбільш руйнівна операція в системі — тому вона має два специфічні захисти.
- 2-хвилинний буфер (
SAFETY_BUFFER_MS = 120000) звільняє будь-яку програму, що починається протягом наступних двох хвилин, надаючи живому відтворенню пільгове вікно, щоб скидання ніколи не могло конкурувати з програмою, яка ось-ось вийде в ефір. - Збереження перенесення виключає програми, які почалися попереднього дня, але переходять на сьогодні — вони належать до вчорашнього розкладу, а не до сьогоднішнього.
Також доступний коректний варіант: якщо канал взагалі ніколи не розгортався в MediaLive, Lambda повертає певну рядок помилки, яку backend розуміє, реєструє як `no_infrastructure`, а потім виконує м'яке видалення тільки в MongoDB. Скидання все одно відбувається успішно; крок AWS просто стає бездіяльним.
Чому саме таке поєднання дизайнерських рішень
| Рішення | Чому було прийнято | Розглянута альтернатива | Прийнятий компроміс |
|---|---|---|---|
| Автоматичне вирішення під час перетягування проти відхилення та запиту | Зберігає зручність використання перетягування в масштабі; ручна математика прив'язки не витримує зростаючого плейлиста | Відхилити при конфлікті, попросити оператора виправити | Вимагає від системи, а не від оператора, гарантії коректності |
| 6-секундна нижня межа проти заявлених MediaLive 5-секундного мінімуму | Поглинає дрейф годинника між backend'ом та AWS, запобігаючи періодичним збоям розгортання | Вимагати рівно 5 секунд | Малі (3–4с) навмисні проміжки прив'язуються до нуля замість збереження |
| Одиночний прямий прохід проти рекурсивного вирішення конфліктів | Каскади вирішуються детерміністично без проблем з глибиною рекурсії | Рекурсивне зміщення та повторна перевірка | Вимагає ретельного попереднього упорядкування відсортованої часової шкали |
| Спочатку Lambda / потім DB проти Спочатку DB / потім Lambda | Гарантує, що MongoDB та MediaLive ніколи не зможуть мовчазно не погоджуватися | Оновити MongoDB оптимістично, потім синхронізувати MediaLive | Трохи більша затримка на кожну зміщену та розгорнуту програму в обмін на нульовий ризик дрейфу |
Що ще під наглядом
Чесна інженерія означає називання відкритих прогалин, а не лише тих, що вже вирішені.
- Одночасні редагування на одному каналі. Якщо два оператори натискають Deploy на одному каналі з інтервалом у кілька сотень мілісекунд, обидва завантажать той самий знімок, обидва незалежно запустять прохід прив'язки і обидва запишуть до MongoDB. Наразі немає блокування для кожного каналу або оптимістичної перевірки версії. Поточне пом'якшення полягає в операційному підході — один оператор володіє одним каналом одночасно — тоді як технічне виправлення, поле версії в документі каналу, що перевіряється під час запису, знаходиться в дорожній карті.
- Відсутність зворотного зв'язку в UI про те, що було прив'язано. Коли прохід зміщує програму на три секунди, відображення оператора оновлюється до виправленого стану, але ще не показує, що перемістилося і чому. Дані вже існують у відповіді; «тост», бічна панель або подання відмінностей заплановані на наступну ітерацію UI планувальника.
Результати
- Оператори можуть перетягувати програму будь-куди на часовій шкалі, і система робить отриманий розклад легальним за один детермінований прохід — без конфліктних вікон, без стін помилок, без ручних розрахунків прив'язки.
- Єдиний чотириправильний алгоритм охоплює кожен випадок — нове-проти-нового, нове-проти-існуючого та каскадні зміщення через межі дня — без особливого оброблення будь-якого з них.
- Програми, що тривають через опівніч, та переходи на літній час проходять через той самий запит на перекриття часових діапазонів, що й будь-який інший випадок; не існує окремого «кодового шляху опівночі» для підтримки.
- День скидання не може випадково зняти програму з ефіру — 2-хвилинний буфер і збереження перенесення застосовуються до кожного каналу, щоразу.
- Контракт Lambda-first / database-second унеможливлює мовчазне зміщення розкладу: MongoDB та MediaLive гарантовано погоджуються, або оператор бачить явну помилку.
MIN_NEIGHBOUR_GAP_MSє єдиним регульованим параметром. Кожен запас безпеки, кожне правило прив'язки та кожне вікно перекриття посилаються на нього, тому зміна визначення «легального» для платформи — це зміна одного рядка.
Завершальні думки
Найцікавіше в цій системі — не якесь одне правило, а те, як мало правил знадобилося. Чотири умови щодо значення проміжку, застосовані за один прямий прохід, охоплюють кожен конфлікт, який може створити оператор, включно з багатоступеневими каскадами через межі опівночі. Це свідомий результат дизайну: складність була вкладена в одноразове правильне визначення правил, а не в обробку постійно зростаючого списку особливих випадків.
Ширший урок виходить за рамки програмного забезпечення для планування: коли система має жорсткі зовнішні обмеження — мінімальний інтервал кодера, гарантії узгодженості бази даних, пряма трансляція, яку не можна «розвішати» — найбезпечніше місце для застосування цих обмежень — це невелика кількість детермінованих правил, що застосовуються послідовно, а не спеціальна обробка, розкидана по всьому коду. І коли дві системи записів (тут MongoDB та MediaLive) повинні залишатися синхронними, упорядкування записів таким чином, щоб збій завжди залишав їх у відомому, узгодженому стані, варте додаткової затримки, яку це коштує.
Про MicrocosmWorks
У MicrocosmWorks ми створюємо програмне забезпечення промислового класу для організацій, що вирішують складні інженерні проблеми.
Наша експертиза включає AI applications, SaaS platforms, enterprise software, cloud-native systems, media technology, and custom backend architecture.
Через наш інженерний блог ми ділимося практичними уроками, отриманими під час розробки та експлуатації реальних виробничих систем.
Продовжити читання
Якщо вам сподобалася ця стаття, вам також можуть бути корисні такі теми:

