الأدلة التقنية

كيف يعمل بث الفيديو المباشر وعند الطلب؟

كيف يصل الفيديو إلى كل مشاهد بجودة تناسب سرعة اتصاله، ولماذا يتأخر البث المباشر عشرين ثانية أحياناً وأقل من ثانية أحياناً أخرى.

كيف يعمل بث الفيديو المباشر وعند الطلب؟

عندما تضغط زر التشغيل في منصة فيديو وأنت على شبكة الجوال في الطريق، يبدأ الفيديو بجودة منخفضة، ثم يصبح أوضح بعد ثوان، ثم ينخفض مرة أخرى عندما تدخل نفقاً، دون أن يتوقف. وعندما تشاهد مباراة مباشرة، تسمع صراخ الجيران بالهدف قبل أن تراه على شاشتك بعشرين ثانية. وفي مكالمة فيديو لا يحدث هذا التأخير إطلاقاً. وقد يتساءل البعض: أليس كل هذا فيديو يصل عبر الانترنت؟ فلماذا يتصرف بثلاث طرق مختلفة؟ والإجابة أن كل واحدة منها طريقة بث مختلفة، صممت لتوازن بشكل مختلف بين ثلاثة أشياء: عدد المشاهدين، وثبات التشغيل، والتأخير.

في مفاهيم الفيديو للمبتدئين شرحنا الإطار والترميز Codec والحاوية Container ومعدل البت Bitrate والإطارات المفتاحية Keyframes، وهنا نأخذ الملف ونوصله إلى المشاهد، وكل ما في هذا المقال يعتمد على تلك المفاهيم، وخاصة الإطارات المفتاحية. وسوف نناقش في هذا المقال ما يلي:

  • الفرق بين الفيديو عند الطلب Video on Demand والبث المباشر Live.
  • التحميل التدريجي Progressive Download، ولماذا لا يكفي، ثم البث المتكيف Adaptive Streaming.
  • HLS وDASH: ملفات القوائم Manifests والأجزاء Segments، وسلم الجودات Bitrate Ladder، وكيف يختار المشغل بينها.
  • بروتوكولات الإدخال Ingest: RTMP وSRT وWHIP، ومستويات التأخير Latency من عشرات الثواني إلى أقل من ثانية.
  • دور شبكة توصيل المحتوى CDN، وحماية المحتوى DRM باختصار.
  • تجربة عملية: سلم من ثلاث جودات بأداة ffmpeg يقدمه nginx ويشغله hls.js في المتصفح، ونخنق الاتصال لنرى المشغل يغير الجودة، ثم بث مباشر عبر RTMP وSRT إلى MediaMTX، ونقيس التأخير بساعة مطبوعة على الفيديو.

عند الطلب أم مباشر؟

  • الفيديو عند الطلب Video on Demand واختصاراً VOD: الملف موجود كاملاً قبل أن يطلبه أحد، مثل فيلم أو درس مسجل، فتستطيع أن تضغطه بأبطأ إعداد وأفضل جودة، وتجهز له عدة نسخ، وتضعه في ال Cache مسبقاً، والمشاهد يستطيع أن يقفز إلى أي دقيقة.
  • البث المباشر Live: الفيديو يصنع الآن، والسيرفر يستقبله من الكاميرا ويضغطه ويقسمه ويرسله في نفس اللحظة، فلا وقت لإعداد بطيء، وكل ثانية يقضيها في أي خطوة تضاف إلى التأخير عن الواقع، وهو ما يسمى التأخير من الكاميرا إلى الشاشة Glass-to-Glass Latency.

والجميل أن الطريقتين اليوم تستخدمان نفس الأدوات تقريباً: نفس الأجزاء ونفس القوائم ونفس المشغل، والفرق أن القائمة في البث المباشر تتحدث كل بضع ثوان، وفي الفيديو عند الطلب ثابتة ولها نهاية.

التحميل التدريجي: الطريقة البسيطة

الطريقة البسيطة أن تضع ملف MP4 على سيرفر ويب، وتكتب في الصفحة <video src="movie.mp4">، فيبدأ المتصفح بتحميل الملف ويعرضه أثناء التحميل، وإذا قفزت إلى الدقيقة 10 طلب ذلك الجزء بطلب نطاق HTTP Range، وهذا يسمى التحميل التدريجي Progressive Download، وهو يعمل جيداً لمقطع قصير على موقع شركتك. ولكنه يفشل في ثلاث حالات:

  • جودة واحدة لكل المشاهدين: ملف 1080p بمعدل 6 Mbps يتوقف كل بضع ثوان عند مشاهد سرعته 2 Mbps، وملف 360p يبدو سيئاً على تلفزيون كبير باتصال ألياف.
  • لا يتكيف أثناء المشاهدة: إذا انخفضت سرعة الاتصال في منتصف الفيلم فلا يوجد ملف أصغر ينتقل إليه المشغل.
  • لا يصلح للبث المباشر: لا يوجد ملف كامل أصلاً حتى يحمله المتصفح.

والحل الذي تستخدمه كل المنصات اليوم هو البث المتكيف Adaptive Bitrate Streaming واختصاراً ABR، وفكرته ثلاث خطوات: اضغط الفيديو بعدة جودات، وقسم كل جودة إلى أجزاء Segments قصيرة متساوية، واكتب قائمة Manifest تصف كل ذلك، ثم اترك المشغل يختار لكل جزء الجودة التي يتحملها اتصاله الآن. والصورة التالية تبين الفرق:

جزءان: في الأعلى التحميل التدريجي، سيرفر عليه ملف MP4 واحد بجودة 1080p يصل إلى ثلاثة أجهزة، الهاتف على شبكة جوال يتوقف وعليه علامة تحميل، وفي الأسفل البث المتكيف، السيرفر عليه قائمة master.m3u8 وثلاث جودات 360p و720p و1080p مقسمة إلى أجزاء من 6 ثوان، والهاتف يأخذ أجزاء 360p والحاسوب 720p والتلفزيون 1080p، والهاتف ينتقل بين الجودات حسب سرعته
ملف واحد لكل المشاهدين، مقابل أجزاء بعدة جودات يختار منها كل مشغل

HLS وDASH: القائمة والأجزاء

هناك معياران مشهوران للبث المتكيف، وفكرتهما واحدة والفرق في صيغة القائمة:

  • HLS اختصار HTTP Live Streaming، من Apple، ونشر في RFC 8216 سنة 2017 كوثيقة معلوماتية Informational. القائمة ملف نصي بامتداد .m3u8، وهي على مستويين: قائمة رئيسية Master Playlist فيها الجودات المتاحة، ولكل جودة قائمة وسائط Media Playlist فيها أجزاؤها بالترتيب. والأجزاء كانت تقليدياً بحاوية TS، واليوم يمكن أن تكون Fragmented MP4. وأجهزة Apple ومتصفح Safari تشغله مباشرة، وفي المتصفحات الأخرى تستخدم مكتبة مثل hls.js التي تحول أجزاء TS إلى أجزاء MP4 وتسلمها للمتصفح عبر Media Source Extensions.
  • MPEG-DASH اختصار Dynamic Adaptive Streaming over HTTP، وهو معيار دولي برقم ISO/IEC 23009-1، والقائمة ملف XML بامتداد .mpd اسمه Media Presentation Description، والأجزاء غالباً MP4 مقسمة، ويشغله في المتصفح مثلاً dash.js من منتدى DASH Industry Forum.

