عندما تسأل أي مشغل بث تلفزيوني مباشر عما يرغبون في رؤيته في أداة scheduling tool، فإنهم يقولون دائمًا تقريبًا: "دعني أبني قناة بالطريقة التي أنظم بها الملفات في مجلد. اسحب برنامجًا، وأسقطه حيث أريد، انتهى الأمر."
المشكلة هي أن channel schedule ليس مجلدًا. إنه وثيقة قانونية ذات قيود صارمة. لا يمكن لبرنامجين أن يعرضا في نفس اللحظة. لا يمكن لـ encoder تبديل المدخلات أسرع من كل خمس ثوانٍ. لا يمكن تعديل برنامج بدأ بالفعل. ويجب التعامل مع الفيلم الذي يستمر بعد منتصف الليل كبرنامج واحد، لا يُقطع عند حدود اليوم.
كل إجراء drag-and-drop هو انتهاك محتمل للقيود ينتظر الحدوث. هذه هي القصة الهندسية لكيفية تحويل scheduler الخاص بـ mStudio السحب والإفلات الصديق للمشغل إلى جدول زمني قانوني مضمون للقناة على AWS MediaLive — بما في ذلك اللحظة التي يسقط فيها المشغل برنامجًا مباشرة فوق آخر — ولماذا يحل النظام هذه النزاعات تلقائيًا بدلاً من إعطاء المشغل خطأ ولغزًا لحله.
نظرة عامة سريعة
| الجانب | التفصيل |
|---|---|
| النطاق | جدولة السحب والإفلات لقنوات FAST المباشرة على AWS MediaLive |
| حل النزاعات | تحويل تلقائي عبر خوارزمية التقاط ذات 4 قواعد |
| الحد الأدنى للفجوة بين الجيران | 6 ثوانٍ (الحد الأدنى 5 ثوانٍ لـ MediaLive + ثانية واحدة لأمان انحراف الساعة) |
| معالجة التتالي | أوقات البداية الأصلية مخزنة مؤقتًا عند أول لمسة، بحيث تنتج التحولات المتسلسلة استدعاء تنظيف واحدًا نظيفًا لكل برنامج |
| أمان التواريخ الماضية | حماية من مرحلتين — رفض الإدخال، بالإضافة إلى التراجع الصامت بعد الالتقاط |
| أمان يوم إعادة الضبط | مخزن مؤقت للتشغيل المباشر مدته دقيقتان، بالإضافة إلى الحفاظ على البرامج المتجاوزة |
| الحالة | قيد الإنتاج |
المشكلة التجارية: تقويم ليس بتقويم
تبدو جدولة القنوات وكأنها برنامج calendar software. يتوقع المشغلون أن تتصرف مثله — سحب فيلم إلى خانة الساعة 9 مساءً، وتحريك البرامج في المخطط الزمني، وإضافة سلسلة بشكل جماعي ومشاهدة الحلقات وهي تتوالى الواحدة تلو الأخرى. لكن live channel schedule يحمل قيودًا لا يملكها التقويم ببساطة:
- لا يُسمح للبرامج بالتداخل. يلعب Live TV شيئًا واحدًا بالضبط في كل مرة.
- يمتلك المُشفر حدًا أدنى لتباعد الإجراءات. لن يقوم AWS MediaLive بتبديل المدخلات أسرع من كل 5 ثوانٍ. إذا جدولت برنامجين بفارق 4 ثوانٍ، فسيتم رفض deploy.
- لا يمكن تعديل البرامج الماضية. لقد فات وقت البث بالفعل؛ والبِتات موجودة بالفعل على شاشات المشاهدين.
- البرامج التي تتجاوز منتصف الليل هي وحدة واحدة. يجب التعامل مع فيلم يعمل من 23:30 إلى 01:15 كبرنامج واحد، وليس كبرنامجين مقسمين عند حدود اليوم.
إن "التداخل الجزئي" لمدة ثانيتين ليس operator error — بل هو النتيجة الطبيعية لسحب برنامجين يتناسبان تقريبًا، ولكن ليس تمامًا، معًا. التحدي الهندسي الحقيقي هو ترجمة "سحب فيلم إلى الساعة 9 مساءً" إلى "a legal channel schedule." إذا تم ذلك بشكل خاطئ، فإما أن يصطدم المشغل بجدار خطأ عند كل إفلات، أو — الأسوأ — يكتشف عند وقت البث أن المُشفر رفض جزءًا من الجدول بصمت.
ماذا تعني "قانوني" على AWS MediaLive
لكي يكون الجدول قانونيًا على MediaLive، يجب أن تكون البرامج المتجاورة إما:
- متتالية — فجوة صفرية بينها، أو
- مفصولة بـ 5 ثوانٍ على الأقل — الحد الأدنى لتباعد الإجراءات للمُشفر.
الفخ يكمن في كل ما بينهما. فجوة ثانية واحدة، فجوة 3 ثوانٍ، فجوة 4.9 ثانية — كلها تبدو جيدة تمامًا في UI، وكلها تُرفض في وقت deploy. الأسوأ من ذلك، أن الرفض ليس فشلًا نظيفًا وذريًا؛ بل يمكن أن يؤدي إلى قناة منشورة جزئيًا، حيث وصلت بعض إجراءات الجدول إلى MediaLive ولم تصل الأخرى.
أضف هامش أمان بثانية واحدة لانحراف الساعة بين خوادم التطبيقات و AWS، ويصبح الحد الأدنى العملي 6 ثوانٍ، وليس 5. هذا الرقم الواحد — MIN_NEIGHBOUR_GAP_MS = 6000 — هو الثابت الوحيد الذي بُني عليه نظام حل النزاعات بأكمله.
لماذا تفشل الأساليب الواضحة
قبل الاستقرار على الحل التلقائي، تم النظر في العديد من الاستراتيجيات الأكثر وضوحًا ورفضها:
"رفض أي خلاف وطلب من المشغل حله." لهذا السبب، يجب على المشغلين إجراء snap math يدويًا عند كل إفلات. يصبح سحب لمدة خمس ثوانٍ لغزًا يستغرق خمس دقائق، ويزداد اللغز صعوبة مع نمو قائمة التشغيل. عمليًا، يتخلى المشغلون عن drag-and-drop تمامًا ويعودون إلى spreadsheets.
"التقاط كل شيء عند حدود الـ 5 دقائق حتى لا يتداخل أي شيء أبدًا." يحل هذا المشكلة التقنية بتدمير نية المشغل. يجب ألا ينتقل برنامج كان من المفترض أن يبدأ في الساعة 21:03:15 بصمت إلى الساعة 21:05:00. الجدول ملك للمشغل، وليس لدالة تقريب.
"اكتشاف التعارضات عند وقت deploy بدلاً من وقت الإفلات." يبدو هذا أسرع في UI، لكنه ينقل الفشل إلى أسوأ لحظة ممكنة. بحلول الوقت الذي ينقر فيه المشغل على Deploy ويرى "schedule rejected at program 47،" يكون قد تجاوز ذهنيًا التعديل الذي تسبب في ذلك.
"السماح بفجوات دقيقة في UI والسماح لـ MediaLive برفضها." يدفع هذا opaque encoder errors مباشرة إلى المشغل، ويمكن أن يترك القناة في حالة نشر جزئي يصعب حقًا التعافي منها.
كانت الرافعة التي نجحت بالفعل هي حل النزاعات تلقائيًا، في وقت الإفلات، باستخدام قواعد حتمية — وعكس المخطط الزمني المصحح للمشغل على الفور.
الحل: خوارزمية التقاط ذات القواعد الأربع
عند كل إفلات، تفحص خوارزمية التقاط كل زوج من البرامج المتجاورة على القناة المتأثرة — أزواج جديدة بجديدة، جديدة بموجودة، أو موجودة بموجودة تغيرت فجوتها بسبب الإفلات — وتطبق قاعدة واحدة بالضبط من أربع قواعد بناءً على الفجوة بينها:
- الفجوة = 0 → لا يوجد إجراء. التتابع المباشر قانوني، وهو بالتأكيد ما قصده المشغل.
- الفجوة ≥ 6 ثوانٍ → لا يوجد إجراء. ترك المشغل مساحة عمدًا، غالبًا لشاشة فاصلة أو كتلة إعلانية.
- 0 < الفجوة < 6 ثوانٍ → تحويل البرنامج الثاني إلى الخلف لإغلاق الفجوة إلى صفر.
- فجوة سلبية (تداخل) → تحويل البرنامج الثاني إلى الأمام بمقدار التداخل.
الأهم من ذلك، أن الخوارزمية تعالج الأزواج المتجاورة في تمريرة أمامية واحدة على المخطط الزمني المرتب. يصبح كل برنامج مُحول على الفور العنصر "السابق" للمقارنة التالية — لذا فإن الإفلات الذي يؤدي إلى سلسلة من التحولات يحل في مسار خطي واحد، دون الحاجة إلى تكرار.
الرسم التوضيحي 1 · شجرة قرار الالتقاط

