> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# كيف تبني تطبيق بث مباشر مثل Jaco وTwitch
- URL: https://arabroot.io/articles/كيف-تبني-تطبيق-بث-مباشر-مثل-jaco-و-twitch/
- Published: 2026-10-09T16:00:00.000Z
- Updated: 2026-10-10T05:23:19.000Z
- Description: كيف يصل بث المذيع إلى آلاف المشاهدين بتأخير ثانية أو ثانيتين، ومعه دردشة وهدايا لا تتوقف عند الذروة.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, مجالات متخصصة, الشبكات والوصول, DevOps وCI/CD, الاستضافة الذاتية

في مباراة نهائية يكتب آلاف المشاهدين «هدف» في الدردشة في نفس الثانية، ويرسل بعضهم هدايا تظهر رسومها فوق الفيديو، والمذيع يرد عليهم بالاسم، وكل هذا يعمل فقط إذا كان البث متأخراً عن الحدث ثانية أو ثانيتين، لأن المذيع الذي يرد على تعليق كتب قبل نصف دقيقة يبدو وكأنه يتحدث مع نفسه. وهنا يختلف البث المباشر عن منصة الفيديو عند الطلب التي شرحناها في [كيف تبني منصة فيديو عند الطلب مثل Netflix](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D9%85%D9%86%D8%B5%D8%A9-%D9%81%D9%8A%D8%AF%D9%8A%D9%88-%D8%B9%D9%86%D8%AF-%D8%A7%D9%84%D8%B7%D9%84%D8%A8-%D9%85%D8%AB%D9%84-netflix/): هناك كانت المعالجة تحدث مرة واحدة وتنتظر دقائق، وهنا يجب أن تحدث المعالجة والتوصيل والدردشة كلها في الوقت الحقيقي، ولا يوجد ملف كامل تحسن ترميزه ولا ليلة تملأ فيها ال Cache.