ولأن المعيارين يستخدمان نفس نوع الترميز ونفس فكرة الأجزاء، ظهرت صيغة CMAF برقم ISO/IEC 23000-19، وهي أجزاء MP4 موحدة يمكن أن تشير إليها قائمة HLS وقائمة DASH معاً، فتحفظ نسخة واحدة من الأجزاء بدل نسختين. والمهم هنا أن كل جزء يجب أن يبدأ بإطار مفتاحي، حتى يستطيع المشغل أن يبدأ منه أو ينتقل إليه من جودة أخرى، وهذا هو السبب الذي جعل مواصفات Apple لتأليف HLS تشترط أن يبدأ كل جزء بإطار IDR، وتوصي بإطار مفتاحي كل ثانيتين وبأجزاء مدتها 6 ثوان.

شجرة ملفات HLS من التجربة: master.m3u8 في الأعلى يشير إلى ثلاث قوائم 360p/index.m3u8 و720p/index.m3u8 و1080p/index.m3u8 مع BANDWIDTH وRESOLUTION لكل منها، وكل قائمة تشير إلى عشرة أجزاء seg000.ts إلى seg009.ts مدة كل منها 6.006 ثانية، وأحجام الجودات 3.2 و22 و44 ميغابايت، والأجزاء في الجودات الثلاث تبدأ في نفس اللحظات
قائمة رئيسية، ثم قائمة لكل جودة، ثم أجزاء متساوية تبدأ في نفس اللحظات في كل الجودات

سلم الجودات Bitrate Ladder

سلم الجودات هو قائمة الجودات التي تقدمها، كل درجة فيها دقة ومعدل بت، وتختاره بحيث يجد كل مشاهد درجة تناسب اتصاله وشاشته. ولا يوجد سلم صحيح لكل المحتوى، ولكن مواصفات Apple تعطي مثالاً كنقطة بداية، ومنه أخذنا الدرجات الثلاث للتجربة:

الدقةH.264 بالكيلوبت في الثانيةHEVC بالكيلوبت في الثانية
416×234145
640×360365145
960×5402000600 إلى 1600
1280×7203000 و45002400 و3400
1920×10806000 و78004500 و5800
3840×216011600 و16800

ولاحظ أن HEVC يصل إلى نفس الدقة بمعدل أقل، كما رأينا في مقارنة الترميزات في المقال السابق، وأن المنصات الكبيرة لا تستخدم سلماً ثابتاً وإنما تحسبه لكل فيديو بحسب صعوبته، فمقطع رسوم متحركة بسيط لا يحتاج ما يحتاجه مقطع رياضي سريع.

كيف يختار المشغل الجودة؟

المشغل في البث المتكيف هو الذي يقرر، وليس السيرفر، فالسيرفر يقدم ملفات ثابتة فقط. وطرق القرار المعروفة ثلاث:

  • بحسب السرعة Throughput-Based: يقيس المشغل سرعة تحميل آخر الأجزاء، ويختار أعلى جودة يكون معدلها أقل من السرعة المقدرة بهامش أمان. وهذا ما سوف نراه في سجل hls.js في التجربة.
  • بحسب المخزن المؤقت Buffer-Based: يراقب كم ثانية من الفيديو محملة مسبقاً، فإذا امتلأ المخزن رفع الجودة، وإذا قارب الفراغ خفضها قبل أن يتوقف الفيديو.
  • المختلطة Hybrid: تجمع الاثنين، وهي ما تستخدمه أغلب المشغلات الحديثة بدرجات مختلفة.

والقرار يحدث على حدود الأجزاء فقط، ولذلك يجب أن تبدأ الأجزاء في كل الجودات في نفس اللحظة وبإطار مفتاحي، فإذا كانت الإطارات المفتاحية في 720p عند الثانية 6 وفي 1080p عند الثانية 8.3، فلن يجد المشغل نقطة نظيفة ينتقل منها.

رسم زمني من تجربة hls.js: خط السرعة المتاحة مرتفع في البداية ثم ينخفض إلى 1.5 ميغابت في الثانية عند الثانية 12 تقريباً، وتحته الأجزاء التي حملها المشغل: الجزء الأول 360p، ثم الأجزاء 1 إلى 3 بجودة 1080p، ثم بعد خنق الاتصال الأجزاء 4 إلى 9 بجودة 360p، وتقدير السرعة ينزل من قرابة 140 ميغابت إلى 0.58 ثم يرتفع ببطء إلى 1.3 ميغابت، وهو أقل من 3.55 ميغابت التي تحتاجها 720p
المشغل يبدأ بالجودة الأولى، ثم يرتفع بحسب السرعة، وعندما تنخفض السرعة يعود إلى ما يتحمله الاتصال

بروتوكولات الإدخال Ingest: من الكاميرا إلى السيرفر

في البث المباشر يجب أن يصل الفيديو أولاً من برنامج البث، مثل OBS أو ffmpeg أو كاميرا، إلى سيرفر الوسائط Media Server، وهذا الجزء اسمه الإدخال Ingest، وله بروتوكولاته الخاصة لأنه اتصال واحد مستمر بجودة عالية، وليس آلاف المشاهدين:

  • RTMP: بروتوكول قديم من أيام Flash يعمل فوق TCP على المنفذ 1935، وما زال أشهر طريقة لإرسال البث إلى المنصات وأسهلها في برامج البث، وحاويته FLV كانت محدودة بـ H.264 وAAC، ثم جاء مشروع Enhanced RTMP فأضاف ترميزات مثل HEVC وAV1 وVP9 وOpus. ومشكلته أن TCP يتوقف لينتظر الحزمة المفقودة، فعلى اتصال سيئ يتراكم التأخير.
  • SRT: اختصار Secure Reliable Transport، من Haivision، مفتوح المصدر برخصة MPL-2.0 وموصوف في مسودة IETF، ويعمل فوق UDP ويعيد إرسال الحزم المفقودة بطريقة ARQ داخل نافذة تأخير تحددها أنت، ويدعم التشفير AES، لذلك هو مناسب لإرسال البث عبر الانترنت العام من موقع تصوير إلى السيرفر.
  • WHIP: اختصار WebRTC-HTTP Ingestion Protocol، ونشر في RFC 9725 في مارس 2025 كمعيار Standards Track، وهو يجعل إرسال البث عبر WebRTC بسيطاً: طلب HTTP POST واحد يحمل عرض SDP، والرد يحمل الجواب، ثم تمر الوسائط عبر WebRTC العادي. وفائدته أن المتصفح نفسه يستطيع أن يبث دون برنامج، وأن التأخير منخفض من البداية. وقد شرحنا SDP وICE وTURN في مكالمات WebRTC خلف NAT، ولن نكررها هنا.

وبعد الإدخال يقوم السيرفر بثلاثة أعمال: يعيد الترميز Transcode إلى درجات السلم إذا احتاج، ويقسم الفيديو إلى أجزاء ويكتب القوائم Packaging، ويقدم كل ذلك لمن يطلبه، مباشرة أو عبر CDN. والصورة التالية تبين المسار كاملاً:

مسار البث المباشر من اليسار إلى اليمين بخطوات مرقمة: 1 كاميرا وحاسوب عليه OBS أو ffmpeg، 2 الإدخال عبر RTMP على TCP أو SRT على UDP أو WHIP عبر HTTP وWebRTC، 3 سيرفر الوسائط يعيد الترميز إلى 360p و720p و1080p ويقسم الأجزاء ويكتب القوائم، 4 سيرفر الأصل Origin، 5 نقاط CDN قريبة من المشاهدين تحفظ الأجزاء، 6 المشاهدون على الهاتف والحاسوب والتلفزيون عبر HLS أو DASH، وفي الأسفل مسار مختصر من سيرفر الوسائط عبر WebRTC مباشرة إلى المشاهد بتأخير أقل من ثانية
الإدخال، ثم التحويل والتقسيم، ثم التوزيع عبر HTTP وCDN، أو عبر WebRTC للتأخير المنخفض

لماذا يتأخر البث المباشر؟

كل خطوة في المسار أعلاه تضيف وقتاً، ولكن الجزء الأكبر في HLS التقليدي يأتي من قاعدة واحدة: المشغل لا يبدأ من آخر جزء في القائمة. ففي RFC 8216 أن المشغل لا يجب أن يختار جزءاً يبدأ على بعد أقل من ثلاث مدد مستهدفة Target Durations من نهاية القائمة، والسبب أن الجزء التالي قد يتأخر في الوصول، فيحتاج المشغل مخزناً يكفيه حتى لا يتوقف، وبالتالي مع أجزاء 6 ثوان يبدأ المشغل على بعد 18 ثانية على الأقل من الحافة الحية Live Edge، ثم أضف إليها مدة الجزء الذي يجب أن يكتمل قبل أن يكتب في القائمة، ووقت الترميز والشبكة. وهذا يقسم البث إلى مستويات:

  • HLS وDASH العاديان: تأخير من عشرات الثواني غالباً، وفي تجربتنا قرابة 23 ثانية، والمقابل أن البث يتحمل الشبكات السيئة ويمر عبر أي CDN كملفات عادية.
  • HLS منخفض التأخير Low-Latency HLS: امتداد من Apple يقسم الجزء إلى أجزاء صغيرة Partial Segments، وتوثيق Apple يعطي مثالاً أجزاء جزئية مدتها 200 ملي ثانية داخل أجزاء 6 ثوان، مع طلبات قائمة تنتظر حتى يظهر الجديد Blocking Playlist Reload، والهدف أن يصل التأخير إلى مستوى بث التلفزيون. وفي تجربتنا قرابة 3.5 إلى 4 ثوان.
  • WebRTC: لا توجد أجزاء ولا قوائم، وإنما حزم RTP تصل فور ترميزها، والتأخير أقل من ثانية، وفي تجربتنا قرابة ربع ثانية على نفس الجهاز. والمقابل أن توزيعه لآلاف المشاهدين يحتاج سيرفرات خاصة SFU وليس CDN عادية، وهو أغلى بكثير لكل مشاهد.
ثلاثة أعمدة أفقية للتأخير المقاس في التجربة على نفس الجهاز: HLS بأجزاء 6 ثوان 22.9 ثانية، وLow-Latency HLS بأجزاء ثانية وأجزاء جزئية 200 ملي ثانية 3.5 ثانية، وWebRTC عبر WHEP 0.25 ثانية، وتحت عمود HLS تفسير: المشغل يبدأ على بعد ثلاث مدد مستهدفة من نهاية القائمة أي 18 ثانية، وتحت WebRTC: لا أجزاء ولا قوائم
كلما قل التأخير زادت كلفة توزيع البث، فاختر المستوى الذي يحتاجه المحتوى فعلاً

متى تحتاج كل مستوى؟ محاضرة أو خطبة جمعة أو حفل مسجل لا يضره تأخير 20 ثانية، وHLS العادي هنا أرخص وأثبت. مباراة يتابعها الناس مع تعليقاتهم في وسائل التواصل تحتاج LL-HLS حتى لا يسبق التعليق الهدف. أما المزاد المباشر أو البث التفاعلي الذي يرد فيه المذيع على المشاهدين، أو كاميرا مراقبة تتحكم بها، فتحتاج WebRTC.

دور CDN: البث المتكيف مجرد ملفات

أهم ميزة في HLS وDASH أن الأجزاء والقوائم ملفات HTTP عادية، فتمر عبر أي شبكة توصيل محتوى CDN دون أي إعداد خاص، وقد شرحنا ال Cache وترويسة Cache-Control وHIT وMISS في كيف تعمل شبكة توصيل المحتوى CDN. والقاعدة العملية للبث بسيطة:

  • الأجزاء لا تتغير أبداً بعد كتابتها: الجزء seg004.ts يبقى كما هو، فأعطه مدة تخزين طويلة، وكل مشاهد بعد الأول يأخذه من أقرب نقطة دون أن يصل إلى سيرفرك، وهذا ما يجعل مليون مشاهد ممكناً بسيرفر أصل واحد.
  • القائمة في البث المباشر تتغير كل جزء: فإما ألا تحفظها أو تحفظها لمدة أقل من مدة الجزء، وإلا رأى المشاهدون قائمة قديمة وتأخر البث أو توقف.
  • في الفيديو عند الطلب القائمة ثابتة أيضاً: وفي نهايتها #EXT-X-ENDLIST، فتحفظ مثل الأجزاء.

حماية المحتوى DRM باختصار

إذا كان محتواك مدفوعاً، مثل أفلام أو دورات، فقد يطلب منك أصحاب الحقوق إدارة الحقوق الرقمية Digital Rights Management واختصاراً DRM، وفكرتها أن الأجزاء مشفرة، والمفتاح لا يعطى للمشغل نفسه، وإنما لوحدة فك مغلقة داخل المتصفح أو الجهاز اسمها Content Decryption Module واختصاراً CDM، بعد أن تسأل سيرفر رخص License Server هل يحق لهذا المستخدم أن يشاهد. وهناك ثلاثة أنظمة تغطي أغلب الأجهزة: Widevine من Google في Chrome وAndroid، وPlayReady من Microsoft في Edge وأجهزة Windows وكثير من أجهزة التلفزيون، وFairPlay Streaming من Apple في Safari وأجهزة Apple.