هندسة النظام
تم بناء الجدولة حول مجموعة صغيرة ومركزة من المكونات:
- الواجهة الخلفية لـ NestJS (
schedule.service.ts) تمتلك مسار الالتقاط (snap walk)، وتخزين التتالي (cascade memoization)، وعقد الكتابة مع MediaLive. - استعلام تداخل النطاق الزمني (Time-range overlap query). عندما يصل إفلات، تستخرج الواجهة الخلفية كل برنامج موجود تتلامس نافذته الزمنية (time window) مع نطاق الدفعة الجديدة، بالإضافة إلى مخزن مؤقت مدته 6 ثوانٍ على كل جانب. نظرًا لأن هذا الاستعلام يعمل على نطاقات زمنية (time ranges) بدلاً من التواريخ التقويمية (calendar dates)، يتم التعامل مع البرامج التي تتجاوز منتصف الليل بشكل مماثل لأي برنامج آخر — لا توجد منطق خاص بحدود التاريخ في أي مكان في النظام.
- المخطط الزمني المدمج (Merged timeline). يتم دمج كائنات نقل بيانات البرنامج الجديدة (DTOs) والبرامج الموجودة التي تم الاستعلام عنها في قائمة مرتبة واحدة. يعمل مسار الالتقاط (snap walk) على هذا المخطط الزمني المدمج.
- خريطة
shiftedExistings. لأي برنامج موجود تأثر بتحويل، تلتقط هذه الخريطة أوقات بدايته ونهايته الأصلية في المرة الأولى التي يتم لمسها — ولا تقوم أبدًا بالكتابة فوقها في اللمسات اللاحقة. هذا الهيكل البياني (data structure) هو ما يجعل التتالي متعدد الخطوات (multi-step cascades) آمنًا. - دالة Lambda
DELETE_PROGRAM. لأي برنامج تم تحويله وتم نشره بالفعل على MediaLive، يتم إرسال أوقاته الأصلية إلى دالة Lambda للتنظيف قبل تحديث MongoDB. MIN_NEIGHBOUR_GAP_MS = 6000— الثابت الوحيد الذي تشير إليه كل قاعدة، وكل استعلام تداخل، وكل مخزن أمان مؤقت.
قرارات هندسية رئيسية
1. أربع قواعد، مسار واحد، لا توجد حالات خاصة. تغطي القواعد الأربع نفسها كل سيناريو يمكن للمشغل إنشاؤه: برنامج جديد تم إفلاته بين برنامجين موجودين، برنامجين جديدين يتضاربان مع بعضهما البعض، أو برنامج موجود يتم دفعه إلى التداخل بواسطة تحويل سابق في نفس الإفلات. لا يوجد code path منفصل لأي من هذه الحالات — كل حالة تختزل إلى "فحص الفجوة بين البرامج المتجاورة وتطبيق القاعدة."
2. ست ثوانٍ، وليس خمسًا. يفرض MediaLive حدًا أدنى للمسافة بين إجراءات الجدولة يبلغ 5 ثوانٍ؛ جدولة تبديلين للمدخلات بفارق 4.9 ثوانٍ يسبب deploy rejection. يفرض النظام 6 ثوانٍ — هامش أمان بثانية واحدة لانحراف الساعة بين backend's clock و AWS's. يؤدي تقديم إجراء عند 5.000 ثوانٍ بالضبط، عندما تقرأ encoder's clock ذلك على أنه 4.997 ثوانٍ، إلى حالات رفض متقطعة تبدو وكأنها فشل في الشبكة وتشبه الأخطاء غير القابلة للتكرار. تحول الثانية الإضافية وضع فشل متقطع إلى وضع لا يحدث أبدًا.
يأتي هذا مع مفاضلة مدروسة: حد أدنى يبلغ 6 ثوانٍ يعني أن الفجوات الصغيرة المتتالية التي تتراوح بين 3 أو 4 ثوانٍ يتم إغلاقها إلى الصفر بدلاً من الحفاظ عليها. تم قبول هذه المفاضلة عمدًا — الانتقالات المتتالية نظيفة على MediaLive، وتميل الفجوة المرئية التي تبلغ 3 ثوانٍ إلى الظهور كخلل للمشاهدين على أي حال.
3. التخزين المؤقت للوقت الأصلي يجعل التتالي آمنًا. يمكن أن يؤدي إفلات واحد إلى سلسلة من التحولات — البرنامج A يحول B، وB يحول C، وC يحول D. يجب أن يستهدف استدعاء التنظيف إلى MediaLive الوقت الأصلي المنشور لكل برنامج، وليس وقته المحول بالتتالي؛ استخدام الوقت الخاطئ يتسبب في استجابة MediaLive بـ "لم يتم العثور على إجراء"، مما يؤدي إلى فشل التنظيف بصمت. يحتفظ المسار بخريطة من programId → {oldStartTime, oldEndTime}، يتم التقاطها في المرة الأولى التي يتم فيها لمس كل برنامج. تقوم تحولات التتالي اللاحقة بتحديث المخطط الزمني في الذاكرة فقط؛ تبقى الأصول المخزنة مؤقتًا دون تغيير، ويستخدم التنظيف دائمًا بالضبط ما لدى MediaLive في السجل بالفعل.
الرسم التوضيحي 2 · مثال على التتالي