وسوف نأخذ مثالين: [JACO جاكو](https://apps.apple.com/sa/app/jaco-%D8%AC%D8%A7%D9%83%D9%88-live-streaming/id6447761097?ref=arabroot.io)، وهو تطبيق بث مباشر عربي تذكر صفحته في App Store السعودي أن ناشره شركة JACO ARABIAN COMPANY FOR INFORMATION TECHNOLOGY، وأنه نشر على المتجر في 16 يونيو 2023، ويصف نفسه بأنه منصة اجتماعية للبث المباشر والفيديوهات القصيرة فيها الرياضة والألعاب والموسيقى والترفيه، مع مشاهدة مباريات كرة القدم السعودية مجاناً، والبث من الهاتف أو من برنامج OBS، ومواجهات بين المذيعين يسميها PK، ومحادثات صوتية جماعية ومحادثات خاصة، وتعرض الصفحة نفسها مشتريات داخل التطبيق على شكل حزم عملات Coins، وتذكر ملاحظات الإصدارات هدايا Gifts يرسلها المشاهدون. والمثال الثاني Twitch، وهي منصة بث يرسل لها المذيع بثه بروتوكول RTMP كما تشرح [وثائق المطورين](https://dev.twitch.tv/docs/video-broadcast/?ref=arabroot.io) فيها، ولها دردشة لكل قناة ولها [واجهات](https://dev.twitch.tv/docs/chat/?ref=arabroot.io) للبوتات والإشراف، وعملة اسمها Bits يستخدمها المشاهد للتشجيع Cheering كما في [صفحة المساعدة](https://help.twitch.tv/s/article/guide-to-cheering-with-bits?ref=arabroot.io). ولن نتحدث عن تفاصيل داخلية في أي من التطبيقين لم تنشرها الشركتان، وإنما نبني تطبيقاً بنفس الميزات من قطع مفتوحة المصدر.

ومفاهيم البث نفسها، مثل RTMP وSRT وHLS وزمن التأخير من جهة المشغل، لها مقال مستقل في قسم [الفيديو والبث المباشر](https://arabroot.io/start/#stage-video)، فلن نكرر شرحها هنا:

[![](https://arabroot.io/content/images/2026/10/streaming-concepts-cover-cover.webp)دليلكيف يعمل بث الفيديو المباشر وعند الطلب؟كيف يصل الفيديو إلى كل مشاهد بجودة تناسب سرعة اتصاله، ولماذا يتأخر البث المباشر عشرين ثانية أحياناً وأقل من ثانية أحياناً أخرى.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D8%A8%D8%AB-%D8%A7%D9%84%D9%81%D9%8A%D8%AF%D9%8A%D9%88/)[![](https://arabroot.io/content/images/2026/10/video-basics-cover-cover.webp)دليلمفاهيم الفيديو: الحاوية والترميز Codec ومعدل البتما الذي يحدد حجم ملف الفيديو وجودته، ولماذا يعمل ملف في متصفح ولا يعمل في آخر، مع تجربة حقيقية تقيس الفرق بين الترميزات.](https://arabroot.io/articles/%D9%85%D9%81%D8%A7%D9%87%D9%8A%D9%85-%D8%A7%D9%84%D9%81%D9%8A%D8%AF%D9%8A%D9%88-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)

وسوف نناقش في هذا المقال ما يلي:

- الصورة الكاملة لتطبيق البث المباشر، ولماذا يحتاج مسارين للتوصيل.
- المعمارية الكاملة: المكونات ونموذج البيانات والواجهات وتقدير السعة ومراحل النمو، وماذا يحدث عندما يتعطل كل جزء.
- الاستقبال Ingest ببروتوكولات RTMP وSRT وWHIP، والتحويل في الوقت الحقيقي Real-time Transcoding.
- التوصيل بزمن تأخير قصير: LL-HLS عبر ال CDN، وWebRTC عبر خوادم SFU، ومتى تختار كلاً منهما.
- الدردشة لآلاف المشاهدين بتوزيع الرسائل Fan-out على WebSocket.
- الهدايا والعملات كدفتر حسابات، والإشراف على المحتوى، والتوسع عند الذروة، والتكلفة.
- تجربة عملية: بث من ffmpeg بدل OBS إلى MediaMTX، ومشاهدته عبر WebRTC وLL-HLS جنباً إلى جنب مع قياس التأخير، ودردشة على سيرفرين نرسل فيها الرسائل إلى آلاف المشاهدين الوهميين ونقيس زمن وصولها.

## الصورة الكاملة: من كاميرا المذيع إلى آلاف الشاشات

المذيع يرسل بثاً واحداً بأعلى جودة يتحملها اتصاله، والسيرفر يستقبله ويحوله في نفس اللحظة إلى عدة جودات، ثم يوزعه في مسارين: مسار WebRTC بتأخير أقل من ثانية للتفاعل الحي، مثل الضيوف على المسرح ومواجهات المذيعين، ومسار LL-HLS عبر ال CDN بتأخير ثوان قليلة يتحمل أي عدد من المشاهدين بتكلفة أقل، وحول الفيديو خدمات لا تقل أهمية عنه: الدردشة والهدايا والإشراف والمقاييس والتسجيل، والصورة التالية تبين ذلك:

![المذيع بهاتفه أو ببرنامج OBS يرسل البث بأحد البروتوكولات RTMP أو SRT أو WHIP إلى سيرفر الاستقبال، ومنه إلى محول في الوقت الحقيقي يصنع عدة جودات، ثم مساران: الأخضر إلى WebRTC SFU بتأخير أقل من ثانية للضيوف ومواجهات PK، والأزرق إلى LL-HLS عبر CDN بتأخير ثوان قليلة للجمهور الكبير والتسجيل، وفي الأسفل الخدمات حول الفيديو: الدردشة والهدايا والعملات والإشراف والمقاييس والتسجيل](https://arabroot.io/content/images/2026/10/build-live-streaming-01-overview.webp)

بث واحد يدخل، ومساران يخرجان، وخمس خدمات حول الفيديو

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

## المعمارية الكاملة: كيف تتصل الأجزاء ببعضها؟

الصورة السابقة تبين طريق الفيديو، أما التطبيق الكامل فيحتاج أن تعرف أين تعمل كل خدمة، وأين تحفظ بياناتها، وأي اتصال ينتظر الرد وأي حدث يذهب إلى قناة أو طابور ويكمل وحده، لأن البث المباشر يجمع نوعين مختلفين تماماً من الحركة: وسائط Media تتدفق كل ثانية ولا تمر بقاعدة بيانات، وطلبات تحكم Control مثل مفتاح البث والهدايا والدردشة. والصورة التالية تبين المعمارية في خمسة أعمدة: الكلاينت Clients، ثم الحافة Edge، ثم الخدمات Services، ثم البيانات Data، ثم ال Workers والمزودين الخارجيين:

![خمسة أعمدة: في الكلاينت المذيع ببرنامج OBS أو تطبيق الهاتف والمشاهدون. وفي الحافة نقطة الاستقبال Ingest ببروتوكولات RTMP وSRT وWHIP، وخادم WebRTC SFU من LiveKit، ونقطة CDN لأجزاء LL-HLS، وبوابة API. وفي الخدمات خادم الوسائط MediaMTX الذي يسجل ويحول بال ffmpeg ويصنع أجزاء LL-HLS ويخدم WHEP، وواجهة القنوات، وخدمة الهدايا والمحفظة، وسيرفرات الدردشة. وفي البيانات التسجيلات في Garage، وعينات الإطارات في قائمة Valkey، والقنوات والدفتر في PostgreSQL، وقنوات الغرف في Valkey pub/sub. وفي العمود الأخير Workers للإشراف بمصنفات على الإطارات، وفريق المراجعة الذي يوقف البث ويبطل المفتاح، ومتاجر التطبيقات للتحقق من الإيصالات. الأسهم الخضراء للفيديو، والرمادية طلبات متزامنة، والبنفسجية المتقطعة أحداث غير متزامنة، والبرتقالية مزود خارجي](https://arabroot.io/content/images/2026/10/build-live-streaming-08-architecture.webp)

الفيديو يتدفق في الأعلى دون أن يلمس قاعدة بيانات، والدردشة والهدايا في الأسفل تمر بالبوابة

وفي الصورة أعلاه لاحظ ثلاث قواعد: أن الفيديو لا يمر بأي خدمة كتبتها أنت، فهو يدخل إلى نقطة الاستقبال ثم خادم الوسائط ثم يخرج إلى ال SFU وال CDN، وخدماتك تتدخل مرة واحدة فقط عند بداية البث، عندما يسأل خادم الوسائط واجهة القنوات هل مفتاح البث صحيح. وأن الدردشة لا تكتب في قاعدة بيانات، فالرسالة تمر في قناة Pub/Sub وتختفي، كما شرحنا في [قسم الدردشة](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A8%D8%AB-%D9%85%D8%A8%D8%A7%D8%B4%D8%B1-%D9%85%D8%AB%D9%84-jaco-%D9%88-twitch/#chat). وأن الهدية هي الشيء الوحيد الذي يكتب بمعاملة Transaction في قاعدة بيانات قبل أن يراه أحد، ثم ينشر حدثها في قناة الغرفة.

### المكونات ومسؤولية كل منها

| المكون                                  | مسؤوليته                                                                             | أداة مفتوحة المصدر مقترحة                                                                                              | لماذا هي                                                                                                                                                                                                                         |
| --------------------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| برنامج المذيع                           | ترميز الكاميرا والشاشة وإرسال البث مع مفتاحه                                         | [OBS Studio](https://github.com/obsproject/obs-studio?ref=arabroot.io) (GPL-2.0)، ومكتبة بث داخل تطبيق الهاتف          | أشهر برنامج بث على الحاسوب، ويرسل RTMP وSRT وWHIP                                                                                                                                                                                |
| الاستقبال وخادم الوسائط Ingest + Origin | استقبال البث، والتحويل الحي، وصنع أجزاء LL-HLS، وخدمة WHEP، والتسجيل                 | [MediaMTX](https://github.com/bluenviron/mediamtx?ref=arabroot.io) (MIT)                                               | ملف تنفيذي واحد يفعل كل ذلك كما في التجربة، ويسأل خدمتك عن المفتاح بطريقة [المصادقة عبر HTTP](https://mediamtx.org/docs/features/authentication?ref=arabroot.io)                                                                 |
| التحويل الحي Live Transcoding           | صنع 480p و360p من بث المذيع في الوقت الحقيقي                                         | [FFmpeg](https://ffmpeg.org/legal.html?ref=arabroot.io) بإعدادات zerolatency                                           | يشغله MediaMTX بأمر runOnAvailable كما في التجربة                                                                                                                                                                                |
| خادم WebRTC SFU                         | توزيع البث بأقل من ثانية على الضيوف والمواجهات                                       | [LiveKit](https://github.com/livekit/livekit?ref=arabroot.io) (Apache-2.0)                                             | يدعم طبقات Simulcast، ومعه حزم SDK للهاتف والمتصفح                                                                                                                                                                               |
| ال CDN                                  | توزيع أجزاء LL-HLS على الجمهور الكبير                                                | [nginx](https://nginx.org/LICENSE?ref=arabroot.io) (BSD-2-Clause) كنقطة Cache أمام ال Origin في البداية، ثم CDN تجارية | الأجزاء ملفات صغيرة تحفظ لثوان، لكن اختبر الطلبات المعلقة على أي Cache قبل أن تعتمد عليه                                                                                                                                         |
| بوابة ال API Gateway                    | TLS، والتحقق من التوكن، وحدود الطلبات، وتمرير اتصالات WebSocket                      | [Traefik](https://github.com/traefik/traefik?ref=arabroot.io) (MIT)                                                    | يمرر WebSocket دون إعداد خاص، ويصدر الشهادات تلقائياً                                                                                                                                                                            |
| واجهة القنوات Channel API               | مفاتيح البث، وحالة القناة، والمتابعة، والرد على سؤال خادم الوسائط عن المفتاح         | كودك، ومعه [Keycloak](https://github.com/keycloak/keycloak?ref=arabroot.io) (Apache-2.0) لتسجيل الدخول                 | منطق خاص بمنتجك، والهوية جاهزة                                                                                                                                                                                                   |
| سيرفرات الدردشة                         | اتصالات WebSocket، وتوزيع الرسائل على كل من في الغرفة، وحد الرسائل لكل مستخدم        | كودك كما في التجربة، أو [Centrifugo](https://github.com/centrifugal/centrifugo?ref=arabroot.io) (Apache-2.0)           | الثاني خادم جاهز للرسائل اللحظية بالقنوات، إذا لم ترد كتابة التوزيع بنفسك                                                                                                                                                        |
| قنوات الغرف Pub/Sub                     | ربط سيرفرات الدردشة ببعضها، ونشر أحداث الهدايا                                       | [Valkey Pub/Sub](https://valkey.io/topics/pubsub/?ref=arabroot.io) (BSD-3-Clause)                                      | استهلك 0.28% من المعالج في التجربة مع 4,000 مشاهد                                                                                                                                                                                |
| الهدايا والمحفظة                        | شراء العملات بعد التحقق من الإيصال، وإرسال الهدية بمعاملة واحدة، وتعليق أرباح المذيع | كودك على [PostgreSQL](https://www.postgresql.org/about/licence/?ref=arabroot.io) (PostgreSQL License)                  | المعاملات والقيود الفريدة هي ما يمنع الخصم المكرر، كما في [مقال المحفظة الرقمية](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D9%85%D8%AD%D9%81%D8%B8%D8%A9-%D8%B1%D9%82%D9%85%D9%8A%D8%A9-ledger/) |
| التسجيلات Recordings                    | حفظ البث لإعادة المشاهدة والمراجعة                                                   | [Garage](https://git.deuxfleurs.fr/Deuxfleurs/garage?ref=arabroot.io) (AGPL-3.0)                                       | تخزين كائنات متوافق مع S3 على أجهزتك                                                                                                                                                                                             |
| الإشراف Moderation                      | عينات من الإطارات على مصنفات، وطابور مراجعة بشرية، وإيقاف البث                       | قائمة في Valkey وWorkers بمصنفاتك، وفريق مراجعة                                                                        | المصنفات تفصل الواضح، والإنسان يقرر في غير الواضح                                                                                                                                                                                |
| متاجر التطبيقات                         | التحقق من إيصالات شراء العملات                                                       | خدمة خارجية من Apple وGoogle، ولا يوجد بديل مفتوح لها                                                                  | لذلك رسمناها بإطار متقطع في الصورة                                                                                                                                                                                               |

### نموذج البيانات

في التجربة العملية كان المسار في MediaMTX اسمه `live`، والنسخة المحولة `live/480p`، وغرفة الدردشة `match-1` وقناتها في Valkey `room:match-1`، والرسالة JSON فيها `user` و`text` و`sent_ns` و`via`. وفي الإنتاج يصبح لكل قناة اسم قصير Slug واحد يستخدم في المسار والغرفة معاً، وتحفظ القنوات والبث والعملات في PostgreSQL، والعملات بطريقة القيد المزدوج التي شرحناها في [مقال المحفظة الرقمية](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D9%85%D8%AD%D9%81%D8%B8%D8%A9-%D8%B1%D9%82%D9%85%D9%8A%D8%A9-ledger/):

```sql
-- PostgreSQL: channels, streams and the coins ledger (journal + postings, as in the wallet article)
CREATE TABLE channels (
  id              bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  owner_id        uuid NOT NULL,
  slug            text NOT NULL UNIQUE,        -- media path live/<slug>, chat room room:<slug>
  stream_key_hash text NOT NULL UNIQUE,        -- a hash, never the key itself
  status          text NOT NULL DEFAULT 'offline' CHECK (status IN ('offline', 'live', 'suspended'))
);
CREATE TABLE streams (                         -- one row per broadcast
  id            bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  channel_id    bigint NOT NULL REFERENCES channels (id),
  protocol      text NOT NULL CHECK (protocol IN ('rtmp', 'srt', 'whip')),
  started_at    timestamptz NOT NULL DEFAULT now(),
  ended_at      timestamptz,
  recording_key text,                          -- recordings/<slug>/<stream id>/
  peak_viewers  int
);
CREATE TABLE journal (                         -- one row per purchase, gift, payout or reversal
  id              bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  idempotency_key text NOT NULL UNIQUE,        -- sent by the app with every gift
  kind            text NOT NULL CHECK (kind IN ('purchase', 'gift', 'payout', 'reversal')),
  stream_id       bigint REFERENCES streams (id),
  created_at      timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE postings (                        -- never updated: a gift is -100 for the viewer, +100 held for the streamer
  journal_id bigint NOT NULL REFERENCES journal (id),
  account    text   NOT NULL,                  -- coins:<user>, earnings_held:<streamer>, store:clearing
  amount     bigint NOT NULL CHECK (amount <> 0)
);
CREATE TABLE purchases (                       -- the same store receipt is credited once
  store          text   NOT NULL CHECK (store IN ('app_store', 'google_play')),
  transaction_id text   NOT NULL,
  user_id        uuid   NOT NULL,
  coins          int    NOT NULL CHECK (coins > 0),
  journal_id     bigint NOT NULL REFERENCES journal (id),
  PRIMARY KEY (store, transaction_id)
);
-- Valkey:   room:<slug>        pub/sub channel, messages {"user", "text", "sent_ns", "via"} (lab: room:match-1)
--           moderation:frames   list of sampled frames waiting for the classifiers
--           viewers:<slug>      viewer counter shown in the app
-- MediaMTX: live/<slug> from the streamer, live/<slug>/480p from the transcoder (lab: live and live/480p)
```

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

- رسائل الدردشة ليست في أي جدول، فإذا احتجت سجلاً لها للإشراف أو للأنظمة فاكتبها بشكل غير متزامن إلى سجل أحداث، ولا تجعل الكتابة جزءاً من طريق الرسالة إلى المشاهدين.
- مفتاح البث محفوظ كهاش Hash، فإذا تسربت القاعدة لا يستطيع أحد أن يبث على قنوات غيره.
- المفتاح الأساسي `(store, transaction_id)` في `purchases` يمنع إضافة نفس الإيصال مرتين، و `idempotency_key` الفريد يمنع خصم نفس الهدية مرتين.

### الواجهات الرئيسية

| الطريقة والمسار                                                                         | ما تفعله                                                                                  | ملاحظات                                                           |
| --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| POST /channels/{id}/stream-key                                                          | تصدر مفتاح بث جديداً وتبطل القديم، وتعيد المفتاح مرة واحدة فقط                            | لصاحب القناة، والقاعدة تحفظ الهاش                                 |
| POST /internal/ingest-auth                                                              | يسألها MediaMTX عند كل بث أو قراءة، فتتحقق من المفتاح وحالة القناة، والرد 20x يعني السماح | داخلية فقط، ولا تمر بالبوابة العامة                               |
| rtmp://ingest/live/{slug} وsrt://…?streamid=publish:live/{slug} وPOST /live/{slug}/whip | إرسال البث بأحد البروتوكولات الثلاثة                                                      | المسارات نفسها التي في التجربة، مع اسم القناة                     |
| GET /live/{slug}/index.m3u8 وPOST /live/{slug}/whep                                     | المشاهدة عبر LL-HLS من ال CDN، أو عبر WebRTC من ال SFU                                    | الأول للجمهور، والثاني لمن يحتاج أقل من ثانية                     |
| WS /chat/{slug}                                                                         | اتصال الدردشة، وكل رسالة تنشر في room:{slug}                                              | حد للرسائل لكل مستخدم، وحد لحجم الرسالة كما في chat.py            |
| POST /gifts                                                                             | تخصم العملات من المشاهد وتضيفها معلقة للمذيع في معاملة واحدة، ثم تنشر حدث الهدية          | ترويسة Idempotency-Key إلزامية، والطلب المكرر يعيد النتيجة الأولى |
| POST /purchases/verify                                                                  | تتحقق من الإيصال مع المتجر، ثم تضيف العملات مرة واحدة                                     | الإيصال المكرر يعيد نفس النتيجة دون إضافة                         |
| POST /reports                                                                           | بلاغ من مشاهد على بث أو رسالة، يذهب إلى طابور المراجعة                                    | البلاغات الكثيرة على بث واحد ترفع أولويته                         |

### تقدير السعة

لنفرض تطبيقاً في بدايته الناجحة، وكل رقم هنا افتراض نكتبه صراحة، إلا ما أخذناه من التجربة العملية، فغيره بأرقامك قبل أن تعتمد النتيجة:

- افتراض: 200 قناة تبث في نفس الوقت في ساعة الذروة، و5,000 مشاهد في نفس الوقت، وغرفة مباراة كبيرة فيها 10,000 مشاهد يكتبون 20 رسالة في الثانية كما في [قسم الدردشة](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A8%D8%AB-%D9%85%D8%A8%D8%A7%D8%B4%D8%B1-%D9%85%D8%AB%D9%84-jaco-%D9%88-twitch/#chat).
- افتراض: المذيع يرسل 2.5 ميجابت في الثانية كما في التجربة، ومتوسط ما يستقبله المشاهد 1.5 ميجابت لخليط الجودات.
- افتراض: نحول فقط القنوات التي فيها 20 مشاهداً أو أكثر، ونقدرها ب 60 قناة، ولكل منها جودتان إضافيتان.
- من التجربة: تحويل جودة واحدة استهلك قرابة 0.9 من نواة، وسيرفر الدردشة بلغة Python امتلأت نواته عند قرابة 20,000 رسالة مكتوبة في الثانية.
- افتراض: هدية واحدة لكل 100 مشاهد في الدقيقة، وتتضاعف 20 مرة عند الهدف، وساعتان من البث في اليوم لكل قناة من 200 قناة.

```text
ingest bandwidth     200 channels x 2.5 Mbit/s            = 500 Mbit/s in
transcoding          60 channels x 2 renditions x ~0.9 core = ~108 cores (only while they are live)
egress at peak       5,000 viewers x 1.5 Mbit/s           = 7.5 Gbit/s
egress per peak hour 7.5 Gbit/s x 3600 / 8                = ~3.4 TB
LL-HLS requests      5,000 x (5 parts/s + 5 playlist reloads/s) = ~50,000 requests/s at the CDN
chat fan-out         10,000 viewers x 20 messages/s       = 200,000 writes/s to sockets
chat servers         200,000 / 20,000 per full core       = 10 cores at 100%, so plan ~20 processes
Valkey pub/sub       20 messages/s x 20 chat servers      = 400 deliveries/s (the lab: 0.28% CPU)
gifts, normal        5,000 / 100 per minute               = ~0.8 gifts/s
gifts, goal spike    0.8 x 20                             = ~17 transactions/s = 17 journal + 34 postings rows/s
recordings           200 x 2 h x 2.5 Mbit/s x 3600 / 8    = ~450 GB per day = ~164 TB per year if kept forever
```

ولاحظ أين تقع الأرقام الكبيرة: التحويل الحي يكلف أنوية ما دامت القنوات تبث، والنقل الخارج 3.4 تيرابايت في كل ساعة ذروة، والدردشة 200,000 كتابة في الثانية في غرفة واحدة، أما قاعدة البيانات فتكتب 17 معاملة في الثانية في أسوأ لحظة، وهذا لا يحتاج أكثر من PostgreSQL واحد جيد. والتسجيلات تحتاج سياسة حذف من اليوم الأول، فالاحتفاظ بها 30 يوماً بدل الأبد يجعلها قرابة 13.5 تيرابايت بدل 164.

### مراحل النمو

| الجزء          | MVP: سيرفر أو اثنان                                                        | النمو: آلاف المشاهدين في نفس الوقت                                                                      | الحجم الكبير: ملايين المشاهدين                                                          |
| -------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| الوسائط        | MediaMTX واحد يستقبل ويحول جودة واحدة ويخدم LL-HLS، أو Owncast لقناة واحدة | نقاط استقبال منفصلة، وسيرفرات تحويل للقنوات التي فيها مشاهدون فقط، وCDN أمام ال Origin، وLiveKit للضيوف | نقاط استقبال في عدة مناطق قريبة من المذيعين، وسلسلة SFU، وأكثر من CDN، وترميز في العتاد |
| الدردشة        | سيرفر واحد مع Valkey                                                       | عدة سيرفرات خلف قناة Pub/Sub، وحد للرسائل لكل مستخدم                                                    | توزيع الغرف على عدة عقد Valkey، وعرض عينة من الرسائل في الغرف الضخمة                    |
| الهدايا        | تؤجل حتى تراجع أنظمة الدفع، أو دفتر بسيط بقيد مزدوج                        | خدمة محفظة منفصلة، وتحقق من الإيصالات، وتعليق الأرباح قبل الصرف                                         | فريق لمكافحة الاحتيال، ومطابقة يومية مع المتاجر                                         |
| الإشراف        | زر بلاغ، وإيقاف البث يدوياً من لوحة الإدارة                                | عينات إطارات على مصنفات، وطابور مراجعة بأولويات                                                         | فريق مراجعة على مدار اليوم، وسياسات بحسب الدولة                                         |
| قاعدة البيانات | PostgreSQL واحد ونسخة احتياطية يومية                                       | نسخة قراءة للقنوات والمتابعة، والدفتر في قاعدة منفصلة                                                   | فصل الدفتر في خدمة مستقلة بفريقها، وتقسيم بيانات المتابعة                               |

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

### ماذا لو تعطل جزء؟

| ماذا لو تعطل...؟                   | الأثر                                                                                                                                                                                                                                                          | كيف تخففه                                                                                               |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| نقطة الاستقبال                     | ينقطع بث كل المذيعين المتصلين بها                                                                                                                                                                                                                              | أكثر من نقطة خلف اسم واحد في ال DNS، والمفتاح صالح على أي منها، وإعادة الاتصال التلقائي في برنامج البث  |
| التحويل الحي أبطأ من الزمن الحقيقي | يزيد التأخير ثانية بعد ثانية حتى يصبح البث بلا معنى                                                                                                                                                                                                            | راقب سرعة التحويل، وعند الضغط احذف جودة وأرسل الأصل كما هو                                              |
| ال CDN أو ال Origin                | يتوقف مشاهدو LL-HLS                                                                                                                                                                                                                                            | CDN ثانية يعرفها المشغل، وOrigin ثان يستقبل نفس البث                                                    |
| قناة Valkey Pub/Sub                | كل سيرفر دردشة يوزع رسائله على من اتصل به فقط                                                                                                                                                                                                                  | نسخة احتياطية مع تحويل تلقائي، وليس هناك ما تستعيده لأن الدردشة لا تحفظ أصلاً                           |
| سيرفرات الدردشة عند الامتلاء       | الرسائل تتأخر ثوان، كما رأينا مع 4,000 مشاهد في [التجربة](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A8%D8%AB-%D9%85%D8%A8%D8%A7%D8%B4%D8%B1-%D9%85%D8%AB%D9%84-jaco-%D9%88-twitch/#lab-chat) | سيرفرات إضافية قبل الحدث، وعينة من الرسائل في الغرف الضخمة                                              |
| قاعدة المحفظة                      | تفشل الهدايا والشراء                                                                                                                                                                                                                                           | وضع متدهور يخفي زر الهدايا، والتطبيق يعيد الطلب بنفس Idempotency-Key، ولا تظهر الرسوم قبل نجاح المعاملة |

## الاستقبال Ingest: كيف يصل بث المذيع إلى السيرفر؟

المذيع يرسل بثه إلى نقطة استقبال Ingest، ومعه مفتاح البث Stream Key الذي يعرف القناة ويثبت أن المرسل صاحبها، ويتحقق منه خادم الوسائط بسؤال واجهة القنوات Channel API في [المعمارية أعلاه](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A8%D8%AB-%D9%85%D8%A8%D8%A7%D8%B4%D8%B1-%D9%85%D8%AB%D9%84-jaco-%D9%88-twitch/#architecture)، فاعتبر هذا المفتاح كلمة مرور، وغيره إذا ظهر على الشاشة بالخطأ. وهناك ثلاثة بروتوكولات منتشرة، ويستطيع خادم الوسائط الواحد أن يستقبلها كلها:

- RTMP: الأقدم والأكثر انتشاراً، وتدعمه كل برامج البث مثل OBS، وTwitch تستقبل به كما في [وثائقها](https://dev.twitch.tv/docs/video-broadcast/?ref=arabroot.io)، ويعمل فوق TCP، لذلك يتأخر ويتراكم عندما تفقد الشبكة بعض الحزم.
- SRT: بروتوكول مفتوح المصدر يعمل فوق UDP، ويصحح الحزم المفقودة بإعادة الإرسال خلال مهلة تحددها أنت، لذلك يناسب البث من شبكات غير مستقرة مثل الهاتف في الملعب، ومكتبته الرسمية [Haivision/srt](https://github.com/Haivision/srt?ref=arabroot.io) برخصة MPL-2.0.
- WHIP: معيار من IETF صدر في مارس 2025 باسم [RFC 9725](https://www.rfc-editor.org/rfc/rfc9725.html?ref=arabroot.io)، يرسل البث بتقنية WebRTC بطلب HTTP واحد، فيمكن للمتصفح نفسه أن يبث دون أي برنامج، وبتأخير منخفض جداً من البداية.

وفي تطبيق مثل JACO يبث أغلب المذيعين من الهاتف، فيدمج تطبيق الهاتف مكتبة بث ترسل بأحد هذه البروتوكولات، بينما يستخدم محترفو الألعاب برنامج OBS على الحاسوب، وهذا ما تذكره صفحة التطبيق من دعم OBS.

## التحويل في الوقت الحقيقي Real-time Transcoding

المشاهد على شبكة ضعيفة يحتاج جودة أقل من التي أرسلها المذيع، لذلك يحول السيرفر البث إلى عدة جودات كما في منصة الفيديو عند الطلب، لكن بفرق كبير: التحويل هنا يجب أن يكون أسرع من الزمن الحقيقي دائماً، فكل ثانية من البث يجب أن تنتهي معالجتها في أقل من ثانية، وإلا تراكم التأخير، لذلك تستخدم إعدادات أسرع وأقل كفاءة في الضغط مثل `-preset veryfast -tune zerolatency` في x264\. وفي التجربة العملية سوف ترى أن تحويل بث 720p إلى 480p واحدة استهلك قرابة نواة معالج كاملة، وهذا هو الرقم الذي تحسب به التكلفة: كل قناة نشطة تحتاج من الأنوية بعدد الجودات تقريباً، طوال مدة البث، سواء شاهدها ألف شخص أو شخص واحد. لذلك تحول أغلب المنصات القنوات الكبيرة فقط، أو تستخدم شرائح الترميز في العتاد Hardware Encoders، أو تترك المذيع يرسل عدة جودات بنفسه كما سنرى في [طبقات Simulcast](#webrtc).

## التوصيل بزمن تأخير قصير

زمن التأخير الذي يهمك هو من الكاميرا إلى الشاشة Glass-to-Glass Latency، وهو مجموع زمن الترميز والإرسال والتحويل والتجزئة والتوصيل والذاكرة المؤقتة في المشغل، والصورة التالية تقارن ثلاث طرق للتوصيل:

![رسم بأشرطة أفقية لزمن التأخير من الكاميرا إلى الشاشة: HLS التقليدي بمقاطع 6 ثوان قرابة 26 ثانية مقسمة إلى ترميز ومقطع وذاكرة ثلاثة مقاطع، وLL-HLS في التجربة قرابة 4.3 ثانية، وWebRTC في التجربة قرابة ثانية واحدة](https://arabroot.io/content/images/2026/10/build-live-streaming-02-latency.webp)

كل مقطع ينتظره المشغل يضيف مدته إلى التأخير

والسبب في تأخر HLS التقليدي أن المعيار [RFC 8216](https://www.rfc-editor.org/rfc/rfc8216.html?ref=arabroot.io) يطلب من المشغل في البث المباشر ألا يبدأ من مقطع أقرب من ثلاثة أضعاف المدة المستهدفة Target Duration من نهاية القائمة، حتى لا يتوقف، فمع مقاطع 6 ثوان يكون المشاهد متأخراً 18 ثانية على الأقل قبل أن تحسب زمن الترميز وكتابة المقطع.

### LL-HLS: نفس HLS بأجزاء أصغر

الحل الأول هو HLS منخفض التأخير Low-Latency HLS، وهو موصوف في [مسودة الإصدار الثاني من معيار HLS](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis?ref=arabroot.io)، وفكرته أن يقسم كل مقطع إلى أجزاء Parts صغيرة، مثل 200 مللي ثانية، يطلبها المشغل فور كتابتها، وأن يعلق السيرفر طلب قائمة التشغيل حتى يظهر الجزء التالي بدل أن يسأل المشغل كل ثانية، وأن تذكر القائمة اسم الجزء القادم مسبقاً حتى يطلبه المشغل قبل أن يكتمل، والصورة التالية تبين ذلك بأسماء الملفات الحقيقية من التجربة:

![ثلاثة مقاطع مدة كل منها ثانية: seg8 وseg9 مكتملان وseg10 قيد الكتابة، وتحت كل مقطع خمسة أجزاء مدة كل منها 200 مللي ثانية من part10 إلى part24، والجزء part22 عليه علامة PRELOAD-HINT، وفي الأسفل مشغل hls.js يرسل طلب قائمة التشغيل مع _HLS_msn=10 و_HLS_part=2، وسيرفر MediaMTX يعلق الطلب حتى يجهز part22 ثم يرد بالقائمة الجديدة والجزء](https://arabroot.io/content/images/2026/10/build-live-streaming-03-ll-hls.webp)

الأجزاء الصغيرة والطلب المعلق يقربان المشاهد من الحدث

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

### WebRTC: أقل من ثانية، وثمن مختلف

الحل الثاني هو WebRTC، وهو نفس التقنية التي تعمل بها مكالمات الفيديو، فلا توجد مقاطع ولا ملفات، وإنما حزم RTP تصل كما هي ويعرضها المتصفح فوراً. وقد شرحنا كيف يجد الطرفان طريقاً بينهما خلف NAT، ولماذا تحتاج سيرفر TURN، والفرق بين Mesh وSFU وMCU، في [مكالمات WebRTC خلف NAT](https://arabroot.io/articles/%D9%85%D9%83%D8%A7%D9%84%D9%85%D8%A7%D8%AA-webrtc-%D8%AE%D9%84%D9%81-nat-%D8%B4%D8%B1%D8%AD-ice-%D9%88-stun-%D9%88-turn/#topologies)، فلن نكرره هنا. وفي البث يستخدم دائماً خادم SFU، أي Selective Forwarding Unit، وهو سيرفر يستقبل البث مرة ويرسل نسخة منه لكل مشاهد دون أن يعيد ترميزه. وللمشاهدين بالآلاف تضاف فكرتان، تبينهما الصورة التالية:

![المذيع يرسل ثلاث طبقات Simulcast بجودات 1080p و720p و360p إلى Origin SFU، ومنه إلى ثلاثة Edge SFU في الرياض وجدة ودبي، وكل خادم يرسل لكل مشاهد الطبقة التي يتحملها اتصاله](https://arabroot.io/content/images/2026/10/build-live-streaming-04-sfu.webp)

ال SFU لا يحول الفيديو، وإنما يختار لكل مشاهد طبقة جاهزة

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

وقد يتساءل البعض: إذا كان WebRTC أسرع فلماذا لا نستخدمه للجميع؟ والإجابة أن كل مشاهد في WebRTC اتصال دائم مع حالة على السيرفر، ومشفر بمفاتيح خاصة به، فلا تستطيع CDN عادية أن تحفظه وتوزعه كما تفعل مع مقاطع HLS، وبالتالي تحتاج خوادم SFU بعدد المشاهدين تقريباً. لذلك فالتصميم الشائع أن يكون WebRTC لمن يتفاعل فعلاً، مثل المذيع وضيوفه والمواجهات بين مذيعين، وLL-HLS لبقية الجمهور، ومشغل يستطيع الانتقال من الأول إلى الثاني إذا لم يصل اتصال WebRTC بسبب جدار حماية. وللتشغيل عبر WebRTC تستخدم المشغلات معيار WHEP، وهو المقابل ل WHIP في جهة المشاهدة، وما زال [مسودة في IETF](https://datatracker.ietf.org/doc/draft-ietf-wish-whep/?ref=arabroot.io) لكنه مدعوم في خوادم الوسائط الحديثة.

## الدردشة: رسالة واحدة تنسخ لكل مشاهد

الدردشة في البث المباشر ليست تطبيق محادثة عادياً، فالرسالة الواحدة لا تذهب إلى شخص واحد وإنما إلى كل من في الغرفة، فإذا كان في الغرفة 10,000 مشاهد وكتب منهم 20 رسالة في الثانية، فالسيرفرات يجب أن تكتب 200,000 رسالة في الثانية، وهذا ما يسمى التوزيع Fan-out. والحل البسيط سيرفر WebSocket واحد يحفظ قائمة الاتصالات في ذاكرته ويرسل لهم، ويعمل حتى يمتلئ معالج السيرفر، ثم تضيف سيرفراً ثانياً فتكتشف أن رسالة من مشاهد متصل بالسيرفر الأول لا تصل إلى مشاهدي السيرفر الثاني. والحل الأنسب أن تمر كل رسالة على قناة نشر واشتراك Pub/Sub مشتركة، فيستلمها كل سيرفر ويوزعها على من اتصل به، والصورة التالية تبين ذلك كما في التجربة:

![ستة هواتف متصلة عبر WebSocket، ثلاثة بالسيرفر chat-1 وثلاثة بالسيرفر chat-2، ومشاهد يكتب هدف فتصل رسالته إلى chat-1، فينشرها في قناة room:match-1 على Valkey pub/sub، فتصل إلى السيرفرين، وكل سيرفر يكتبها لكل مشاهد متصل به، وبجانبها ملاحظات: رسالة واحدة في N مشاهد تساوي N كتابة، والمشاهد البطيء لا يجب أن يوقف البقية، ولا ضغط لكل رسالة، وأضف سيرفرات عندما تمتلئ نواة المعالج](https://arabroot.io/content/images/2026/10/build-live-streaming-05-chat.webp)

القناة المشتركة تربط السيرفرات، وكل سيرفر يوزع على من اتصل به

وهنا يناسب Pub/Sub بالضبط لأن رسالة الدردشة لا قيمة لها بعد ثوان، فإذا كان المشاهد غير متصل لحظتها فلن يحتاجها، وهذا هو الفرق الذي شرحناه بين الطابور وسجل الأحداث في [الطابور أم بث الأحداث](https://arabroot.io/articles/%D8%A7%D9%84%D8%B7%D8%A7%D8%A8%D9%88%D8%B1-%D8%A7%D9%85-%D8%A8%D8%AB-%D8%A7%D9%84%D8%A7%D8%AD%D8%AF%D8%A7%D8%AB-message-queue-%D9%88-event-streaming/): الدردشة الحية تحتاج توزيعاً فورياً بلا حفظ، أما الأحداث التي يجب ألا تضيع، مثل الهدايا والبلاغات، فمكانها سجل أو قاعدة بيانات. وانتبه لثلاثة أشياء في سيرفر الدردشة: ألا يوقف مشاهد بطيء الإرسال للبقية، وأن تحدد معدلاً لكل مستخدم Rate Limit يمنع الإغراق، وأن تخفف الرسائل عندما تزدحم الغرفة جداً، فتعرض لكل مشاهد عينة من الرسائل بدل كلها، لأن عيناً بشرية لا تقرأ 200 رسالة في الثانية.

## الهدايا والعملات: دفتر حسابات قبل أن تكون رسوماً متحركة

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

![تطبيق المشاهد يشتري من App Store أو Google Play، ثم يرسل الإيصال إلى واجهة الدفع التي تتحقق منه وتضيف 500 عملة في دفتر المحفظة، ثم يرسل المشاهد هدية بمفتاح يمنع التكرار، فتحدث معاملة واحدة في قاعدة البيانات تخصم 100 عملة من المشاهد وتضيف 100 للمذيع معلقة، ثم تظهر الهدية في الغرفة، ثم تصرف أرباح المذيع بعد المراجعة](https://arabroot.io/content/images/2026/10/build-live-streaming-06-gifts.webp)

كل هدية سطران في دفتر حسابات داخل معاملة واحدة

- الشراء: في تطبيقات الهاتف تباع العملات الرقمية عبر نظام الشراء داخل التطبيق In-App Purchase في متجر التطبيقات، وتسمح [إرشادات مراجعة App Store](https://developer.apple.com/app-store/review/guidelines/?ref=arabroot.io) باستخدام عملات الشراء داخل التطبيق ليمنح المستخدمون «إكرامية» لمقدمي المحتوى الرقمي، وصفحة JACO في المتجر تعرض هذه الحزم بالريال. ولا تقم بإضافة العملات بمجرد أن يقول التطبيق إن الدفع نجح، والسبب أن الإيصال يمكن تزويره، فتحقق منه في السيرفر مع المتجر قبل أي إضافة.
- الدفتر Ledger: لا تحفظ الرصيد كرقم تعدله، وإنما كقيود تضاف ولا تعدل، والرصيد مجموعها، فإذا ظهر خلاف عرفت من أين جاء، وقد وضعنا جداوله في [نموذج البيانات](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A8%D8%AB-%D9%85%D8%A8%D8%A7%D8%B4%D8%B1-%D9%85%D8%AB%D9%84-jaco-%D9%88-twitch/#arch-data).
- الإرسال: خصم العملات من المشاهد وإضافتها للمذيع في معاملة Transaction واحدة، مع مفتاح يمنع التكرار Idempotency Key يرسله التطبيق، فإذا ضغط المشاهد الزر مرتين أو أعاد التطبيق الطلب بعد انقطاع الشبكة لا تخصم الهدية مرتين.
- الإعلان: بعد نجاح المعاملة فقط ينشر حدث الهدية في قناة الغرفة لتظهر الرسوم، وليس قبلها.
- الصرف: أرباح المذيع تبقى معلقة مدة للمراجعة ومكافحة الاحتيال، مثل شراء عملات ببطاقات مسروقة وإهدائها لحساب شريك، قبل أن تصرف.

وقبل أن تطلق الهدايا والعملات اقرأ أنظمة المدفوعات وحماية المستهلك في كل دولة تعمل فيها، وشروط متاجر التطبيقات التي تنشر عليها، لأن هذه الشروط تختلف بين الدول وتتغير، ولا يشرحها هذا المقال.

## الإشراف على البث المباشر

الإشراف على البث المباشر أصعب من الفيديو المرفوع، لأن المخالفة تصل إلى المشاهدين لحظة حدوثها، ولا توجد مرحلة مراجعة قبل النشر، لذلك يبنى على طبقات: فلاتر آلية على الدردشة للكلمات والروابط والإغراق، ومشرفون من المذيع نفسه يستطيعون حذف الرسائل وحظر المستخدمين في قناته، وأدوات للمنصة تستطيع إيقاف بث كامل بضغطة واحدة وإبطال مفتاح البث، وعينات دورية من صور البث تمر على مصنفات آلية وتنبه فريق المراجعة، وزر بلاغ سهل للمشاهدين، وقد شرحنا طبقات المراجعة الآلية والبشرية بتفصيل أكثر في [مقال تطبيق الفيديو القصير](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D9%81%D9%8A%D8%AF%D9%8A%D9%88-%D9%82%D8%B5%D9%8A%D8%B1-%D9%85%D8%AB%D9%84-tiktok/#moderation).

## التوسع عند الذروة

الذروة في البث المباشر لها شكل خاص، فالمشاهدون لا يزيدون ببطء وإنما يدخلون في نفس الدقيقة عند بداية المباراة، ثم يكتبون في نفس الثانية عند الهدف، لذلك:

- جهز السيرفرات قبل الحدث المعروف بدل أن تنتظر التوسع التلقائي، فالتوسع التلقائي يحتاج دقائق والمشاهدون يدخلون في ثوان.
- اجعل المشغل يعيد الاتصال بانتظار عشوائي يزيد مع كل محاولة، وإلا عاد كل المشاهدين في نفس الثانية بعد أي انقطاع وأسقطوا السيرفر من جديد.
- قس كل طبقة على حدة باختبار أحمال قبل الحدث، كما شرحنا في [اختبار الأحمال والضغط](https://arabroot.io/articles/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%A7%D8%AD%D9%85%D8%A7%D9%84-%D9%88%D8%A7%D9%84%D8%B6%D8%BA%D8%B7-load-%D9%88-stress-testing-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، ففي التجربة العملية سوف ترى أن عنق الزجاجة في الدردشة كان معالج السيرفر وليس Valkey.
- اجعل للتطبيق وضعاً متدهوراً Degraded Mode، مثل إيقاف الرسوم المتحركة للهدايا أو تخفيف الدردشة، بدل أن يسقط كله.

## التكلفة

في البث المباشر ثلاثة بنود تكبر بشكل مختلف: التحويل يكبر مع عدد القنوات النشطة وعدد جوداتها، لا مع عدد المشاهدين، والنقل الخارج يكبر مع عدد المشاهدين ومعدل البت، وهو نفس بند Egress الذي شرحناه في [التكاليف الخفية في السحابة](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D9%83%D8%A7%D9%84%D9%8A%D9%81-%D8%A7%D9%84%D8%AE%D9%81%D9%8A%D8%A9-%D9%81%D9%8A-%D8%A7%D9%84%D8%B3%D8%AD%D8%A7%D8%A8%D8%A9-egress-%D9%88%D8%A7%D9%84%D8%AA%D8%AE%D8%B2%D9%8A%D9%86/#egress)، فبث بمعدل 2.5 ميجابت يشاهده ألف شخص ساعة واحدة ينقل قرابة 1,125 جيجابايت، وقد حسبنا هذه البنود لتطبيق كامل في [تقدير السعة](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A8%D8%AB-%D9%85%D8%A8%D8%A7%D8%B4%D8%B1-%D9%85%D8%AB%D9%84-jaco-%D9%88-twitch/#arch-capacity). وخوادم WebRTC تكبر مع عدد المشاهدين المتصلين بها، وكل مشاهد عليها أغلى من مشاهد على CDN، لأنه اتصال دائم ومشفر لا يحفظ. وهذا يقودك إلى قرارات عملية: حول القنوات التي فيها مشاهدون فقط، وأعط المشاهدين الصامتين LL-HLS، واحتفظ ب WebRTC لمن يتفاعل.

## القطع مفتوحة المصدر

| الأداة                                                             | الرخصة       | ما تقوم به                                                                                                                                    |
| ------------------------------------------------------------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| [MediaMTX](https://github.com/bluenviron/mediamtx?ref=arabroot.io) | MIT          | خادم وسائط في ملف تنفيذي واحد يستقبل وينشر عبر RTMP وSRT وWebRTC وRTSP وHLS، ويحول بين البروتوكولات تلقائياً ويسجل، وهو ما نستخدمه في التجربة |
| [LiveKit](https://github.com/livekit/livekit?ref=arabroot.io)      | Apache-2.0   | خادم WebRTC SFU موزع للتطبيقات التي فيها صوت وفيديو تفاعلي مع حزم SDK للهواتف والمتصفح                                                        |
| [Owncast](https://github.com/owncast/owncast?ref=arabroot.io)      | MIT          | خادم بث مباشر ودردشة لقناة واحدة تستضيفه بنفسك، فهو قناتك الخاصة وليس منصة لكثير من المذيعين                                                  |
| [SRS](https://github.com/ossrs/srs?ref=arabroot.io)                | MIT          | خادم وسائط آخر منتشر يدعم RTMP وHLS وWebRTC وSRT                                                                                              |
| [Valkey Pub/Sub](https://valkey.io/topics/pubsub/?ref=arabroot.io) | BSD-3-Clause | القناة المشتركة بين سيرفرات الدردشة                                                                                                           |
| [hls.js](https://github.com/video-dev/hls.js?ref=arabroot.io)      | Apache-2.0   | مشغل HLS وLL-HLS في المتصفح                                                                                                                   |

وإذا كانت حاجتك قناة بث واحدة لمؤسستك أو مسجدك أو فعالياتك فابدأ ب [Owncast](https://owncast.online/?ref=arabroot.io)، وإذا كانت تطبيقاً فيه غرف صوت وفيديو تفاعلية فابدأ ب LiveKit، وسوف نشرح تثبيت الاثنين بإذن الله في دليلين ضمن هذا القسم. أما إذا كانت منصة لكثير من المذيعين بدردشة وهدايا فهي ما نبنيه هنا من قطع.

## تجربة عملية: بث ومشاهدة بطريقتين ودردشة لآلاف المشاهدين

الآن سوف نشغل MediaMTX 1.21.2 مع ffmpeg، ونرسل إليه بثاً من ffmpeg يقوم بدور OBS، عليه ساعة المرسل بالمللي ثانية مطبوعة فوق الصورة، ثم نشاهده في صفحة واحدة عبر WebRTC وLL-HLS معاً لنقيس التأخير، ثم نشغل سيرفري دردشة مع Valkey ونرسل الرسائل إلى آلاف الاتصالات. وكل المنافذ على 127.0.0.1 فقط، والتجربة كلها على جهاز واحد، فالأرقام توضيحية لفهم الفروق وليست مقياساً لأداء الأدوات.

### ملفات التجربة

أول ملف هو `Dockerfile` لأدوات التجربة: ffmpeg مع خط لكتابة الساعة، ومكتبة websockets للدردشة:

```dockerfile
FROM python:3.13.16-alpine3.24
RUN apk add --no-cache ffmpeg font-dejavu
RUN pip install --no-cache-dir websockets==17.2 redis==8.1.0
WORKDIR /app
COPY chat.py load.py compare.html ./
```

ثم إعداد MediaMTX في `mediamtx.yml`، ونغير فيه ما نحتاجه فقط ونترك بقية القيم الافتراضية:

```yaml
logLevel: info
api: true
apiAddress: :9997

rtmpAddress: :1935            # OBS and ffmpeg publish here
srtAddress: :8890             # SRT ingest (UDP), for unstable networks

hlsAddress: :8888
hlsVariant: lowLatency        # LL-HLS: 1 s segments split into 200 ms parts
hlsSegmentDuration: 1s
hlsPartDuration: 200ms

webrtcAddress: :8889          # WHEP playback page and endpoint
webrtcLocalUDPAddress: :38923
webrtcIPsFromInterfaces: false
webrtcAdditionalHosts: [127.0.0.1]

paths:
  live:
    # real-time transcoding: when the stream starts, ffmpeg makes a 480p copy at live/480p
    runOnAvailable: >
      ffmpeg -hide_banner -loglevel error -rtsp_transport tcp -i rtsp://localhost:$RTSP_PORT/$MTX_PATH
      -map 0:v -c:v libx264 -preset veryfast -tune zerolatency -vf scale=-2:480
      -b:v 900k -maxrate 900k -bufsize 900k -g 30 -map 0:a -c:a libopus -b:a 64k
      -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH/480p
    runOnAvailableRestart: yes
  all_others:
```

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

- `hlsVariant: lowLatency` هو LL-HLS بمقاطع ثانية وأجزاء 200 مللي ثانية، وهي القيم الافتراضية في هذا الإصدار، وكتبناها لتكون واضحة.
- WebRTC يحتاج أن يعرف العنوان الذي يصل إليه المتصفح، فأوقفنا جمع العناوين من واجهات ال Container وأعطيناه 127.0.0.1، ونشرنا منفذ ال UDP بنفس رقمه داخل ال Container وخارجه، لأن الرقم يرسل للمتصفح داخل عرض SDP.
- عند وصول بث إلى المسار `live` يشغل MediaMTX أمر `runOnAvailable`، وهو ffmpeg يقرأ البث ويحوله في الوقت الحقيقي إلى 480p مع صوت Opus وينشره في المسار `live/480p`، وهذا هو التحويل في الوقت الحقيقي، والصوت Opus هو ما يفهمه WebRTC.

ثم `publish.sh`، وهو يقوم بدور OBS، فيرسل صورة اختبار 720p وعليها آخر ست خانات من ساعة المرسل بالمللي ثانية:

```bash
# Plays the role of OBS: a 720p test picture with the sender clock (last 6 digits of epoch ms) burned in,
# sent over RTMP with the usual live settings: x264 zerolatency, a keyframe every second, AAC audio.
START=$(python -c 'import time; print(int(time.time() * 1000) % 1000000)')
exec ffmpeg -hide_banner -loglevel error -re \
  -f lavfi -i testsrc2=size=1280x720:rate=30 -f lavfi -i sine=frequency=440 \
  -vf "drawtext=fontfile=/usr/share/fonts/dejavu/DejaVuSansMono-Bold.ttf:fontsize=64:fontcolor=yellow:box=1:boxcolor=black@0.7:x=40:y=40:text='sender %{eif\:mod($START+t*1000\,1000000)\:d}'" \
  -c:v libx264 -preset veryfast -tune zerolatency -b:v 2500k -maxrate 2500k -bufsize 2500k -g 30 -pix_fmt yuv420p \
  -c:a aac -b:a 128k -f flv rtmp://media:1935/live
```

ثم صفحة المقارنة `compare.html`، وفيها ساعة المشاهد في الأعلى، ومشغل WebRTC يتصل ب WHEP بطلب POST واحد، ومشغل hls.js:

```html
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>lab-live compare</title>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.7.3/dist/hls.min.js"></script>
<style>body{margin:12px;background:#111;color:#eee;font:15px system-ui} .row{display:flex;gap:12px}
.box{flex:1} video{width:100%;background:#000} h2{font:600 15px system-ui;margin:6px 0}
#clock{font:700 34px Consolas,monospace;color:#ffd54a;margin-bottom:8px}</style></head>
<body>
<div id="clock"></div>
<div class="row">
  <div class="box"><h2>WebRTC (WHEP)</h2><video id="rtc" autoplay muted playsinline></video></div>
  <div class="box"><h2>LL-HLS (hls.js)</h2><video id="hls" autoplay muted playsinline></video></div>
</div>
<script>
const clock = document.getElementById('clock');
setInterval(() => clock.textContent = 'viewer clock (ms): ' + (Date.now() % 1000000), 16);

// WebRTC playback with WHEP: one POST with the SDP offer, the answer comes back in the response
(async () => {
  const pc = new RTCPeerConnection();
  pc.addTransceiver('video', { direction: 'recvonly' });
  pc.ontrack = (e) => { document.getElementById('rtc').srcObject = e.streams[0]; };
  await pc.setLocalDescription(await pc.createOffer());
  await new Promise(r => { if (pc.iceGatheringState === 'complete') r(); pc.onicegatheringstatechange = () => pc.iceGatheringState === 'complete' && r(); setTimeout(r, 2000); });
  const res = await fetch('http://127.0.0.1:38922/live/whep', { method: 'POST', headers: { 'Content-Type': 'application/sdp' }, body: pc.localDescription.sdp });
  await pc.setRemoteDescription({ type: 'answer', sdp: await res.text() });
  window.rtcState = () => pc.connectionState;
})();

const hls = window.hls = new Hls({ lowLatencyMode: true });
hls.loadSource('http://127.0.0.1:38921/live/index.m3u8');
hls.attachMedia(document.getElementById('hls'));
</script>
</body></html>
```

ثم سيرفر الدردشة `chat.py`:

```python
# Chat server: WebSocket clients join a room; every message goes through Valkey pub/sub,
# so a message sent to one server reaches the viewers connected to every other server.
import asyncio, json, os
import redis.asyncio as redis
from websockets.asyncio.server import serve, broadcast

rooms = {}                                   # room -> set of local connections
db = redis.Redis(host=os.environ["VALKEY_HOST"])
NAME = os.environ.get("SERVER_NAME", "chat")

async def handler(ws):
    room = ws.request.path.strip("/") or "lobby"
    rooms.setdefault(room, set()).add(ws)
    try:
        async for raw in ws:
            msg = json.loads(raw)
            if len(msg.get("text", "")) > 300:       # a cheap guard; real moderation comes later
                continue
            msg["via"] = NAME
            await db.publish(f"room:{room}", json.dumps(msg))
    finally:
        rooms[room].discard(ws)

async def fan_out():
    pubsub = db.pubsub()
    await pubsub.psubscribe("room:*")
    async for m in pubsub.listen():
        if m["type"] == "pmessage":
            room = m["channel"].decode()[5:]
            # broadcast() writes to every socket without waiting; a slow client cannot stall the others
            broadcast(rooms.get(room, ()), m["data"].decode())

async def main():
    asyncio.create_task(fan_out())
    # compression=None: with per-message deflate every socket compresses the same message again
    async with serve(handler, "0.0.0.0", 8765, max_size=4096, ping_interval=20, compression=None):
        print(f"{NAME} listening on :8765", flush=True)
        await asyncio.Future()

asyncio.run(main())
```

ثم `load.py` الذي يقوم بدور المشاهدين والمرسل، وكل رسالة تحمل وقت إرسالها، فيحسب كل مشاهد كم تأخرت عنده:

```python
# Chat load test.  watch: N viewers split over the chat servers record the delay of every message.
#                   send:  one user posts M messages at R per second.
# All containers run on the same Docker host and read the same clock, so the delays are comparable.
import asyncio, json, sys, time
from websockets.asyncio.client import connect

role, servers = sys.argv[1], sys.argv[2].split(",")
URL = "ws://{}:8765/match-1"

async def watch(viewers, messages, out):
    delays, gate = [], asyncio.Semaphore(200)       # at most 200 handshakes at a time
    async def viewer(i, ready):
        async with gate:
            ws = await connect(URL.format(servers[i % len(servers)]), max_size=4096, open_timeout=60, compression=None)
        ready.set()
        async with ws:
            for _ in range(messages):
                msg = json.loads(await ws.recv())
                delays.append((time.time_ns() - msg["sent_ns"]) / 1e6)
    events = [asyncio.Event() for _ in range(viewers)]
    tasks = [asyncio.create_task(viewer(i, e)) for i, e in enumerate(events)]
    await asyncio.gather(*(e.wait() for e in events))
    print(f"{viewers} viewers connected", flush=True)
    await asyncio.wait_for(asyncio.gather(*tasks), 300)
    with open(out, "w") as f:
        f.write("\n".join(f"{d:.2f}" for d in delays))

async def send(messages, rate):
    async with connect(URL.format(servers[0])) as ws:
        for n in range(messages):
            await ws.send(json.dumps({"user": "u1", "text": f"goal {n}", "sent_ns": time.time_ns()}))
            await asyncio.sleep(1 / rate)

if role == "watch":
    asyncio.run(watch(int(sys.argv[3]), int(sys.argv[4]), sys.argv[5]))
else:
    asyncio.run(send(int(sys.argv[3]), float(sys.argv[4])))
```

ثم `run-load.sh` الذي يشغل أربعة Containers للمشاهدين ومرسلاً واحداً، و `stats.py` الذي يجمع النتائج:

```bash
# usage: sh run-load.sh <total viewers>   (4 watcher containers + 1 sender, 50 messages at 10 per second)
N=$1; W=$(pwd); rm -rf out && mkdir out
for i in 1 2 3 4; do
  docker rm -f lab-live-load-$i >/dev/null 2>&1; docker run -d --name lab-live-load-$i --network lab-live_default -v "$W/out:/out" -v "$W/load.py:/app/load.py:ro" \
    lab-live-tools:1 python load.py watch lab-live-chat-1,lab-live-chat-2 $((N / 4)) 50 /out/w$i.txt > /dev/null
done
until [ "$(for i in 1 2 3 4; do docker logs lab-live-load-$i 2>&1; done | grep -c connected)" = 4 ]; do sleep 1; done
docker run --rm --name lab-live-send --network lab-live_default -v "$W/load.py:/app/load.py:ro" \
  lab-live-tools:1 python load.py send lab-live-chat-1 50 10
until [ "$(ls out/*.txt 2>/dev/null | wc -l)" = 4 ]; do sleep 1; done
python stats.py out
docker rm -f lab-live-load-1 lab-live-load-2 lab-live-load-3 lab-live-load-4 > /dev/null
```

```python
import glob, sys
d = sorted(float(x) for f in glob.glob(sys.argv[1] + "/*.txt") for x in open(f).read().split())
q = lambda p: d[min(len(d) - 1, int(p * len(d)))]
print(f"deliveries={len(d)}  p50={q(.5):.1f} ms  p95={q(.95):.1f} ms  p99={q(.99):.1f} ms  max={d[-1]:.1f} ms")
```

وأخيراً `compose.yml`:

```yaml
name: lab-live
services:
  media:
    image: bluenviron/mediamtx:1.21.2-ffmpeg
    volumes: ["./mediamtx.yml:/mediamtx.yml:ro"]
    ports:
      - "127.0.0.1:38920:1935"          # RTMP ingest
      - "127.0.0.1:38921:8888"          # LL-HLS
      - "127.0.0.1:38922:8889"          # WebRTC (WHEP)
      - "127.0.0.1:38923:38923/udp"     # WebRTC media (ICE)
  valkey:
    image: valkey/valkey:9.1.2-alpine
  chat:
    build: .
    image: lab-live-tools:1
    command: ["python", "chat.py"]
    environment: { VALKEY_HOST: valkey }
    deploy: { replicas: 2 }
  web:
    image: lab-live-tools:1
    command: ["python", "-m", "http.server", "8000"]
    ports: ["127.0.0.1:38924:8000"]
```

### البث إلى MediaMTX

```bash
docker compose up -d --build
docker run -d --name lab-live-obs --network lab-live_default \
  -v "$PWD/publish.sh:/publish.sh:ro" --entrypoint sh lab-live-tools:1 /publish.sh
docker compose logs media | tail -9
```

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

```text
media-1  | 2026/10/09 00:10:18 INF [RTMP] [conn 192.168.112.7:35630] opened
media-1  | 2026/10/09 00:10:20 INF [path live] runOnAvailable command started
media-1  | 2026/10/09 00:10:20 INF [path live] stream is available and online, 2 tracks (H264, MPEG-4 Audio)
media-1  | 2026/10/09 00:10:20 INF [RTMP] [conn 192.168.112.7:35630] is publishing to path 'live'
media-1  | 2026/10/09 00:10:20 INF [RTSP] [conn [::1]:60448] opened
media-1  | 2026/10/09 00:10:20 INF [RTSP] [session 78423577] is reading from path 'live', with TCP, 2 tracks (H264, MPEG-4 Audio)
media-1  | 2026/10/09 00:10:23 INF [path live/480p] stream is available and online, 2 tracks (H264, Opus)
media-1  | 2026/10/09 00:10:23 INF [RTSP] [session e9675527] is publishing to path 'live/480p'
media-1  | 2026/10/09 00:10:23 INF [path live/480p] RTP packets are too big (1460 > 1440), remuxing them into smaller ones
```

البث وصل بال RTMP إلى المسار `live`، فبدأ أمر التحويل وقرأه بال RTSP، وبعد ثلاث ثوان ظهر المسار `live/480p` بصوت Opus. ونسأل واجهة MediaMTX عن المسارات، ونقيس استهلاك المعالج:

```bash
docker exec lab-live-media-1 wget -qO- http://localhost:9997/v3/paths/list | python3 -c "
import sys, json
for p in json.load(sys.stdin)['items']:
    print(p['name'], p['source']['type'], p['tracks'], 'readers:', len(p['readers']), 'bytesReceived:', p['bytesReceived'])"
docker stats --no-stream --format '{{.Name}} CPU {{.CPUPerc}} MEM {{.MemUsage}}' lab-live-media-1 lab-live-obs
```

```text
live rtmpConn ['H264', 'MPEG-4 Audio'] readers: 1 bytesReceived: 28460886
live/480p rtspSession ['H264', 'Opus'] readers: 0 bytesReceived: 10329062
lab-live-media-1 CPU 89.25% MEM 128.4MiB / 31.29GiB
lab-live-obs CPU 91.94% MEM 94.8MiB / 31.29GiB
```

لاحظ أن القارئ الوحيد للمسار `live` هو أمر التحويل نفسه، وأن ال Container الذي فيه MediaMTX والتحويل استهلك قرابة 89% من نواة واحدة، وأغلبها لفك ترميز 720p وترميز 480p واحدة، فقناة بثلاث جودات تحتاج قرابة ثلاث أنوية طوال البث، وهذا هو رقم التكلفة الذي تكلمنا عنه. والمرسل أيضاً استهلك نواة كاملة، لأنه يرمز 720p بنفسه كما يفعل هاتف المذيع.

ونفس الخادم يستقبل SRT دون أي إعداد إضافي، فالأمر التالي يرسل 6 ثوان بال SRT من شبكة غير مستقرة مثلاً، بمهلة 200 مللي ثانية لإعادة إرسال الحزم المفقودة:

```bash
docker run --rm --network lab-live_default --name lab-live-srt --entrypoint sh lab-live-tools:1 -c \
  "ffmpeg -hide_banner -loglevel error -re -f lavfi -i testsrc2=size=1280x720:rate=30 -t 6 \
   -c:v libx264 -preset veryfast -tune zerolatency -g 30 -f mpegts 'srt://media:8890?streamid=publish:field-cam&latency=200000'"
docker compose logs media | grep SRT | tail -3
```

```text
media-1  | 2026/10/09 00:16:17 INF [SRT] [conn 192.168.112.8:38912] opened
media-1  | 2026/10/09 00:16:18 INF [SRT] [conn 192.168.112.8:38912] is publishing to path 'field-cam'
media-1  | 2026/10/09 00:16:23 INF [SRT] [conn 192.168.112.8:38912] closed: EOF
```

### ماذا يرى مشغل LL-HLS؟

MediaMTX يرد على الطلب الأول بإعادة توجيه تضع Cookie، لذلك نستخدم `-L` مع ملف Cookies:

```bash
curl -s -L -c /tmp/cj -b /tmp/cj http://127.0.0.1:38921/live/index.m3u8
curl -s -L -c /tmp/cj -b /tmp/cj http://127.0.0.1:38921/live/video1_stream.m3u8 | head -9
curl -s -L -c /tmp/cj -b /tmp/cj http://127.0.0.1:38921/live/video1_stream.m3u8 | tail -16
```

```text
#EXTM3U
#EXT-X-VERSION:10
#EXT-X-INDEPENDENT-SEGMENTS

#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="audio2",AUTOSELECT=YES,DEFAULT=YES,URI="audio2_stream.m3u8"

#EXT-X-STREAM-INF:BANDWIDTH=2491144,AVERAGE-BANDWIDTH=2491144,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=30.000,AUDIO="audio"
video1_stream.m3u8
#EXTM3U
#EXT-X-VERSION:10
#EXT-X-TARGETDURATION:1
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.50000,CAN-SKIP-UNTIL=6.00000
#EXT-X-PART-INF:PART-TARGET=0.20000
#EXT-X-MEDIA-SEQUENCE:41
#EXT-X-MAP:URI="c0fa877fa860_video1_init.mp4"
#EXTINF:1.00000,
c0fa877fa860_video1_seg41.mp4
#EXT-X-PART:DURATION=0.20000,URI="c0fa877fa860_video1_part9.mp4"
#EXTINF:1.00000,
c0fa877fa860_video1_seg8.mp4
#EXT-X-PROGRAM-DATE-TIME:2026-10-09T00:11:48.997Z
#EXT-X-PART:DURATION=0.20000,URI="c0fa877fa860_video1_part10.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.20000,URI="c0fa877fa860_video1_part11.mp4"
#EXT-X-PART:DURATION=0.20000,URI="c0fa877fa860_video1_part12.mp4"
#EXT-X-PART:DURATION=0.20000,URI="c0fa877fa860_video1_part13.mp4"
#EXT-X-PART:DURATION=0.20000,URI="c0fa877fa860_video1_part14.mp4"
#EXTINF:1.00000,
c0fa877fa860_video1_seg9.mp4
#EXT-X-PART:DURATION=0.20000,URI="c0fa877fa860_video1_part15.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.20000,URI="c0fa877fa860_video1_part16.mp4"
#EXT-X-PART:DURATION=0.20000,URI="c0fa877fa860_video1_part17.mp4"
#EXT-X-PART:DURATION=0.20000,URI="c0fa877fa860_video1_part18.mp4"
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="c0fa877fa860_video1_part19.mp4"
```

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

- `CAN-BLOCK-RELOAD=YES` يعني أن السيرفر يقبل الطلب المعلق، و `PART-HOLD-BACK=0.5` يعني أن المشغل يبدأ على بعد نصف ثانية من آخر جزء.
- كل مقطع مدته ثانية واحدة مقسم إلى خمسة أجزاء مدة كل منها 0.2 ثانية، والجزء الذي عليه `INDEPENDENT=YES` يبدأ بإطار مفتاحي، فيستطيع المشغل أن يبدأ منه.
- `EXT-X-PRELOAD-HINT` يعطي اسم الجزء القادم قبل أن يكتب، فيطلبه المشغل مسبقاً ويرد السيرفر عندما يجهز.
- الأمرين الثاني والثالث نفذا في لحظتين مختلفتين، لذلك تختلف أرقام المقاطع بينهما، فالقائمة تتحرك مع البث.

### WebRTC وLL-HLS جنباً إلى جنب

افتح `http://127.0.0.1:38924/compare.html` في المتصفح، والصورة التالية لقطة من التجربة في Chrome:

![صفحة فيها ساعة المشاهد 886618 بالمللي ثانية في الأعلى، وفي اليسار فيديو WebRTC عليه ساعة المرسل 885602، وفي اليمين فيديو LL-HLS عليه ساعة المرسل 882336](https://arabroot.io/content/images/2026/10/build-live-streaming-07-compare.webp)

نفس البث في نفس اللحظة: WebRTC متأخر ثانية تقريباً، وLL-HLS قرابة أربع ثوان

ساعة المشاهد في اللقطة 886618، وساعة المرسل في فيديو WebRTC هي 885602، أي أن التأخير قرابة 1.0 ثانية، وفي فيديو LL-HLS هي 882336، أي قرابة 4.3 ثانية، وفي لقطة ثانية بعد 7 ثوان كانت الأرقام 893515 و892536 و889236، أي نفس الفرق تقريباً. والفرق بين المشغلين قرابة 3.3 ثانية، وهو رقم لا يتأثر بأي فرق بين ساعة الجهاز وساعة ال Containers. ولاحظ أن ثانية WebRTC تشمل ترميز المرسل والذاكرة المؤقتة في المتصفح، وأن LL-HLS هنا أبطأ من أفضل ما يمكن أن يصل إليه، لأن hls.js يحتفظ بهامش أمان، وهذا ما تريده لجمهور كبير على شبكات مختلفة.

### الدردشة لآلاف المشاهدين

الآن نشغل نفس الاختبار ثلاث مرات: 100 مشاهد ثم 1,000 ثم 4,000، موزعين على سيرفري الدردشة، ومرسل واحد يرسل 50 رسالة بمعدل 10 رسائل في الثانية، وكل مشاهد يسجل تأخير كل رسالة:

```bash
sh run-load.sh 100
sh run-load.sh 1000
```

```text
deliveries=5000  p50=4.1 ms  p95=7.2 ms  p99=10.4 ms  max=11.8 ms
deliveries=50000  p50=36.3 ms  p95=87.5 ms  p99=149.6 ms  max=226.3 ms
```

مع 100 مشاهد وصلت 99% من الرسائل خلال 10 مللي ثوان، ومع ألف خلال 150 مللي ثانية، وكلاهما أسرع بكثير مما يلاحظه إنسان. ثم 4,000 مشاهد، ونقيس المعالج أثناء الاختبار:

```bash
(sleep 25; docker stats --no-stream --format '{{.Name}} {{.CPUPerc}}' \
  lab-live-chat-1 lab-live-chat-2 lab-live-load-1 lab-live-load-2 lab-live-valkey-1) &
sh run-load.sh 4000
```

```text
lab-live-chat-1 100.70% lab-live-chat-2 89.13% lab-live-load-1 101.47% lab-live-load-2 92.68% lab-live-valkey-1 0.28%
deliveries=200000  p50=2306.2 ms  p95=4857.2 ms  p99=5475.8 ms  max=5844.6 ms
```

هنا انهار الزمن إلى ثانيتين في المتوسط، والسبب واضح من سطر المعالج: كل سيرفر دردشة بلغة Python يعمل على نواة واحدة وصل إلى 100%، لأنه يكتب 20,000 رسالة في الثانية، وكذلك برامج المشاهدين الوهميين نفسها، أما Valkey فاستهلك 0.28% فقط لأنه ينقل 10 رسائل في الثانية لكل سيرفر لا لكل مشاهد. والدرس أن التوزيع يكلف في السيرفر الذي يكتب للمشاهدين لا في القناة المشتركة، والحل أن تضيف سيرفرات دردشة بعدد الأنوية، وهذا سهل لأن القناة المشتركة تربطها، أو تكتب السيرفر بلغة تستخدم كل الأنوية، أو تخفف الرسائل في الغرف المزدحمة. ولاحظ أن الأرقام تختلف بين مرة وأخرى عندما يمتلئ الجهاز، ففي تشغيل سابق لنفس الاختبار كان المتوسط قرابة 5.9 ثانية.

### نكسر التجربة: ضغط كل رسالة

مكتبة websockets تفعل ضغط الرسائل permessage-deflate افتراضياً، كما تذكر [وثائق المكتبة](https://websockets.readthedocs.io/en/stable/topics/compression.html?ref=arabroot.io)، وهذا مفيد في اتصال واحد ينقل رسائل كبيرة. احذف `compression=None` من `chat.py` ومن `load.py`، وأعد بناء ال Image وتشغيل الاختبار:

```text
deliveries=5000  p50=44.7 ms  p95=142.6 ms  p99=176.8 ms  max=245.0 ms
deliveries=50000  p50=4253.6 ms  p95=7602.9 ms  p99=8183.3 ms  max=8370.0 ms
```

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

### حذف المختبر

```bash
docker rm -f lab-live-obs
docker compose down -v
docker image rm lab-live-tools:1
```

## أخطاء شائعة

- مقاطع HLS بطول 6 ثوان في بث تفاعلي، ثم الاستغراب من أن المذيع يرد على تعليقات قديمة.
- WebRTC لكل المشاهدين، فتصبح تكلفة الخوادم أضعاف تكلفة CDN مع LL-HLS.
- تحويل كل القنوات إلى كل الجودات حتى القنوات التي لا يشاهدها أحد.
- سيرفر دردشة واحد يحفظ المتصلين في ذاكرته دون قناة مشتركة، فلا تستطيع إضافة سيرفر ثان.
- تعديل رصيد العملات كرقم واحد دون دفتر قيود، أو دون مفتاح يمنع تكرار الهدية.
- إضافة العملات قبل التحقق من إيصال الشراء في السيرفر.
- مفتاح البث في لقطة شاشة أو مقطع تعليمي، فيبث غيرك على قناتك.

## الخلاصة

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

- تطبيق البث المباشر بث واحد يدخل بال RTMP أو SRT أو WHIP، ويحول في الوقت الحقيقي، ويخرج في مسارين: WebRTC للتفاعل بأقل من ثانية، وLL-HLS عبر ال CDN للجمهور الكبير بثوان قليلة.
- التحويل الحي يكلف أنوية بعدد الجودات طوال البث، لذلك حول ما يشاهد فقط، أو استخدم طبقات Simulcast من جهاز المذيع.
- HLS التقليدي متأخر بثلاثة مقاطع على الأقل بحسب المعيار، وLL-HLS يقلل ذلك بأجزاء صغيرة وطلب معلق، وفي التجربة كان الفرق بينه وبين WebRTC قرابة 3.3 ثانية.
- الدردشة توزيع رسالة واحدة على كل الغرفة، وقناة Pub/Sub مشتركة تربط سيرفراتها، والعمل الحقيقي في السيرفر الذي يكتب للمشاهدين، وضغط كل رسالة جعلها أبطأ مئة مرة.
- الهدايا دفتر حسابات بمعاملة واحدة ومفتاح يمنع التكرار، والشراء يتحقق منه السيرفر.
- جهز السيرفرات قبل الأحداث المعروفة، واجعل للتطبيق وضعاً متدهوراً بدل السقوط الكامل.

وفي المقال التالي من هذا القسم نبني تطبيق فيديو قصير بخلاصة لا تنتهي ونظام توصية يتعلم من كل ثانية مشاهدة:

[![](https://arabroot.io/content/images/2026/10/build-short-video-cover-cover.webp)دليلكيف تبني تطبيق فيديو قصير مثل TikTokكيف يجهز تطبيق الفيديو القصير المقطع التالي قبل أن تسحب الشاشة، وكيف يختار ما يناسبك من إشارات المشاهدة.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D9%81%D9%8A%D8%AF%D9%8A%D9%88-%D9%82%D8%B5%D9%8A%D8%B1-%D9%85%D8%AB%D9%84-tiktok/)[![](https://arabroot.io/content/images/2026/10/build-netflix-cover-cover.webp)دليلكيف تبني منصة فيديو عند الطلب مثل Netflixكيف يتحول فيديو مرفوع إلى بث يناسب كل شاشة وسرعة اتصال، وما الذي تحتاجه منصتك من التحويل إلى التوصيل والتكلفة.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D9%85%D9%86%D8%B5%D8%A9-%D9%81%D9%8A%D8%AF%D9%8A%D9%88-%D8%B9%D9%86%D8%AF-%D8%A7%D9%84%D8%B7%D9%84%D8%A8-%D9%85%D8%AB%D9%84-netflix/)[![](https://arabroot.io/content/images/2026/10/webrtc-turn-cover-cover.webp)دليلمكالمات WebRTC خلف NAT: شرح STUN وTURN وICEلماذا تتصل المكالمة ولا يصل الصوت، وكيف تحل STUN وTURN المشكلة دون أن تفتح باباً إلى شبكتك الداخلية.](https://arabroot.io/articles/%D9%85%D9%83%D8%A7%D9%84%D9%85%D8%A7%D8%AA-webrtc-%D8%AE%D9%84%D9%81-nat-%D8%B4%D8%B1%D8%AD-ice-%D9%88-stun-%D9%88-turn/)[![](https://arabroot.io/content/images/2026/10/queue-vs-streaming-cover-cover.webp)دليلالطابور Queue أم بث الأحداث Event Streaming؟ما يحدث للرسالة بعد قراءتها هو الفرق بين طابور مثل RabbitMQ وسجل أحداث مثل Kafka، ومنه تعرف أيهما يناسب مشروعك.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B7%D8%A7%D8%A8%D9%88%D8%B1-%D8%A7%D9%85-%D8%A8%D8%AB-%D8%A7%D9%84%D8%A7%D8%AD%D8%AF%D8%A7%D8%AB-message-queue-%D9%88-event-streaming/)[![](https://arabroot.io/content/images/2026/10/load-testing-cover-cover.webp)دليلاختبار الأحمال والضغط Load Testing للمبتدئينلماذا يعمل تطبيقك مع خمسة مستخدمين وينهار مع خمسمئة، وكيف تكتشف ذلك وتصلحه قبل يوم الإطلاق.](https://arabroot.io/articles/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%A7%D8%AD%D9%85%D8%A7%D9%84-%D9%88%D8%A7%D9%84%D8%B6%D8%BA%D8%B7-load-%D9%88-stress-testing-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/cloud-hidden-costs-cover-cover.webp)دليلالتكاليف الخفية في السحابة وكيف تخفضهاالبنود التي لا تظهر في حساب يوم التصميم ثم تملأ فاتورة السحابة بعد أشهر، وكيف تحسب كل بند وتخفضه.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D9%83%D8%A7%D9%84%D9%8A%D9%81-%D8%A7%D9%84%D8%AE%D9%81%D9%8A%D8%A9-%D9%81%D9%8A-%D8%A7%D9%84%D8%B3%D8%AD%D8%A7%D8%A8%D8%A9-egress-%D9%88%D8%A7%D9%84%D8%AA%D8%AE%D8%B2%D9%8A%D9%86/)

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

- أكتوبر 2026: إضافة قسم المعمارية الكاملة: المخطط والمكونات ونموذج البيانات والواجهات وتقدير السعة ومراحل النمو.
- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة العملية على Docker Engine 29.8.0 وDocker Compose 5.5.1 مع MediaMTX 1.21.2 وFFmpeg 8.1.2 وValkey 9.1.2 وPython 3.13.16 مع websockets 17.2 وhls.js 1.7.3 في Chrome.