وحتى لا تشفر الفيديو ثلاث مرات، يأتي معيار التشفير المشترك Common Encryption واختصاراً CENC برقم ISO/IEC 23001-7، فتشفر الأجزاء مرة واحدة ويستطيع كل نظام أن يفكها بمفتاحه من سيرفر رخصه، والمتصفح يربط المشغل بوحدة الفك عبر واجهة Encrypted Media Extensions من W3C. والصورة التالية تبين المسار:

مسار DRM: أداة التغليف تشفر الأجزاء مرة واحدة بمعيار CENC، والأجزاء تصل عبر CDN إلى المتصفح أو الجهاز، وفيه المشغل عبر EME ووحدة الفك CDM التي تفك داخلياً، وأسماء الأنظمة Widevine وPlayReady وFairPlay، ثم طلب رخصة من وحدة الفك إلى سيرفر الرخص، الذي يسأل تطبيقك هل يحق لهذا المستخدم أن يشاهد
الأجزاء مشفرة مرة واحدة، والمفتاح يصل إلى وحدة الفك داخل الجهاز بعد موافقة سيرفر الرخص

ولاحظ أن DRM يرفع كلفة النسخ ولا يمنعه تماماً، فكل ما يظهر على الشاشة يمكن تصويره، لذلك لا تضفه إلا إذا طلبه أصحاب الحقوق، فهو يضيف سيرفرات ورخصاً وتعقيداً في كل جهاز.

التجربة العملية

سوف نبني التجربة على جزأين: فيديو عند الطلب بسلم من ثلاث جودات نشغله في المتصفح ونخنق اتصاله، ثم بث مباشر نقيس تأخيره بثلاث طرق. ونستخدم نفس Docker Image من تجربة مفاهيم الفيديو، أي lab-video-tools:1 وفيها ffmpeg 9.0.2، ونفس مقطع Artemis II من NASA في الملكية العامة، وMediaMTX بالإصدار 1.21.2 كسيرفر وسائط، وnginx بالإصدار 1.29.8 لتقديم الملفات، وكل المنافذ على 127.0.0.1 فقط.

سلم من ثلاث جودات بأداة ffmpeg

نأخذ دقيقة من المقطع ونضغطها مرة واحدة إلى ثلاث درجات من سلم Apple، ونطلب من ffmpeg أن يكتب أجزاء HLS والقوائم مباشرة. ملف ladder.sh:

#!/bin/sh
# One source, three renditions (Apple's ladder: 360p 365k, 720p 3000k, 1080p 6000k), 6 s HLS segments
cd /work
ffmpeg -hide_banner -v error -y -ss 60 -t 60 -i artemis-src.webm \
  -filter_complex "[0:v]split=3[a][b][c];[a]scale=640:360[v360];[b]scale=1280:720[v720];[c]scale=1920:1080[v1080]" \
  -map "[v360]" -map "[v720]" -map "[v1080]" -map 0:a -map 0:a -map 0:a \
  -c:v libx264 -preset veryfast -profile:v high -g 60 -keyint_min 60 -sc_threshold 0 \
  -b:v:0 365k  -maxrate:v:0 400k  -bufsize:v:0 730k \
  -b:v:1 3000k -maxrate:v:1 3300k -bufsize:v:1 6000k \
  -b:v:2 6000k -maxrate:v:2 6600k -bufsize:v:2 12000k \
  -c:a aac -b:a 128k -ac 2 \
  -f hls -hls_time 6 -hls_playlist_type vod -hls_segment_type mpegts \
  -hls_segment_filename "web/vod/%v/seg%03d.ts" \
  -master_pl_name master.m3u8 \
  -var_stream_map "v:0,a:0,name:360p v:1,a:1,name:720p v:2,a:2,name:1080p" \
  web/vod/%v/index.m3u8