4. حراس التواريخ الماضية، مطبقة على مرحلتين منفصلتين. يتم حظر التعديلات المتعلقة بالماضي مرتين، عن عمد:
- المرحلة 0، قبل تشغيل مسار الالتقاط: أي برنامج جديد بوقت بدء أبكر من "الآن" يرفض الدفعة بأكملها بشكل مباشر، مع خطأ واضح. لا يعمل مسار الالتقاط حتى ضد إدخال مستحيل.
- المرحلة 4، بعد مسار الالتقاط: يتم التعامل مع حالتين فرعيتين متميزتين بشكل مختلف. يتم إعادة برنامج جديد سحبه الالتقاط عن طريق الخطأ إلى الماضي (نادر، ولكنه ممكن عند حدود الساعة وقت الطلب) بصمت إلى وقته الأصلي قبل الالتقاط — يتم الحفاظ على نية المشغل، ولا يتم تطبيق الالتقاط ببساطة. برنامج موجود كان التحويل سيدفعه إلى الماضي يرفض بدلاً من ذلك الدفعة بأكملها — لمس برنامج بدأ بثه بالفعل ليس شيئًا سيمتصه النظام بصمت أبدًا.
5. عقد كتابة يعتمد على Lambda أولاً، ثم database ثانيًا. عندما يحول الالتقاط برنامجًا تم نشره بالفعل على MediaLive، يصبح MongoDB و MediaLive غير متزامنين لفترة وجيزة، ويعد ترتيب التسوية أمرًا مهمًا. العقد: Lambda أولاً، MongoDB ثانيًا. تستدعي الواجهة الخلفية DELETE_PROGRAM على Lambda باستخدام الأوقات الأصلية للبرنامج؛ إذا فشل أي استدعاء، فإن الواجهة الخلفية تطلق خطأ قبل حدوث أي كتابة في قاعدة البيانات. فقط بمجرد نجاح كل استدعاء حذف، يقوم bulkWrite واحد بتحديث MongoDB بالأوقات الجديدة وإعادة تعيين isDeployed: false.
ينتج عن هذا ثابت نظيف واحد: إذا أظهر MongoDB برنامجًا في وقت جديد، فقد قبلت MediaLive بالفعل هذا التحويل. إذا رأى المشغل خطأ بدلاً من ذلك، لم يتم لمس أي من النظامين. لا توجد حالة ممكنة حيث يختلف MongoDB و MediaLive بصمت حول وقت البرنامج.
6. يوم إعادة الضبط لديه شبكة أمان مخصصة خاصة به. "يوم إعادة الضبط" يحذف كل برنامج على قناة ليوم تقويمي معين — وهي العملية الأكثر تدميرًا في النظام — لذلك يحمل حمايتين محددتين.
- مخزن مؤقت مدته دقيقتان (
SAFETY_BUFFER_MS = 120000) يستثني أي برنامج يبدأ خلال الدقيقتين التاليتين، مما يمنح التشغيل المباشر فترة سماح حتى لا يتسابق إعادة الضبط أبدًا مع برنامج على وشك البث. - الحفاظ على البرامج المتجاوزة يستثني البرامج التي بدأت في اليوم السابق ولكنها تمتد إلى اليوم — تلك تنتمي إلى جدول الأمس، وليس اليوم.
يتوفر أيضًا حل احتياطي مرن: إذا لم يتم نشر قناة على MediaLive على الإطلاق، فإن Lambda تعيد سلسلة خطأ معينة تفهمها الواجهة الخلفية، وتسجلها كـ no_infrastructure، ثم تجري حذفًا برمجيًا يعتمد على MongoDB فقط. يظل إعادة الضبط ناجحًا؛ وتصبح خطوة AWS مجرد عملية لا تفعل شيئًا.
لماذا هذا المزيج من خيارات التصميم
| القرار | سبب اتخاذه | البديل الذي تم النظر فيه | المفاضلة المقبولة |
|---|---|---|---|
| الحل التلقائي في وقت الإفلات مقابل الرفض والطلب | يحافظ على قابلية استخدام السحب والإفلات على نطاق واسع؛ حسابات الالتقاط اليدوية لا تصمد أمام قائمة تشغيل متنامية | الرفض عند التعارض، وطلب من المشغل الإصلاح | يتطلب من النظام، وليس المشغل، ضمان الصدق |
| حد أدنى 6 ثوانٍ مقابل الحد الأدنى المعلن لـ MediaLive وهو 5 ثوانٍ | يمتص انحراف الساعة بين الواجهة الخلفية و AWS، مما يمنع فشل النشر المتقطع | فرض 5 ثوانٍ بالضبط | الفجوات المتعمدة الصغيرة (3-4 ثوانٍ) يتم التقاطها إلى الصفر بدلاً من الحفاظ عليها |
| مسار تمريرة أمامية واحدة مقابل حل النزاعات التكراري | تحل التتابعات بشكل حتمي دون مخاوف عمق التكرار | التحويل وإعادة الفحص التكراري | يتطلب ترتيبًا دقيقًا للمخطط الزمني المرتب مقدمًا |
| Lambda أولاً / database ثانيًا مقابل database أولاً / Lambda ثانيًا | يضمن أن MongoDB و MediaLive لا يمكن أن يختلفا بصمت أبدًا | تحديث MongoDB بشكل تفاؤلي، ثم مزامنة MediaLive بعد ذلك | تأخير أعلى قليلاً لكل برنامج تم تحويله ونشره، مقابل عدم وجود خطر انحراف |
ما زال قيد المراقبة
الهندسة الصادقة تعني تسمية الفجوات التي لا تزال مفتوحة، وليس فقط تلك التي تم حلها.
- التعديلات المتزامنة على نفس القناة. إذا نقر مشغلان على Deploy على نفس القناة في غضون بضع مئات من الأجزاء من الثانية من بعضهما البعض، فسيقوم كلاهما بتحميل نفس الـ snapshot، وسيقوم كلاهما بتشغيل الـ snap walk بشكل مستقل، وسيكتب كلاهما إلى MongoDB. لا يوجد per-channel lock أو optimistic version check اليوم. الحل الحالي هو تشغيلي — مشغل واحد يمتلك قناة واحدة في كل مرة — بينما الحل التقني، وهو حقل إصدار في الـ channel document يتم التحقق منه وقت الكتابة، هو على الـ roadmap.
- لا توجد ملاحظات داخل UI حول ما تم التقاطه. عندما يحول المسار برنامجًا بثلاث ثوانٍ، يتم تحديث عرض المشغل إلى الحالة المصححة، لكنه لم يظهر بعد ماذا تحرك ولماذا. البيانات موجودة بالفعل في الـ response payload؛ من المخطط إضافة إشعار منبثق (toast)، أو شريط جانبي (sidebar)، أو عرض اختلافات (diff view) في التكرار التالي لـ scheduler UI.
النتائج
- يمكن للمشغلين إفلات برنامج في أي مكان على المخطط الزمني، ويقوم النظام بجعل الجدول الناتج قانونيًا في تمريرة حتمية واحدة — لا توجد conflict modals، ولا error walls، ولا manual snap math.
- تغطي خوارزمية واحدة ذات أربع قواعد كل حالة — جديد مقابل جديد، جديد مقابل موجود، وتحولات متتالية عبر حدود اليوم — دون معالجة أي منها كحالة خاصة.
- تتدفق البرامج التي تتجاوز منتصف الليل وانتقالات DST عبر نفس time-range overlap query مثل أي حالة أخرى؛ لا يوجد "midnight code path" منفصل للحفاظ عليه.
- لا يمكن ليوم إعادة الضبط أن يزيل برنامجًا مباشرًا عن البث عن طريق الخطأ — ينطبق المخزن المؤقت لمدة دقيقتين والحفاظ على البرامج المتجاوزة على كل قناة، في كل مرة.
- عقد Lambda أولاً / database ثانيًا يجعل انحراف الجدول الصامت مستحيلًا: يُضمن توافق MongoDB و MediaLive، أو يرى المشغل خطأ صريحًا.
MIN_NEIGHBOUR_GAP_MSهو المقبض القابل للتعديل الوحيد. يشير إليه كل هامش أمان، وكل قاعدة التقاط، وكل نافذة تداخل، لذا فإن تعديل تعريف المنصة لـ "قانوني" هو تغيير في سطر واحد.
أفكار أخيرة
الشيء الأكثر إثارة للاهتمام في هذا النظام ليس أي قاعدة واحدة — بل هو عدد القواعد القليل التي كانت مطلوبة. أربعة شروط على قيمة الفجوة، مطبقة في تمريرة أمامية واحدة، تغطي كل تعارض يمكن للمشغل إنشاؤه، بما في ذلك التتابعات متعددة الخطوات عبر حدود منتصف الليل. هذه نتيجة تصميم مقصودة: تم دفع التعقيد إلى صياغة القواعد بشكل صحيح مرة واحدة، بدلاً من التعامل مع قائمة متزايدة باستمرار من الحالات الخاصة.
الدرس الأوسع يتجاوز scheduling software: عندما يكون لدى نظام قيود خارجية صارمة — الحد الأدنى للمسافة لـ encoder، ضمانات اتساق database، بث مباشر لا يمكن إزالته — فإن المكان الأكثر أمانًا لفرض هذه القيود هو في عدد قليل من القواعد الحتمية المطبقة باستمرار، وليس في معالجة مخصصة متناثرة عبر codebase. وعندما يجب أن يظل نظامان للسجلات (هنا، MongoDB و MediaLive) متزامنين، فإن ترتيب عمليات الكتابة بحيث يؤدي الفشل دائمًا إلى تركهما في حالة معروفة ومتوافقة يستحق وقت الاستجابة الإضافي الذي يكلفه.
عن MicrocosmWorks
في MicrocosmWorks، نبني برمجيات بجودة إنتاجية للمؤسسات التي تحل مشاكل هندسية معقدة.
تشمل خبرتنا AI applications, SaaS platforms, enterprise software, cloud-native systems, media technology, and custom backend architecture.
من خلال مدونتنا الهندسية، نشارك الدروس العملية المستفادة من تصميم وتشغيل production systems حقيقية.
مواصلة القراءة
إذا استمتعت بهذا المقال، فقد تجد هذه المواضيع مفيدة أيضًا:

