هل تمنيت يوما ان تتمكن من معاينة مسار الدراجة قبل ركوبه؟ لقد بنينا نظاما آليا يولد فيديوهات تحليق جميلة لكل مسار حفلة على Party Onbici، مما يمنح راكبي الدراجات معاينة سينمائية لما يمكن توقعه في رحلتهم.

التحدي

عند تنظيم حدث للدراجات او الانضمام اليه، فان فهم المسار امر بالغ الاهمية. الخرائط الثابتة مفيدة، لكنها لا تنقل تجربة ركوب المسار فعليا. اردنا ان نمنح المستخدمين طريقة “للتحليق افتراضيا” عبر المسار قبل الالتزام بالرحلة.

حلنا: عرض الفيديو بدون واجهة

بنينا خدمة Node.js تشغل متصفح Chrome بدون واجهة باستخدام Puppeteer، وتعرض المسار على خريطة MapLibre GL تفاعلية، وتلتقط الاطارات بينما تحلق كاميرا افتراضية على طول المسار. اليك كيف يعمل:

1. استيفاء المسار

قد يحتوي مسار الدراجة على آلاف نقاط الاحداثيات. لانشاء فيديو سلس بمعدل 30 اطارا في الثانية لمدة 15 ثانية، نحتاج الى 450 اطارا بالضبط. نستخدم الاستيفاء القائم على المسافة لاخذ عينات من المسار بالتساوي:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Calculate cumulative distances using Haversine formula
const distances = [0];
for (let i = 1; i < coordinates.length; i++) {
  const dist = haversineDistance(coordinates[i-1], coordinates[i]);
  totalDistance += dist;
  distances.push(totalDistance);
}

// Sample at exactly totalFrames points
for (let frame = 0; frame < totalFrames; frame++) {
  const targetDist = (frame / (totalFrames - 1)) * totalDistance;
  // Interpolate position at this distance...
}

2. تحريك الكاميرا مع اتجاه سلس

مجرد توجيه الكاميرا في اتجاه السفر يخلق حركة متقطعة في المسارات المتعرجة. نقوم بتنعيم اتجاه الكاميرا باستخدام متوسط متحرك على 12 اطارا، مع معالجة خاصة للالتفاف من 360 درجة الى 0 درجة:

1
2
3
4
5
6
7
// Use sin/cos components for circular averaging
for (let j = i - smoothWindow; j <= i + smoothWindow; j++) {
  const rad = rawBearings[j] * Math.PI / 180;
  sinSum += Math.sin(rad);
  cosSum += Math.cos(rad);
}
const avgBearing = Math.atan2(sinSum/count, cosSum/count) * 180 / Math.PI;

3. عرض MapLibre GL

يتم عرض المسار على طبقة اساس Stadia Maps الجميلة مع:

  • خط مسار مميز بحدود بيضاء للرؤية
  • مؤشر موقع كاميرا متحرك
  • علامات البداية (خضراء) والنهاية (حمراء)
  • بروزات مباني ثلاثية الابعاد اختيارية للمناطق الحضرية

4. التقاط الاطارات وترميز الفيديو

يلتقط Puppeteer كل اطار كلقطة شاشة PNG، ثم نستخدم ffmpeg لترميزها في فيديو WebM باستخدام ترميز VP9:

1
2
3
ffmpeg -framerate 30 -i frame_%05d.png \
  -c:v libvpx-vp9 -crf 20 -b:v 0 \
  route.webm

البنية التقنية

1
Django App → Celery Task → Node.js Renderer → S3 Storage
  1. Django يطلق توليد الفيديو عند انشاء حفلة او تحديثها
  2. Celery يضع المهمة في قائمة الانتظار للمعالجة غير المتزامنة
  3. عارض Node.js يشغل Puppeteer مع MapLibre GL
  4. ffmpeg يرمز الاطارات الملتقطة
  5. يتم رفع الفيديو الى S3 ويتم تحديث سجل الحفلة

اعتبارات الاداء

الدقةالمدةوقت العرضحجم الملف
1280x72015 ث60-90 ث2-4 م.ب
1920x108015 ث90-120 ث4-8 م.ب

للانتاج، نستخدم حاويات Docker مع عرض برمجي (SwiftShader) لتجنب تبعيات GPU:

1
2
--use-angle=swiftshader
--enable-unsafe-swiftshader

الميزات التي اضفناها

  • ابعاد فيديو قابلة للتخصيص - تنسيق مربع للمشاركة على وسائل التواصل الاجتماعي
  • علامة مائية بالشعار - ضع علامة تجارية على فيديوهاتك بشعار منظمتك
  • ميل وتكبير الكاميرا - اضبط زاوية الرؤية والارتفاع
  • تصميم خط المسار - الوان وعروض مخصصة

التكامل مع Django

من كود التطبيق، توليد فيديو بسيط مثل:

1
2
party = Party.objects.get(uid="...")
party.generate_video(width=1080, height=1080, duration=10)

يتم تحديث رابط الفيديو تلقائيا عند اكتمال العرض:

1
2
3
4
5
{% if party.video_url %}
<video controls>
  <source src="{{ party.video_full_url }}" type="video/webm">
</video>
{% endif %}

الخطوات التالية

نحن نستكشف عدة تحسينات:

  • معاينة في الوقت الفعلي - عرض مباشر بينما يرسم المستخدمون المسارات
  • زوايا كاميرا متعددة - مناظر جانبية، لقطات من الاعلى
  • طبقة الطقس - عرض ظروف التوقعات على طول المسار
  • ملف الارتفاع - تصور الصعود والهبوط في الفيديو

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


هل تريد تجربتها؟ انشئ حفلة وسيتم توليد فيديو مسارك تلقائيا!