mkdir -p web/vod
docker run --rm --name lab-stream-ladder -v "$PWD:/work" lab-video-tools:1 sh /work/ladder.sh
cat web/vod/master.m3u8 web/vod/720p/index.m3u8 && du -sh web/vod/*/

والمخرج سوف يكون كما يلي (اختصرنا أسطر الأجزاء من الرابع إلى التاسع):

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=582468,AVERAGE-BANDWIDTH=441959,RESOLUTION=640x360,CODECS="avc1.64001e,mp4a.40.2"
360p/index.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=3550652,AVERAGE-BANDWIDTH=3069101,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
720p/index.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=7160652,AVERAGE-BANDWIDTH=6085891,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2"
1080p/index.m3u8

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:6.006000,
seg000.ts
#EXTINF:6.006000,
seg001.ts
#EXTINF:6.006000,
seg002.ts
...
#EXTINF:6.006000,
seg009.ts
#EXT-X-ENDLIST
44M	web/vod/1080p/
3.2M	web/vod/360p/
22M	web/vod/720p/

في المخرج أعلاه لاحظ التالي:

  • القائمة الرئيسية فيها ثلاثة أسطر EXT-X-STREAM-INF، ولكل جودة BANDWIDTH وهو أعلى معدل فعلي مع الصوت، وAVERAGE-BANDWIDTH المتوسط، وCODECS بالصيغة التي يفهمها المتصفح: avc1.64001f تعني H.264 بال Profile High والمستوى 3.1، وmp4a.40.2 تعني AAC-LC. والمشغل يقرأ هذا قبل أن يحمل أي جزء، فيستبعد ما لا يستطيع تشغيله.
  • قائمة 720p فيها TARGETDURATION:6 وعشرة أجزاء، كل منها 6.006 ثانية بالضبط، لأننا وضعنا إطاراً مفتاحياً كل 60 إطاراً بالخيارات -g 60 -keyint_min 60 -sc_threshold 0، و60 إطاراً بمعدل 29.97 تساوي 2.002 ثانية، فكل جزء ثلاث مجموعات GOP كاملة.
  • PLAYLIST-TYPE:VOD وEXT-X-ENDLIST تقولان للمشغل إن القائمة كاملة ولن تتغير، وهذا ما سوف يختفي في البث المباشر.
  • كل الجودات معاً 69 ميغابايت لدقيقة واحدة، أي أن البث المتكيف يكلفك تخزيناً أكثر مقابل أن يجد كل مشاهد ما يناسبه.

نكسر السلم: إطارات مفتاحية في أماكن عشوائية

ماذا يحدث إذا نسيت خيارات الإطارات المفتاحية؟ نعيد نفس الأمر لجودة 360p فقط دون -g و-sc_threshold:

docker run --rm --name lab-stream-broken -v "$PWD:/work" -w /work lab-video-tools:1 sh -c 'ffmpeg -hide_banner -v error -y -ss 60 -t 60 -i artemis-src.webm -vf scale=640:360 -c:v libx264 -preset veryfast -b:v 365k -c:a aac -b:a 128k -f hls -hls_time 6 -hls_playlist_type vod -hls_segment_filename "broken/seg%03d.ts" broken/index.m3u8 && grep -E "TARGET|EXTINF" broken/index.m3u8'

والمخرج سوف يكون كما يلي:

#EXT-X-TARGETDURATION:11
#EXTINF:8.341667,
#EXTINF:10.944267,
#EXTINF:5.472133,
#EXTINF:8.341667,
#EXTINF:8.341667,
#EXTINF:8.341667,
#EXTINF:8.341667,
#EXTINF:1.935267,

طلبنا أجزاء 6 ثوان فحصلنا على أجزاء بين 1.9 و10.9 ثانية، والسبب أن ffmpeg لا يستطيع قطع الجزء إلا عند إطار مفتاحي، وx264 بإعداده الافتراضي يضع إطاراً مفتاحياً كل 250 إطاراً، أي 8.34 ثانية، وإطاراً إضافياً عند كل تغير للمشهد. والنتيجة ثلاث مشكلات: المدة المستهدفة أصبحت 11 ثانية فزاد تأخير البث المباشر، والأجزاء لا تبدأ في نفس اللحظات في الجودات المختلفة فيصعب الانتقال بينها، ومدة التحميل لكل جزء غير متوقعة. لذلك ثبت الفاصل بين الإطارات المفتاحية دائماً في أي فيديو للبث.

تشغيل السلم في المتصفح مع hls.js

نقدم المجلد web من nginx، مع أنواع الملفات MIME Types الصحيحة للقوائم والأجزاء. ملف nginx.conf:

server {
    listen 80;
    root /usr/share/nginx/html;
    types {
        text/html html;
        text/javascript js;
        application/vnd.apple.mpegurl m3u8;
        video/mp2t ts;
    }
    location /vod/ {
        add_header Cache-Control "public, max-age=3600";
    }
}

ثم صفحة web/index.html، ونحمل hls.js بالإصدار 1.7.3 إلى web/lib بالأمر curl -sfL -o web/lib/hls.min.js https://unpkg.com/[email protected]/dist/hls.min.js:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>HLS ladder</title>
<style>
  body { margin: 0; background: #111; color: #eee; font: 16px/1.5 Consolas, monospace; }
  video { width: 960px; height: 540px; background: #000; display: block; }
  #info { padding: 10px 14px; }
</style>
</head>
<body>
<video id="v" muted autoplay playsinline></video>
<div id="info"></div>
<script src="/lib/hls.min.js"></script>
<script>
const video = document.getElementById('v');
const info = document.getElementById('info');
window.log = [];
const hls = new Hls({ maxBufferLength: 12, maxMaxBufferLength: 12 });   // a short buffer, so the switch shows in a 60 s clip
hls.loadSource('/vod/master.m3u8');
hls.attachMedia(video);
const t0 = performance.now();
hls.on(Hls.Events.FRAG_LOADED, (e, d) => {
  const f = d.frag;
  window.log.push(`t=${((performance.now() - t0) / 1000).toFixed(1)}s  ${hls.levels[f.level].height}p seg${String(f.sn).padStart(3, '0')}  estimate ${Math.round(hls.bandwidthEstimate / 1000)} kbps`);
});
setInterval(() => {
  const l = hls.levels[hls.currentLevel];
  const buf = video.buffered.length ? video.buffered.end(video.buffered.length - 1) - video.currentTime : 0;
  info.textContent = `level: ${l ? l.height + 'p' : '-'}  |  bandwidth estimate: ${Math.round(hls.bandwidthEstimate / 1000)} kbps  |  buffer: ${buf.toFixed(1)} s`;
}, 250);
</script>
</body>
</html>

وملف compose.yaml، وفيه سيرفر الوسائط الذي سوف نستخدمه في الجزء الثاني:

name: lab-stream
services:
  web:
    image: nginx:1.29.8-alpine
    container_name: lab-stream-web
    volumes:
      - ./web:/usr/share/nginx/html:ro
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    ports:
      - "127.0.0.1:18771:80"

  mediamtx:
    image: bluenviron/mediamtx:1.21.2
    container_name: lab-stream-mtx
    environment:
      MTX_HLSVARIANT: ${HLS_VARIANT:-lowLatency}        # lowLatency (default) or mpegts
      MTX_HLSSEGMENTDURATION: ${HLS_SEGMENT:-1s}
      MTX_WEBRTCLOCALUDPADDRESS: ":18189"
      MTX_WEBRTCIPSFROMINTERFACES: "false"
      MTX_WEBRTCADDITIONALHOSTS: "127.0.0.1"
    ports:
      - "127.0.0.1:19350:1935"          # RTMP ingest
      - "127.0.0.1:18890:8890/udp"      # SRT ingest
      - "127.0.0.1:18888:8888"          # HLS out
      - "127.0.0.1:18889:8889"          # WebRTC out (WHEP) and WHIP in
      - "127.0.0.1:18189:18189/udp"     # WebRTC media
docker compose up -d
curl -s -D - -o /dev/null http://127.0.0.1:18771/vod/master.m3u8
HTTP/1.1 200 OK
Server: nginx/1.29.8
Date: Fri, 09 Oct 2026 00:49:45 GMT
Content-Type: application/vnd.apple.mpegurl
Content-Length: 414
Last-Modified: Fri, 09 Oct 2026 00:49:06 GMT
Connection: keep-alive
ETag: "6ac83a02-19e"
Cache-Control: public, max-age=3600
Accept-Ranges: bytes

الآن افتح http://127.0.0.1:18771، وبعد ثوان قليلة سوف ترى الجودة 1080p، والصورة التالية من المتصفح بعد 12 ثانية:

مشغل الفيديو في المتصفح يعرض برج الإطلاق بجوار كبسولة Orion بوضوح عال، وتحته سطر الحالة: level 1080p وتقدير السرعة 94242 kbps والمخزن 12.5 ثانية
على اتصال محلي سريع: الجودة 1080p والمخزن ممتلئ إلى الحد الذي حددناه

ثم خنقنا الاتصال إلى 1.5 ميغابت في الثانية من أدوات المطور في Chrome، أي من تبويب Network واختيار سرعة مخصصة، وهو نفس ما فعلناه في التجربة من بروتوكول أدوات المطور بالأمر Network.emulateNetworkConditions، وتركنا الفيديو يكمل، ثم قرأنا السجل من Console بالأمر window.log.join('\n'):

t=0.2s  360p seg000  estimate 582 kbps
t=0.5s  1080p seg001  estimate 19073 kbps
t=0.8s  1080p seg002  estimate 110434 kbps
t=7.3s  1080p seg003  estimate 141783 kbps
t=17.6s  360p seg004  estimate 582 kbps
t=20.5s  360p seg005  estimate 1077 kbps
t=26.7s  360p seg006  estimate 1167 kbps
t=32.4s  360p seg007  estimate 1268 kbps
t=38.5s  360p seg008  estimate 1308 kbps
t=44.6s  360p seg009  estimate 1315 kbps
نفس المشغل بعد خنق الاتصال يعرض صاروخ SLS على المنصة بجودة أقل وتفاصيل أنعم، وتحته سطر الحالة: level 360p وتقدير السرعة 1333 kbps والمخزن 8.4 ثانية
بعد خنق الاتصال إلى 1.5 ميغابت: الجودة 360p، والفيديو يكمل دون توقف

في السجل أعلاه لاحظ التالي:

  • الجزء الأول دائماً بجودة 360p، لأنها أول جودة في القائمة الرئيسية ولا يعرف المشغل سرعة الاتصال بعد، ثم قاس السرعة من الجزء الأول وقفز مباشرة إلى 1080p.
  • الأجزاء 1 إلى 3 حملت بسرعة لأن المخزن كان فارغاً، ثم توقف المشغل عن التحميل حتى يقل المخزن عن 12 ثانية، وهو الحد الذي وضعناه بالخيار maxBufferLength. والقيمة الافتراضية في hls.js هي 30 ثانية، وصغرناها حتى يظهر الانتقال في مقطع مدته دقيقة.
  • بعد الخنق بدأ تحميل الجزء الرابع بجودة 1080p ولم يكتمل، فانتقل المشغل إلى 360p ونزل تقديره إلى 582، ثم بدأ يرتفع ببطء مع كل جزء حتى 1315 kbps، وهو أقل من 3550 kbps التي تحتاجها 720p في القائمة الرئيسية، لذلك بقي على 360p.
  • لم يتوقف الفيديو أثناء الانتقال، لأن المخزن كان فيه ثوان كافية، وهذا هو الهدف كله: أن تنخفض الجودة بدل أن تظهر دائرة التحميل.

ونفس السلم يمكن أن يكتب كقائمة DASH، فنفس ffmpeg يكتب ملف manifest.mpd بالخيار -f dash (ملف dash.sh بدرجتين وصوت)، وهذا جزء القائمة الذي يصف درجات الصورة (اختصرنا الترويسة وبعض السمات):

#!/bin/sh
# The same source as a DASH presentation: two video representations and one audio, 6 s segments
cd /work
ffmpeg -hide_banner -v error -y -ss 60 -t 60 -i artemis-src.webm \
  -filter_complex "[0:v]split=2[a][b];[a]scale=640:360[v360];[b]scale=1280:720[v720]" \
  -map "[v360]" -map "[v720]" -map 0:a \
  -c:v libx264 -preset veryfast -g 60 -keyint_min 60 -sc_threshold 0 \
  -b:v:0 365k -b:v:1 3000k -c:a aac -b:a 128k \
  -f dash -seg_duration 6 -use_template 1 -use_timeline 0 \
  -adaptation_sets "id=0,streams=v id=1,streams=a" web/dash/manifest.mpd
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" profiles="urn:mpeg:dash:profile:isoff-live:2011" type="static"
	mediaPresentationDuration="PT1M0.0S" maxSegmentDuration="PT6.0S" minBufferTime="PT12.0S">
	<Period id="0" start="PT0.0S">
		<AdaptationSet id="0" contentType="video" startWithSAP="1" segmentAlignment="true" bitstreamSwitching="true" frameRate="30000/1001" maxWidth="1280" maxHeight="720" par="16:9">
			<Representation id="0" mimeType="video/mp4" codecs="avc1.64001e" bandwidth="365000" width="640" height="360" sar="1:1">
				<SegmentTemplate timescale="1000000" duration="6000000" initialization="init-stream$RepresentationID$.m4s" media="chunk-stream$RepresentationID$-$Number%05d$.m4s" startNumber="1">
				</SegmentTemplate>
			</Representation>
			<Representation id="1" mimeType="video/mp4" codecs="avc1.64001f" bandwidth="3000000" width="1280" height="720" sar="1:1">
			...

الفكرة نفسها بأسماء أخرى: AdaptationSet مجموعة من الدرجات يمكن الانتقال بينها، وRepresentation هي الدرجة، وSegmentTemplate يصف أسماء الأجزاء بقالب بدل أن يكتبها كلها، وstartWithSAP="1" وsegmentAlignment="true" تعنيان أن كل جزء يبدأ بنقطة دخول نظيفة وأن الأجزاء متطابقة الحدود بين الدرجات، أي نفس شرط الإطارات المفتاحية الذي كسرناه قبل قليل. والأجزاء هنا .m4s، أي MP4 مقسمة وليس TS.

بث مباشر وقياس التأخير من الكاميرا إلى الشاشة

الآن نحتاج مصدر بث حي، ونحتاج طريقة نقيس بها التأخير، والفكرة بسيطة: نطبع ساعة الجهاز بالملي ثانية على كل إطار عند الإرسال، ونعرض في الصفحة ساعة المتصفح بجانب الفيديو، فالفرق بينهما في نفس لقطة الشاشة هو التأخير من الإرسال إلى العرض، والجهاز واحد فالساعتان واحدة. ملف clock.txt فيه نص الساعة بصيغة drawtext:

%{localtime:%X.%3N}

وملف publish.sh:

#!/bin/sh
# A live test source: moving pattern, a tone, and the wall clock (with milliseconds) burned into every frame
# usage: publish.sh <output url> <format>
exec ffmpeg -hide_banner -v error -re \
  -f lavfi -i testsrc2=size=1280x720:rate=30 -f lavfi -i sine=frequency=440:sample_rate=48000 \
  -vf "drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf:textfile=/work/clock.txt:fontsize=72:fontcolor=white:box=1:[email protected]:boxborderw=16:x=(w-tw)/2:y=h-th-60" \
  -c:v libx264 -preset veryfast -tune zerolatency -g 30 -keyint_min 30 -sc_threshold 0 -b:v 2500k -maxrate 2500k -bufsize 2500k \
  -c:a aac -b:a 128k -f "$2" "$1"

في الإعداد أعلاه لاحظ التالي:

  • -re يجعل ffmpeg يرسل بسرعة الوقت الحقيقي كما تفعل الكاميرا، وليس بأقصى سرعة.
  • -tune zerolatency يلغي إطارات B والتجميع المسبق في x264، حتى لا ينتظر المرمز إطارات قادمة قبل أن يرسل الحالي.
  • -g 30 إطار مفتاحي كل ثانية، لأن MediaMTX يقطع الأجزاء عند الإطارات المفتاحية كما رأينا.
  • إذا كتبت الوقت بالصيغة %H\:%M\:%S داخل الملف فسوف يرفضها drawtext بخطأ ينتهي بعبارة is not known، لأن النقطتين تفصلان معاملات الدالة، والصيغة %X تعطي نفس الساعة دون نقطتين.

نبدأ بالإرسال عبر RTMP إلى MediaMTX على شبكة ال Compose:

docker run -d --name lab-stream-pub --network lab-stream_default -v "$PWD:/work" lab-video-tools:1 \
  sh /work/publish.sh rtmp://lab-stream-mtx:1935/live flv
docker logs lab-stream-mtx 2>&1 | tail -3
2026/10/09 01:00:01 INF [RTMP] [conn 192.168.144.4:54900] opened
2026/10/09 01:00:02 INF [path live] stream is available and online, 2 tracks (H264, MPEG-4 Audio)
2026/10/09 01:00:02 INF [RTMP] [conn 192.168.144.4:54900] is publishing to path 'live'

وللإرسال عبر SRT بدل RTMP نغير الرابط والحاوية فقط، فالحاوية في SRT هي TS، وstreamid=publish:live هي الطريقة التي يعرف بها MediaMTX أن هذا الاتصال يرسل إلى المسار live:

docker rm -f lab-stream-pub
docker run -d --name lab-stream-pub --network lab-stream_default -v "$PWD:/work" lab-video-tools:1 \
  sh /work/publish.sh "srt://lab-stream-mtx:8890?streamid=publish:live&pkt_size=1316" mpegts
docker logs lab-stream-mtx 2>&1 | grep -E "SRT|path live" | tail -3
2026/10/09 01:09:54 INF [SRT] [conn 192.168.144.4:32794] opened
2026/10/09 01:09:55 INF [path live] stream is available and online, 2 tracks (H264, MPEG-4 Audio)
2026/10/09 01:09:55 INF [SRT] [conn 192.168.144.4:32794] is publishing to path 'live'

وMediaMTX يحول نفس البث تلقائياً إلى HLS على المنفذ 18888 وإلى WebRTC على المنفذ 18889، وكلاهما مع صفحة مشغل جاهزة على المسار /live. ولنقرأ قائمة HLS الحية بأداة curl نحتاج أن نحفظ Cookie، لأن MediaMTX في هذا الإصدار يرد على الطلب الأول بإعادة توجيه 302 إلى ?cookieCheck=1:

curl -s -L -c cj.txt -b cj.txt http://127.0.0.1:18888/live/index.m3u8
curl -s -L -c cj.txt -b cj.txt http://127.0.0.1:18888/live/main_stream.m3u8 | head -11

والمخرج سوف يكون كما يلي، وهو مع HLS_VARIANT=mpegts HLS_SEGMENT=6s، أي HLS التقليدي بأجزاء 6 ثوان:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-INDEPENDENT-SEGMENTS

#EXT-X-STREAM-INF:BANDWIDTH=2765104,AVERAGE-BANDWIDTH=2765104,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=30.000
main_stream.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:NO
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PROGRAM-DATE-TIME:2026-10-09T01:07:07.89Z
#EXTINF:6.00000,
c40fb9e6d7ce_main_seg0.ts
#EXT-X-PROGRAM-DATE-TIME:2026-10-09T01:07:13.89Z
#EXTINF:6.00000,
c40fb9e6d7ce_main_seg1.ts

لاحظ الفرق عن قائمة الفيديو عند الطلب: لا توجد EXT-X-ENDLIST لأن البث لم ينته، وMEDIA-SEQUENCE يزيد كلما حذف السيرفر جزءاً قديماً من أول القائمة، وPROGRAM-DATE-TIME يربط كل جزء بوقت حقيقي، وALLOW-CACHE:NO تذكير بأن هذه القائمة تتغير ولا تحفظ.

ثم صفحة القياس web/live.html، وهي تعرض صفحة مشغل MediaMTX داخل إطار، وتحتها ساعة المتصفح:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Glass to glass</title>
<style>
  body { margin: 0; background: #111; color: #eee; font: 16px Consolas, monospace; }
  iframe { width: 960px; height: 540px; border: 0; display: block; }
  #clock { font: bold 54px "DejaVu Sans Mono", Consolas, monospace; padding: 8px 20px; }
  #clock small { font-size: 20px; color: #aaa; }
</style>
</head>
<body>
<!-- the player page from MediaMTX: ?src=http://127.0.0.1:18888/live (HLS) or http://127.0.0.1:18889/live (WebRTC) -->
<iframe id="player" allow="autoplay"></iframe>
<div id="clock"></div>
<script>
const src = new URLSearchParams(location.search).get('src');
document.getElementById('player').src = src;
const clock = document.getElementById('clock');
const tick = () => {
  clock.innerHTML = new Date().toISOString().slice(11, 23) + ' <small>browser clock (UTC)</small>';
  requestAnimationFrame(tick);
};
tick();
</script>
</body>
</html>

وفتحنا الصفحة ثلاث مرات مع الإرسال عبر SRT في كل مرة: مرة مع HLS التقليدي بأجزاء 6 ثوان، ومرة مع LL-HLS الذي يأتي افتراضياً في MediaMTX بأجزاء ثانية وأجزاء جزئية 200 ملي ثانية، ومرة مع WebRTC، وفي كل مرة انتظرنا 20 ثانية ثم أخذنا ثلاث لقطات بفاصل 4 ثوان. والصورة التالية لقطة كاملة من WebRTC:

صفحة القياس: الفيديو الحي بأشرطة ألوان ونمط متحرك، وعليه الساعة المطبوعة عند الإرسال 01:02:54.076، وتحته ساعة المتصفح 01:02:55.362
الساعة المطبوعة على الفيديو عند الإرسال، وتحتها ساعة المتصفح في نفس اللحظة

والصورة السابقة كانت أول لقطة بعد الاتصال عبر RTMP، والفرق فيها 1.29 ثانية، ثم نزل إلى 1.01 ثم 0.50 في اللقطتين التاليتين لأن مخزن WebRTC يتقلص بعد أن يستقر الاتصال. أما مع SRT فكانت النتائج كما في الصورة التالية، وهي الجزء السفلي من اللقطة الأولى لكل طريقة:

ثلاثة أشرطة من لقطات الشاشة: HLS بأجزاء 6 ثوان، الساعة على الفيديو 01:17:12.576 وساعة المتصفح 01:17:35.509 أي 23.0 ثانية، وLL-HLS بأجزاء ثانية وأجزاء جزئية 200 ملي ثانية، الساعة على الفيديو 01:10:21.980 وساعة المتصفح 01:10:25.494 أي 3.5 ثانية، وWebRTC عبر WHEP، الساعة على الفيديو 01:11:04.643 وساعة المتصفح 01:11:04.892 أي 0.25 ثانية
نفس البث عبر SRT، وثلاث طرق للمشاهدة: 23 ثانية و3.5 ثانية وربع ثانية

والأرقام من اللقطات الثلاث لكل طريقة:

طريقة المشاهدةاللقطة 1اللقطة 2اللقطة 3
HLS بأجزاء 6 ثوان22.933 ث23.005 ث23.014 ث
LL-HLS بأجزاء ثانية3.514 ث3.695 ث3.961 ث
WebRTC0.249 ث0.268 ث0.256 ث

في النتائج أعلاه لاحظ التالي:

  • HLS التقليدي تأخر قرابة 23 ثانية، وهذا يطابق القاعدة: ثلاث مدد مستهدفة من 6 ثوان هي 18 ثانية، ثم الجزء الذي يجب أن يكتمل قبل أن يظهر في القائمة، وبقية الثواني في التحميل والمخزن.
  • هذا الرقم كان بعد أن عمل البث أكثر من دقيقة وامتلأت القائمة، وعندما فتحنا الصفحة في أول 20 ثانية من بدء البث كان التأخير 7.4 ثانية، ثم 9.3 في محاولة أخرى، لأن القائمة لم يكن فيها إلا جزء أو جزءان فبدأ المشغل من أقرب ما وجد، ثم يبقى على هذا البعد ولا يقترب من الحافة الحية. لذلك قس التأخير دائماً بعد أن يستقر البث.
  • LL-HLS نزل بالتأخير إلى 3.5 أو 4 ثوان دون تغيير في الإرسال، والفرق كله في طريقة التقسيم والقائمة، وفي محاولة سابقة عبر RTMP تراوح بين 1.2 و5 ثوان، أي أنه أقل ثباتاً من HLS التقليدي.
  • WebRTC كان ربع ثانية، ولكن تذكر أن السيرفر والمتصفح على نفس الجهاز، فعلى الانترنت سوف يضاف وقت الشبكة، وما زال أقل من ثانية في أغلب الحالات.
  • التأخير من RTMP أو SRT نفسه صغير هنا لأن الإرسال داخل نفس الجهاز، وفائدة SRT تظهر على اتصال حقيقي يفقد حزماً، حيث يعيد إرسالها دون أن يتوقف كما يفعل TCP في RTMP.

تنظيف التجربة

نحذف مصدر البث بالاسم، ثم Containers ال Compose وشبكتها:

docker rm -f lab-stream-pub
docker compose down

ثم ال Images والملفات إذا لم تعد تحتاجها:

docker image rm bluenviron/mediamtx:1.21.2 nginx:1.29.8-alpine
rm -rf web broken artemis-src.webm cj.txt

أخطاء شائعة

  • إطارات مفتاحية غير منتظمة في البث: أجزاء بأطوال عشوائية وتأخير أكبر وانتقال سيئ بين الجودات، والحل -g بقيمة ثابتة مع -sc_threshold 0 في كل الدرجات.
  • حفظ القائمة الحية في ال Cache: المشاهدون يرون قائمة قديمة فيتوقف البث أو يتأخر، فأعط القوائم الحية مدة قصيرة جداً أو لا تحفظها، وأعط الأجزاء مدة طويلة.
  • نوع الملف الخاطئ: بعض المشغلات ترفض .m3u8 إذا وصل بالنوع text/plain، فحدد application/vnd.apple.mpegurl كما في إعداد nginx أعلاه.
  • نسيان CORS: إذا كانت الأجزاء على دومين مختلف عن الصفحة، مثل CDN أو تخزين كائنات، فيجب أن يرد بترويسة Access-Control-Allow-Origin، وإلا رفض المتصفح الطلبات من hls.js.
  • اختيار WebRTC لكل شيء: التأخير المنخفض جميل، ولكنه لا يمر عبر CDN عادية، فإذا كان المحتوى محاضرة لا تفاعل فيها فـ HLS أرخص وأثبت.
  • قياس التأخير في أول ثوان البث: كما رأينا، الرقم يتغير حتى تمتلئ القائمة.

تدريب عملي

  • أضف إلى السلم درجة رابعة 960×540 بمعدل 2000 kbps، ثم خنق الاتصال إلى 3 ميغابت بدل 1.5 وانظر أي درجة يختار المشغل.
  • أرسل البث من OBS Studio بدل ffmpeg إلى rtmp://127.0.0.1:19350/live، ثم قس التأخير بنفس الصفحة مع LL-HLS وWebRTC، وقارنه بما رأيناه هنا.

الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

  • التحميل التدريجي يعطي كل المشاهدين نفس الملف، والبث المتكيف يقسم الفيديو إلى أجزاء بعدة جودات ويترك المشغل يختار لكل جزء ما يتحمله الاتصال.
  • HLS بقوائم .m3u8 وDASH بقوائم .mpd فكرتهما واحدة، وCMAF يجعلهما يشتركان في نفس الأجزاء.
  • كل جزء يجب أن يبدأ بإطار مفتاحي وفي نفس اللحظة في كل الدرجات، وإلا خرجت الأجزاء بأطوال عشوائية كما رأينا من 1.9 إلى 10.9 ثانية.
  • الإدخال عبر RTMP أو SRT أو WHIP، والتوزيع عبر HLS وDASH وCDN لأنها ملفات HTTP عادية، أو عبر WebRTC للتأخير المنخفض.
  • في تجربتنا: HLS بأجزاء 6 ثوان تأخر 23 ثانية، وLL-HLS قرابة 3.5 إلى 4 ثوان، وWebRTC ربع ثانية، وكل خطوة أقل في التأخير أغلى في التوزيع.
  • DRM يشفر الأجزاء مرة واحدة بمعيار CENC، ويفكها Widevine أو PlayReady أو FairPlay داخل الجهاز، ولا تضفه إلا إذا طلبه أصحاب الحقوق.

وفي مقالات قادمة بإذن الله نبني على هذه المفاهيم منصة فيديو كاملة مثل Netflix، وتطبيق بث مباشر، وتطبيق فيديو قصير.

دليلمفاهيم الفيديو: الحاوية والترميز Codec ومعدل البتما الذي يحدد حجم ملف الفيديو وجودته، ولماذا يعمل ملف في متصفح ولا يعمل في آخر، مع تجربة حقيقية تقيس الفرق بين الترميزات.دليلكيف تعمل شبكة توصيل المحتوى CDN وما الذي تحفظهلماذا يبطؤ موقعك على الزائر البعيد، وكيف تقرب شبكة CDN ملفاتك منه، وكيف تمنعها من حفظ الصفحات الخاصة.دليلمكالمات WebRTC خلف NAT: شرح STUN وTURN وICEلماذا تتصل المكالمة ولا يصل الصوت، وكيف تحل STUN وTURN المشكلة دون أن تفتح باباً إلى شبكتك الداخلية.دليلHTTP وHTTPS ببساطة: ماذا يحدث بين الرابط والصفحةماذا يحدث بين كتابة الرابط وظهور الصفحة، وكيف تقرأ الطلب والرد ورموز مثل 502 و504، ولماذا يكلف الاتصال الجديد أكثر.دليلكيف تبني منصة فيديو عند الطلب مثل Netflixكيف يتحول فيديو مرفوع إلى بث يناسب كل شاشة وسرعة اتصال، وما الذي تحتاجه منصتك من التحويل إلى التوصيل والتكلفة.دليلكيف تبني تطبيق بث مباشر مثل Jaco وTwitchكيف يصل بث المذيع إلى آلاف المشاهدين بتأخير ثانية أو ثانيتين، ومعه دردشة وهدايا لا تتوقف عند الذروة.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال، وتنفيذ التجربة بأداة ffmpeg 9.0.2 وMediaMTX 1.21.2 وnginx 1.29.8 وhls.js 1.7.3 وChrome بلا واجهة Headless، وDocker Engine 29.8.0 على Windows 11 مع Docker Desktop.
Could not load content
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.