🤖AI-generated documentation☐ curatedAI Generated
About content generation types
(e.g., docs generated from codebase analysis)
(e.g., livestream → blog post, meeting notes → docs)
(e.g., hand-written tutorial)
بروتوكول WebSocket
تحمل نقطة نهاية WebSocket على /skellycam/websocket/connect ثلاثة أنواع من حركة البيانات:
- حمولات الإطارات الثنائية (خادم -> عميل) — إطارات JPEG متعددة الكاميرات
- رسائل JSON (خادم -> عميل) — سجلات، تحديثات معدل الإطارات، حالة التطبيق
- رسائل نصية (عميل -> خادم) — إقرارات الإطارات و ping/pong
دورة حياة الاتصال
- يفتح العميل اتصال WebSocket على
/skellycam/websocket/connect. - يقبل الخادم الاتصال ويبدأ أربع مهام متزامنة: ترحيل الصور، ترحيل السجلات، مرسل الحالة، ومعالج رسائل العميل.
- يبدأ العميل في استقبال الرسائل فورًا.
- عندما ينقطع العميل (أو يُغلق الخادم)، تُلغى جميع المهام ويُغلق الاتصال.
تنسيق حمولة الإطارات الثنائية
عندما تكون الكاميرات نشطة، يرسل الخادم رسائل ثنائية (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 بايت)
| Offset | Size | Type | Field | الوصف |
|---|---|---|---|---|
| 0 | 1 | uint8 | message_type | دائمًا 0 (PAYLOAD_HEADER) |
| 1-7 | 7 | — | (padding) | حشو محاذاة لـ frame_number بحجم 8 بايت |
| 8 | 8 | int64 | frame_number | عداد إطارات متزايد بشكل رتيب |
| 16 | 4 | int32 | number_of_cameras | عدد إطارات الكاميرا في هذه الحمولة |
| 20-23 | 4 | — | (padding) | حشو محاذاة الهيكل |
ترويسة الإطار (56 بايت، واحدة لكل كاميرا)
| Offset | Size | Type | Field | الوصف |
|---|---|---|---|---|
| 0 | 1 | uint8 | message_type | دائمًا 1 (FRAME_HEADER) |
| 1-7 | 7 | — | (padding) | حشو محاذاة |
| 8 | 8 | int64 | frame_number | نفس رقم الإطار في ترويسة الحمولة |
| 16 | 16 | ascii | camera_id | سلسلة ASCII منتهية بـ null، محشوة بأصفار إلى 16 بايت |
| 32 | 4 | int32 | camera_index | فهرس الكاميرا الصحيح |
| 36 | 4 | int32 | image_width | عرض صورة JPEG بالبكسل |
| 40 | 4 | int32 | image_height | ارتفاع صورة JPEG بالبكسل |
| 44 | 4 | int32 | color_channels | عدد قنوات اللون (عادة 3) |
| 48 | 4 | int32 | jpeg_string_length | طول بيانات JPEG التالية بالبايت |
| 52-55 | 4 | — | (padding) | حشو محاذاة الهيكل |
تلي كل ترويسة إطار مباشرة بيانات JPEG الخام (jpeg_string_length بايت). لا يوجد حشو بين بيانات JPEG وترويسة الإطار التالية.
تذييل الحمولة (24 بايت)
| Offset | Size | Type | Field | الوصف |
|---|---|---|---|---|
| 0 | 1 | uint8 | message_type | دائمًا 2 (PAYLOAD_FOOTER) |
| 1-7 | 7 | — | (padding) | حشو محاذاة |
| 8 | 8 | int64 | frame_number | يجب أن يتطابق مع رقم الإطار في ترويسة الحمولة |
| 16 | 4 | int32 | number_of_cameras | يجب أن يتطابق مع عدد الكاميرات في ترويسة الحمولة |
| 20-23 | 4 | — | (padding) | حشو محاذاة الهيكل |
يعمل التذييل كفحص اتساق — يمكن للمحللين التحقق من تطابق frame_number و number_of_cameras مع الترويسة.
ثوابت نوع الرسالة
| Value | Name | الوصف |
|---|---|---|
0 | PAYLOAD_HEADER | بداية حمولة إطارات متعددة |
1 | FRAME_HEADER | بيانات وصفية لإطار كاميرا (متبوعة ببيانات JPEG) |
2 | PAYLOAD_FOOTER | نهاية حمولة إطارات متعددة |
تحليل الحمولة
لتحليل حمولة ثنائية:
- اقرأ 24 بايت -> ترويسة الحمولة. تحقق من أن
message_type == 0. استخرجframe_numberوnumber_of_cameras. - لكل كاميرا (عدد
number_of_camerasمن المرات):- اقرأ 56 بايت -> ترويسة الإطار. تحقق من أن
message_type == 1. استخرجjpeg_string_length. - اقرأ
jpeg_string_lengthبايت -> بيانات صورة JPEG الخام.
- اقرأ 56 بايت -> ترويسة الإطار. تحقق من أن
- اقرأ 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" كاستجابة. يمكن استخدام هذا لفحوصات صحة الاتصال.
إدارة الضغط العكسي
يتتبع الخادم رقم آخر إطار مُرسل ورقم آخر إطار تم الإقرار به. إذا لم تُقر الواجهة الأمامية بأحدث إطار:
- يتخطى الخادم إرسال إطارات جديدة.
- بعد وصول الإقرار، يتخطى الخادم إطارًا إضافيًا واحدًا للسماح للواجهة الأمامية باللحاق.
- إذا تجاوزت الفجوة 1000 إطار، يتم تسجيل تحذير بمستوى trace.
يضمن هذا ألا ينمو مخزن WebSocket المؤقت بلا حدود حتى عندما يكون عرض الواجهة الأمامية أبطأ من معدل التقاط الكاميرا.