כשאתם שואלים כל מפעיל טלוויזיה בשידור חי מה הוא היה רוצה לראות בכלי תזמון, הם כמעט תמיד יגידו, "תן לי לבנות ערוץ כמו שאני מארגן קבצים בתיקייה. גרור תוכנית, שחרר אותה היכן שאני רוצה, וזהו."
הבעיה היא שלוח זמנים של ערוץ אינו תיקייה. זהו מסמך משפטי עם אילוצים קשיחים. שתי תוכניות אינן יכולות להיות משודרות באותו רגע. ה-encoder אינו יכול להחליף כניסות מהר יותר מחמש שניות. תוכנית שכבר החלה לא ניתנת לעריכה. וסרט שרץ מעבר לחצות צריך להיות מטופל כתוכנית אחת, לא חתוך בגבול היום.
כל פעולת גרירה ושחרור היא הפרת אילוץ פוטנציאלית שמחכה להתרחש. זהו הסיפור ההנדסי כיצד המתזמן של mStudio הופך גרירה ושחרור ידידותיים למפעיל לציר זמן ערוץ חוקי ומובטח ב-AWS MediaLive — כולל הרגע שבו מפעיל משחרר תוכנית ישירות על גבי אחרת — ומדוע המערכת פותרת סכסוכים אלה באופן אוטומטי במקום להגיש למפעיל שגיאה וחידה לפתור.
סקירה מהירה
| היבט | פרט |
|---|---|
| תחום | תזמון גרירה ושחרור עבור ערוצי FAST חיים ב-AWS MediaLive |
| פתרון קונפליקטים | הזזה אוטומטית באמצעות אלגוריתם הצמדה בן 4 כללים |
| פער מינימלי בין שכנים | 6 שניות (מינימום 5 שניות של MediaLive + שניית בטיחות אחת לסחף שעון) |
| טיפול במפל | זמנים מקוריים נשמרים בזיכרון במגע ראשוני, כך שהזזות מדורגות מייצרות קריאת ניקוי נקייה אחת לתוכנית |
| בטיחות תאריך עבר | הגנה דו-שלבית — דחיית קלט, בתוספת החזרה שקטה לאחר הצמדה |
| בטיחות איפוס יום | חלון בטיחות של 2 דקות לשידור חי, בתוספת שימור תוכניות גולשות |
| סטטוס | בייצור |
הבעיה העסקית: לוח שנה שאינו לוח שנה
תזמון ערוצים נראה כמו תוכנת לוח שנה. מפעילים מצפים שהיא תתנהג כזו – גרור סרט למשבצת של 21:00, החלק תוכניות לאורך ציר הזמן, הוסף סדרה בקבוצה וראה את הפרקים מסודרים בזה אחר זה. אבל לוח זמנים של ערוץ חי נושא אילוצים שלוח שנה פשוט אינו מכיל:
- אסור לתוכניות לחפוף. טלוויזיה בשידור חי מציגה בדיוק דבר אחד בכל פעם.
- ל-encoder יש מרווח פעולה מינימלי. AWS MediaLive לא יחליף כניסות מהר יותר מכל 5 שניות. אם תתזמן שתי תוכניות בהפרש של 4 שניות, הפריסה תידחה.
- תוכניות עבר לא ניתנות לעריכה. זמן השידור כבר חלף; הביטים כבר על מסכי הצופים.
- תוכניות חוצות חצות הן יחידה אחת. סרט שרץ מ-23:30 עד 01:15 צריך להיות מטופל כתוכנית אחת, לא שתי חצאי תוכניות מחולקות בגבול היום.
"חפיפה כמעט" של שתי שניות אינה טעות מפעיל – זוהי התוצאה הטבעית של גרירת שתי תוכניות שכמעט, אך לא לגמרי, מתאימות זו לזו. האתגר ההנדסי האמיתי הוא לתרגם "גרור סרט לשעה 21:00" ל"לוח זמנים חוקי של ערוץ". אם טועים, המפעיל ייתקל בקיר שגיאות בכל שחרור, או – גרוע מכך – יגלה בזמן השידור שה-encoder דחה בשקט חלק מלוח הזמנים.
מה המשמעות של "חוקי" ב-AWS MediaLive
כדי שלוח זמנים יהיה חוקי ב-MediaLive, תוכניות סמוכות חייבות להיות אחת משתי האפשרויות הבאות:
- בזה אחר זה — ללא מרווח ביניהן, או
- מופרדות ב-5 שניות לפחות — מרווח הפעולה המינימלי של ה-encoder.
המכשול הוא כל מה שביניהם. מרווח של שנייה אחת, מרווח של 3 שניות, מרווח של 4.9 שניות – כולם נראים בסדר גמור בממשק המשתמש, וכולם נדחים בזמן הפריסה. גרוע מכך, הדחייה אינה כשל נקי ואטומי; היא יכולה לגרום לערוץ שנפרס חלקית, כאשר חלק מפעולות התזמון נחתו ב-MediaLive ואחרות לא.
הוסף מרווח בטיחות של שנייה אחת לסחף שעון בין שרתי היישומים ל-AWS, והסף המעשי הופך ל-6 שניות, לא 5. המספר היחיד הזה – MIN_NEIGHBOUR_GAP_MS = 6000 – הוא הקבוע היחיד שעליו בנויה כל מערכת פתרון הקונפליקטים.
מדוע גישות מובנות מאליהן נכשלות
לפני שהוחלט על פתרון אוטומטי, נשקלו ונדחו מספר אסטרטגיות מובנות מאליהן:
"דחיית כל אי-הסכמה ובקשה מהמפעיל לפתור אותה." בגלל זה, מפעילים חייבים לבצע חישובי הצמדה ידניים בכל שחרור. גרירה של חמש שניות הופכת לחידה של חמש דקות, והחידה נעשית קשה יותר ככל שרשימת ההשמעה גדלה. בפועל, מפעילים נוטשים לחלוטין את הגרירה והשחרור וחוזרים לגיליונות אלקטרוניים.
"הצמד הכל לגבולות של 5 דקות כך ששום דבר לא יחפוף." זה פותר את הבעיה הטכנית על ידי הרס כוונת המפעיל. תוכנית שאמורה להתחיל ב-21:03:15 לא צריכה לקפוץ בשקט ל-21:05:00. לוח הזמנים שייך למפעיל, לא לפונקציית עיגול.
"זיהוי קונפליקטים בזמן הפריסה במקום בזמן השחרור." זה מרגיש מהיר יותר בממשק המשתמש, אבל זה מזיז את הכשל לרגע הגרוע ביותר האפשרי. כשהמפעיל לוחץ על Deploy ורואה "לוח הזמנים נדחה בתוכנית 47", הוא כבר עבר מנטלית מהעריכה שגרמה לכך.
"אפשר מרווחי מיקרו בממשק המשתמש ותן ל-MediaLive לדחות אותם." זה דוחף שגיאות encoder אטומות ישר בחזרה למפעיל, ויכול להשאיר את הערוץ במצב פריסה חלקית שממנו קשה באמת להתאושש.
הפתרון שעבד בפועל היה לפתור קונפליקטים באופן אוטומטי, בזמן השחרור, באמצעות כללים דטרמיניסטיים – ולשקף את ציר הזמן המתוקן בחזרה למפעיל באופן מיידי.
הפתרון: אלגוריתם הצמדה בעל ארבעה כללים
בכל שחרור, אלגוריתם הצמדה בוחן כל זוג תוכניות סמוכות בערוץ המושפע — זוגות חדש-לחדש, חדש-לקיים, או קיים-לקיים שהמרווח ביניהם השתנה בגלל השחרור — ומיישם בדיוק אחד מארבעה כללים בהתבסס על המרווח ביניהם:
- מרווח = 0 ← אין פעולה. שידור בזה אחר זה חוקי, וכמעט בוודאות זו הייתה כוונת המפעיל.
- מרווח ≥ 6 שניות ← אין פעולה. המפעיל השאיר בכוונה מקום, כנראה לשקופית או לבלוק פרסומות.
- 0 < מרווח < 6 שניות ← הזז את התוכנית השנייה אחורה כדי לסגור את המרווח לאפס.
- מרווח שלילי (חפיפה) ← הזז את התוכנית השנייה קדימה בגודל החפיפה.
באופן מכריע, האלגוריתם מעבד זוגות סמוכים במעבר יחיד קדימה על ציר הזמן הממוין. כל תוכנית שהוזזה הופכת מיד לפריט ה"קודם" להשוואה הבאה — כך ששחרור שמפעיל תגובת שרשרת של הזזות נפתר בהליכה ליניארית אחת, ללא צורך ברקורסיה.
תרשים 1 · עץ ההחלטות של ההצמדה

