انتقل إلى المحتوى الرئيسي
🤖AI-generated documentation curatedAI Generated
This page was drafted by an AI assistant and may contain inaccuracies.
About content generation types
🤖
AI GeneratedPage drafted entirely by AI from codebase or prompt instructions.
(e.g., docs generated from codebase analysis)
← this page
✋→🤖
AI TransformattedHuman provided raw material; AI restructured it into a different format.
(e.g., livestream → blog post, meeting notes → docs)
Human GeneratedPage written entirely by a human author.
(e.g., hand-written tutorial)
More info about content generation types ↗

بروتوكول WebSocket

تحمل نقطة نهاية WebSocket على /skellycam/websocket/connect ثلاثة أنواع من حركة البيانات:

  1. حمولات الإطارات الثنائية (خادم -> عميل) — إطارات JPEG متعددة الكاميرات
  2. رسائل JSON (خادم -> عميل) — سجلات، تحديثات معدل الإطارات، حالة التطبيق
  3. رسائل نصية (عميل -> خادم) — إقرارات الإطارات و ping/pong

دورة حياة الاتصال

  1. يفتح العميل اتصال WebSocket على /skellycam/websocket/connect.
  2. يقبل الخادم الاتصال ويبدأ أربع مهام متزامنة: ترحيل الصور، ترحيل السجلات، مرسل الحالة، ومعالج رسائل العميل.
  3. يبدأ العميل في استقبال الرسائل فورًا.
  4. عندما ينقطع العميل (أو يُغلق الخادم)، تُلغى جميع المهام ويُغلق الاتصال.

تنسيق حمولة الإطارات الثنائية

عندما تكون الكاميرات نشطة، يرسل الخادم رسائل ثنائية (bytes) تحتوي على إطارات مضغوطة بتنسيق JPEG من جميع الكاميرات في المجموعة النشطة. كل رسالة ثنائية هي حمولة إطارات متعددة مستقلة بالهيكل التالي:

+--------------------------------------------+
| Payload Header (24 bytes) |
+--------------------------------------------+
| Frame Header Camera 0 (56 bytes) |
+--------------------------------------------+
| JPEG Data Camera 0 (variable) |
+--------------------------------------------+
| Frame Header Camera 1 (56 bytes) |
+--------------------------------------------+
| JPEG Data Camera 1 (variable) |
+--------------------------------------------+
| ... |
+--------------------------------------------+
| Frame Header Camera N (56 bytes) |
+--------------------------------------------+
| JPEG Data Camera N (variable) |
+--------------------------------------------+
| Payload Footer (24 bytes) |
+--------------------------------------------+

جميع الأعداد الصحيحة متعددة البايت هي little-endian. تستخدم الهياكل تخطيطًا محاذيًا (numpy align=True)، مما يُدخل بايتات حشو للمحاذاة الطبيعية.

ترويسة الحمولة (24 بايت)

OffsetSizeTypeFieldالوصف
01uint8message_typeدائمًا 0 (PAYLOAD_HEADER)
1-77(padding)حشو محاذاة لـ frame_number بحجم 8 بايت
88int64frame_numberعداد إطارات متزايد بشكل رتيب
164int32number_of_camerasعدد إطارات الكاميرا في هذه الحمولة
20-234(padding)حشو محاذاة الهيكل

ترويسة الإطار (56 بايت، واحدة لكل كاميرا)

OffsetSizeTypeFieldالوصف
01uint8message_typeدائمًا 1 (FRAME_HEADER)
1-77(padding)حشو محاذاة
88int64frame_numberنفس رقم الإطار في ترويسة الحمولة
1616asciicamera_idسلسلة ASCII منتهية بـ null، محشوة بأصفار إلى 16 بايت
324int32camera_indexفهرس الكاميرا الصحيح
364int32image_widthعرض صورة JPEG بالبكسل
404int32image_heightارتفاع صورة JPEG بالبكسل
444int32color_channelsعدد قنوات اللون (عادة 3)
484int32jpeg_string_lengthطول بيانات JPEG التالية بالبايت
52-554(padding)حشو محاذاة الهيكل

تلي كل ترويسة إطار مباشرة بيانات JPEG الخام (jpeg_string_length بايت). لا يوجد حشو بين بيانات JPEG وترويسة الإطار التالية.

تذييل الحمولة (24 بايت)

OffsetSizeTypeFieldالوصف
01uint8message_typeدائمًا 2 (PAYLOAD_FOOTER)
1-77(padding)حشو محاذاة
88int64frame_numberيجب أن يتطابق مع رقم الإطار في ترويسة الحمولة
164int32number_of_camerasيجب أن يتطابق مع عدد الكاميرات في ترويسة الحمولة
20-234(padding)حشو محاذاة الهيكل

يعمل التذييل كفحص اتساق — يمكن للمحللين التحقق من تطابق frame_number و number_of_cameras مع الترويسة.

ثوابت نوع الرسالة

ValueNameالوصف
0PAYLOAD_HEADERبداية حمولة إطارات متعددة
1FRAME_HEADERبيانات وصفية لإطار كاميرا (متبوعة ببيانات JPEG)
2PAYLOAD_FOOTERنهاية حمولة إطارات متعددة

تحليل الحمولة

لتحليل حمولة ثنائية:

  1. اقرأ 24 بايت -> ترويسة الحمولة. تحقق من أن message_type == 0. استخرج frame_number و number_of_cameras.
  2. لكل كاميرا (عدد number_of_cameras من المرات):
    1. اقرأ 56 بايت -> ترويسة الإطار. تحقق من أن message_type == 1. استخرج jpeg_string_length.
    2. اقرأ jpeg_string_length بايت -> بيانات صورة JPEG الخام.
  3. اقرأ 24 بايت -> تذييل الحمولة. تحقق من أن message_type == 2 ومن تطابق frame_number / number_of_cameras مع الترويسة.

مراجع التنفيذ

  • Python (الخادم): skellycam/core/types/frontend_payload_bytearray.py — تبني create_frontend_payload() الحمولة الثنائية باستخدام مصفوفات numpy المهيكلة.
  • TypeScript (العميل): skellycam-ui/src/services/server/server-helpers/frame-processor/binary-protocol.ts — تعريفات الهياكل وإزاحات الحقول. binary-frame-parser.ts — تحلل parseMultiFramePayload() البيانات الثنائية وتنشئ كائنات ImageBitmap.

ملاحظات معالجة الصور

يغيّر الخادم حجم كل إطار كاميرا قبل ترميز JPEG. إذا أرسل العميل displayImageSizes في إقرار إطار، يغيّر الخادم الحجم لمطابقة أبعاد عرض العميل. وإلا، يتم تصغير الصور إلى 50% من دقتها الأصلية. يستخدم ترميز JPEG مستوى جودة 80 افتراضيًا.

رسائل JSON (خادم -> عميل)

سجلات

{
"message_type": "log_record",
"levelname": "INFO",
"levelno": 20,
"message": "Camera group started",
"name": "skellycam.core.camera_group",
"filename": "camera_group.py",
"lineno": 42,
"funcName": "start",
"created": 1700000000.123,
"formatted_message": "2024-01-01 12:00:00 [INFO] Camera group started",
"delta_t": "0.123ms"
}

يتم توجيه سجلات بمستوى TRACE (المستوى 5) وأعلى إلى WebSocket. تعرض الواجهة الأمامية هذه السجلات في لوحة الطرفية. يتم إنتاج السجلات بواسطة skellylogs.

تحديثات معدل الإطارات

{
"message_type": "framerate_update",
"camera_group_id": "group-0",
"backend_framerate": {
"mean_frame_duration_ms": 33.3,
"mean_frames_per_second": 30.0,
"frame_duration_max": 40.1,
"frame_duration_min": 28.5,
"frame_duration_mean": 33.3,
"frame_duration_stddev": 2.1,
"frame_duration_median": 33.2,
"frame_duration_coefficient_of_variation": 0.063,
"calculation_window_size": 100,
"framerate_source": "Server"
},
"frontend_framerate": {
"mean_frame_duration_ms": 34.1,
"mean_frames_per_second": 29.3,
"framerate_source": "Display"
}
}

تُرسل تقريبًا كل 250 ملي ثانية عندما تكون الكاميرات نشطة. يمثل backend_framerate معدل التقاط الكاميرا الحقيقي (محسوب من أرقام الإطارات وطوابع الالتقاط الزمنية، دقيق حتى عندما يتخطى WebSocket إطارات بسبب الضغط العكسي). يمثل frontend_framerate معدل تسليم WebSocket (ما تستقبله واجهة المستخدم فعليًا).

حالة التطبيق

{
"message_type": "app_state",
"state": {
"camera_groups": {
"group-0": {
"id": "group-0",
"camera_ids": ["0", "1"],
"is_recording": false,
"is_paused": false
}
}
}
}

تُرسل تقريبًا كل ثانية وعند كل تغيير في حالة التطبيق (مثل بدء/إيقاف التسجيل).

رسائل العميل -> الخادم

إقرار الإطار

بعد معالجة حمولة إطار ثنائية، يرسل العميل إقرارًا:

{
"frameNumber": 42,
"displayImageSizes": {
"group-0": {
"0": { "width": 640, "height": 480 },
"1": { "width": 640, "height": 480 }
}
}
}

يخبر حقل frameNumber الخادم بالإطار الذي تم عرضه. يستخدم الخادم هذا لإدارة الضغط العكسي — لن يرسل إطارات جديدة حتى يتم الإقرار بالإطار السابق. إذا تأخرت الواجهة الأمامية، يتخطى الخادم الإطارات لمنع تراكم المخزن المؤقت.

حقل displayImageSizes (اختياري) يخبر الخادم بأبعاد العرض الحالية لكل كاميرا، مما يسمح له بتغيير حجم إطارات JPEG لتطابقها، مما يقلل عرض النطاق.

Ping/Pong

إرسال النص "ping" سيتلقى "pong" كاستجابة. يمكن استخدام هذا لفحوصات صحة الاتصال.

إدارة الضغط العكسي

يتتبع الخادم رقم آخر إطار مُرسل ورقم آخر إطار تم الإقرار به. إذا لم تُقر الواجهة الأمامية بأحدث إطار:

  1. يتخطى الخادم إرسال إطارات جديدة.
  2. بعد وصول الإقرار، يتخطى الخادم إطارًا إضافيًا واحدًا للسماح للواجهة الأمامية باللحاق.
  3. إذا تجاوزت الفجوة 1000 إطار، يتم تسجيل تحذير بمستوى trace.

يضمن هذا ألا ينمو مخزن WebSocket المؤقت بلا حدود حتى عندما يكون عرض الواجهة الأمامية أبطأ من معدل التقاط الكاميرا.