ארכיטקטורת המערכת
המתזמן בנוי סביב קבוצה קטנה וממוקדת של רכיבים:
- NestJS backend (
schedule.service.ts) מחזיק ב"הליכת" ההצמדה (snap walk), בשימור זיכרון המפל (cascade memoization), ובהסכם הכתיבה מול MediaLive. - שאילתת חפיפה בטווחי זמן. כאשר מגיע שחרור, ה-backend מושך כל תוכנית קיימת שחלון הזמן שלה נוגע לטווח הקבוצה החדשה, בתוספת חיץ של 6 שניות מכל צד. מכיוון ששאילתה זו פועלת על טווחי זמן ולא על תאריכי לוח שנה, תוכניות חוצות חצות מטופלות באופן זהה לכל תוכנית אחרת — אין כל לוגיקה מיוחדת לגבולות תאריך במערכת.
- ציר זמן מאוחד. אובייקטי העברת הנתונים (DTOs) של התוכניות החדשות והתוכניות הקיימות שנשאלו מאוחדים לרשימה ממוינת אחת. ה"הליכה" ההצמדה (snap walk) מתבצעת על ציר זמן משולב זה.
- מפת
shiftedExistings. עבור כל תוכנית קיימת שהוזזה, מפה זו לוכדת את זמני ההתחלה והסיום המקוריים שלה בפעם הראשונה שהיא ננגעת — ולעולם אינה דורסת אותם במגעים הבאים. מבנה נתונים זה הוא מה שהופך מפלים מרובי שלבים לבטוחים. - פונקציית Lambda
DELETE_PROGRAM. עבור כל תוכנית שהוזזה וכבר נפרסה ל-MediaLive, זמניה המקוריים נשלחים לפונקציית Lambda לניקוי לפני ש-MongoDB מתעדכן. MIN_NEIGHBOUR_GAP_MS = 6000— הקבוע היחיד שכל כלל, כל שאילתת חפיפה וכל חיץ בטיחות מתייחס אליו.
החלטות הנדסיות מרכזיות
1. ארבעה כללים, מעבר אחד, ללא מקרים מיוחדים. אותם ארבעה כללים מכסים כל תרחיש שמפעיל יכול ליצור: תוכנית חדשה ששוחררה בין שתי תוכניות קיימות, שתי תוכניות חדשות שמתנגשות זו עם זו, או תוכנית קיימת שנדחפה לחפיפה על ידי הזזה מוקדמת יותר באותו שחרור. אין נתיב קוד נפרד לאף אחד מהם — כל מקרה מצטמצם ל"בחן את המרווח בין תוכניות סמוכות ויישם את הכלל".
2. שש שניות, לא חמש. MediaLive אוכף מרווח מינימלי של 5 שניות בין פעולות תזמון; תזמון שתי החלפות קלט בהפרש של 4.9 שניות גורם לדחיית פריסה. המערכת אוכפת 6 שניות — מרווח בטיחות של שנייה אחת לסחף שעון בין שעון ה-backend לזה של AWS. הגשת פעולה בדיוק ב-5.000 שניות, כאשר שעון ה-encoder קורא אותה כ-4.997 שניות, מייצרת דחיות לסירוגין שנראות כמו כשלי רשת ומרגישות כמו באגים בלתי ניתנים לשחזור. השנייה הנוספת הופכת מצב כשל לסירוגין לכזה שלעולם אינו מופעל.
זה מגיע עם פשרה מכוונת: רצפה של 6 שניות פירושה שמרווחים קטנים (3 או 4 שניות) בין תוכניות צמודות נסגרים לאפס במקום להישמר. פשרה זו התקבלה בכוונה — מעברים עוקבים הם נקיים ב-MediaLive, ופער גלוי של 3 שניות נוטה להיראות כהפרעה לצופים בכל מקרה.
3. שמירת זמנים מקוריים בזיכרון הופכת את המפלים לבטוחים. שחרור יחיד יכול להפעיל שרשרת של הזזות — תוכנית A מזיזה את B, B מזיזה את C, C מזיזה את D. קריאת הניקוי ל-MediaLive חייבת לכוון לזמן הפריסה המקורי של כל תוכנית, לא לזמן שהוזז עקב המפל; שימוש בזמן שגוי גורם ל-MediaLive להגיב ב"לא נמצאה פעולה", ובכך ניקוי נכשל בשקט. ה"הליכה" (walk) שומרת מפה של 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 שניות לעומת המינימום המוצהר של 5 שניות של MediaLive | קולטת סחף שעון בין ה-backend ל-AWS, מונעת כשלי פריסה לסירוגין | אכיפת 5 שניות בדיוק | מרווחים מכוונים קטנים (3-4 שניות) נצמדים לאפס במקום להישמר |
| מעבר יחיד קדימה לעומת פתרון קונפליקטים רקורסיבי | מפלים נפתרים באופן דטרמיניסטי ללא חששות מעומק רקורסיה | הזזה ובדיקה חוזרת רקורסיבית | דורש סידור קפדני של ציר הזמן הממוין מראש |
| Lambda ראשונה / DB שני לעומת DB ראשון / Lambda שני | מבטיח ש-MongoDB ו-MediaLive לעולם לא יחלקו בשקט | עדכן את MongoDB באופן אופטימי, סנכרן את MediaLive לאחר מכן | חביון מעט גבוה יותר לכל תוכנית שהוזזה ונפרסה, בתמורה לסיכון סחף אפסי |
מה עדיין נמצא במעקב
הנדסה כנה פירושה לנקוב בשמות הפערים שנותרים פתוחים, לא רק באלה שנפתרו.
- עריכות בו-זמניות באותו ערוץ. אם שני מפעילים לוחצים על Deploy באותו ערוץ בתוך כמה מאות מילישניות זה מזה, שניהם יטענו את אותו תצלום, שניהם יריצו את "הליכת" ההצמדה באופן עצמאי, ושניהם יכתבו ל-MongoDB. אין היום נעילה לכל ערוץ או בדיקת גרסה אופטימית. הגידור הנוכחי הוא תפעולי — מפעיל אחד מחזיק בערוץ אחד בכל פעם — בעוד שהתיקון הטכני, שדה גרסה במסמך הערוץ הנבדק בזמן הכתיבה, נמצא במפת הדרכים.
- אין משוב בממשק המשתמש לגבי מה שהוצמד. כאשר ה"הליכה" (walk) מזיזה תוכנית בשלוש שניות, תצוגת המפעיל מתרעננת למצב המתוקן, אך עדיין אינה מציגה מה זז ומדוע. הנתונים כבר קיימים במטען התגובה; התראה (toast), סרגל צד, או תצוגת הבדלים מתוכננים לאיטרציה הבאה של ממשק המשתמש של המתזמן.
תוצאות
- מפעילים יכולים לשחרר תוכנית בכל מקום על ציר הזמן, והמערכת הופכת את לוח הזמנים ל"חוקי" במעבר דטרמיניסטי אחד — ללא חלונות קופצים של קונפליקטים, ללא קירות שגיאה, וללא חישובי הצמדה ידניים.
- אלגוריתם יחיד בעל ארבעה כללים מכסה כל מקרה — חדש מול חדש, חדש מול קיים, והזזות מדורגות חוצות גבולות יום — ללא טיפול מיוחד באף אחד מהם.
- תוכניות חוצות חצות ומעברי DST (שעון קיץ) זורמות דרך אותה שאילתת חפיפת טווחי זמן כמו כל מקרה אחר; אין "נתיב קוד לחצות" נפרד לתחזוקה.
- איפוס יום אינו יכול בטעות להוריד תוכנית חיה מהאוויר — חיץ 2 הדקות ושימור התוכניות הגולשות חלים על כל ערוץ, בכל עת.
- חוזה Lambda-ראשון / מסד נתונים-שני הופך סחף לוח זמנים שקט לבלתי אפשרי: MongoDB ו-MediaLive מובטחים להסכים, או שהמפעיל יראה שגיאה מפורשת.
MIN_NEIGHBOUR_GAP_MSהוא הבורר היחיד הניתן לכוונון. כל מרווח בטיחות, כל כלל הצמדה, וכל חלון חפיפה מתייחסים אליו, כך שכיוונון הגדרת ה"חוקי" של הפלטפורמה הוא שינוי של שורה אחת.
מחשבות אחרונות
הדבר המעניין ביותר במערכת זו אינו כלל יחיד — אלא כמה מעט כללים נדרשו. ארבעה תנאים על ערך מרווח, המיושמים במעבר קדימה אחד, מכסים כל קונפליקט שמפעיל יכול ליצור, כולל מפלים מרובי שלבים החוצים גבולות חצות. זוהי תוצאת תכנון מכוונת: המורכבות נדחפה לכיוון הבטחת נכונות הכללים פעם אחת, ולא לטיפול ברשימה הולכת וגדלה של מקרים מיוחדים.
הלקח הרחב יותר מתייחס מעבר לתוכנות תזמון: כאשר למערכת יש אילוצים חיצוניים קשיחים — מרווח מינימלי של encoder, הבטחות עקביות של מסד נתונים, שידור חי שלא ניתן לבטלו — המקום הבטוח ביותר לאכוף אילוצים אלה הוא במספר קטן של כללים דטרמיניסטיים המיושמים בעקביות, לא בטיפול אד הוק מפוזר על פני בסיס הקוד. וכאשר שתי מערכות תיעוד (כאן, MongoDB ו-MediaLive) חייבות להישאר מסונכרנות, סדר הכתיבה כך שכשל תמיד משאיר אותן במצב ידוע ומוסכם שווה את החביון הנוסף שהוא עולה.
אודות MicrocosmWorks
ב-MicrocosmWorks, אנו בונים תוכנות ברמת ייצור עבור ארגונים הפותרים בעיות הנדסיות מורכבות.
המומחיות שלנו כוללת יישומי AI, פלטפורמות SaaS, תוכנות ארגוניות, מערכות cloud-native, טכנולוגיות מדיה, וארכיטקטורות backend מותאמות אישית.
דרך הבלוג ההנדסי שלנו, אנו חולקים לקחים מעשיים שנלמדו מתכנון והפעלה של מערכות ייצור בעולם האמיתי.
המשך קריאה
אם נהניתם ממאמר זה, ייתכן שתמצאו גם את הנושאים הבאים שימושיים:

