MicrocosmWorksابتكار وتصميم الكون الرقمي
من نحناتصل بنا
MicrocosmWorksابتكار وتصميم الكون الرقمي

نقدم حلول تقنية المعلومات المهمة. نحن شغوفون بالتقنية والأمان ومساعدة الشركات على النمو من خلال بنية تحتية موثوقة ومبتكرة لتقنية المعلومات.

[email protected]
+91 7011868196
New Delhi, India

مركز نمو AI

مركز AIابتكار الشركات الناشئةمسرّع المؤسسات

الحلول

جميع الحلولتطبيقات الصحة واللياقةمنصة فيديو AIتطوير وكلاء AI

الموارد

رؤىأدلة القطاعاتمخططات حالات الاستخدامأنماط المعماريةدراسات الحالة

الشركة

من نحناتصل بناأعمالنا

الخدمات

الاستشارات الرقميةالبنية التحتية السحابيةتطوير SaaSتطوير AIتقنية الفيديو
تطوير ERPتخصيص Zohoتطوير Odooتكامل Salesforceتطوير CRM مخصص
تكامل QuickBooksحلول IoTتطوير بلوكتشين
استشارات الأمن السيبرانيالدعم التقني - L3

© 2026 MicrocosmWorks. جميع الحقوق محفوظة.

سياسة الخصوصيةشروط الخدمة
العودة إلى الرؤى
IoT Development

إعادة تأطير فيديو 360 في Flutter: الإسقاط الكروي، إطارات Quaternion الرئيسية، وتصدير GPU

إعادة تأطير لقطات 360 درجة متساوية المستطيلات في Flutter باستخدام الإسقاط الكروي، إطارات Quaternion الرئيسية، والتصدير المسرع بواسطة GPU.

Saurav Kumar Gupta's image Saurav Kumar Gupta
•
July 10, 2026
•
تم التحديث July 24, 2026
•
5 min read
Developer using Flutter to edit and reframe a 360° video with spherical projection.webp
5 min read

إعادة تأطير فيديو 360 درجة في Flutter: الإسقاط الكروي، إطارات Quaternion الرئيسية، وتصدير GPU

كيف حولنا لقطات 360 درجة الخام متساوية المستطيلات إلى فيديو سلس ومؤطر ومسطح - بالكامل داخل تطبيق Flutter.

المشكلة التي لا يحذرك منها أحد

يحتوي الفيديو العادي على شيء واحد واضح ليظهره لك: الإطار الذي وجهت الكاميرا إليه. أما فيديو 360 درجة فلا يوجد فيه شيء من هذا القبيل. لقد سجلت الكاميرا كل شيء — كرة كاملة من البكسلات مخزنة كمستطيل مسطح متساوي المستطيلات (صورة بنسبة 2:1 حيث المحور X هو خط الطول والمحور Y هو خط العرض). قبل أن تتمكن من وضع تلك اللقطات على Instagram أو YouTube أو شاشة الهاتف، يجب على شخص ما الإجابة على سؤال رفضت الكاميرا عمدًا الإجابة عليه:

أين يجب أن ينظر المشاهد، ومتى؟

"إعادة التأطير" هو فعل تحريك كاميرا افتراضية عبر تلك الكرة بمرور الوقت — التحريك الأفقي، والإمالة، والتكبير/التصغير — وإخراج فيديو مسطح عادي بنسبة 16:9 (أو 9:16، أو 2.35:1). هذه هي الميزة التي بني حولها GoPro Player و Insta360 Studio ومكون GoPro VR Reframe الإضافي في Adobe Premiere.

لقد بنيناه بالكامل في Flutter. هذا المنشور هو جولة في الأجزاء التي كانت صعبة حقًا: حسابات الإسقاط، الاستيفاء الخالي من قفل محور الدوران (gimbal lock)، الحد الفاصل ±180 درجة، وخط أنابيب تصدير GPU الذي يجب أن يعمل مع أجهزة فك ترميز Android الحقيقية.

التدفق الشامل:

التقاط/استيراد فيديو 360
  → تحويل ترميز وكيل معاينة خفيف الوزن
  → ينقر المستخدم / يرسم / يكتشف موضوعًا تلقائيًا
  → تتبع الكائن ينتج مسارًا
  → إنشاء + تنعيم إطارات رئيسية
  → SLERP-استيفاء اتجاه الكاميرا لكل إطار (معاينة حية)
  → تصدير GPU الأصلي إلى فيديو مسطح
  → FFmpeg post-pass (ذرة moov + إصلاح مزامنة الصوت/الفيديو)

نظرة سريعة على البنية

حافظنا على النظام في ثلاث طبقات واضحة بحيث يمكن اختبار الرياضيات بمعزل عن Flutter:

طبقةLives inالمسؤولية
النماذجlib/data/models/reframe/SphericalCoordinates/Quaternion, CameraOrientation/MotionConstraints, ReframeKeyframe, ReframeProject
الخدماتlib/services/reframe/SphericalProjection (إسقاط الأشعة), CoordinateConverter, KeyframeInterpolator/Generator/Smoother, ReframeExportService
العرض التقديميlib/presentation/.../reframe/Riverpod ReframeNotifier، مشغل OpenGL Spherical360Player الأصلي، تراكب رسم bbox، المخطط الزمني للإطارات الرئيسية

القاعدة الذهبية: جميع حسابات المثلثات هي Dart خالصة بدون أي واردات Flutter. هذا يعني أن كل دالة إسقاط واستيفاء قابلة للاختبار الوحدوي بدون شجرة ويدجت (widget tree)، وتدفع نفس الرياضيات كل من المعاينة الحية والتصدير.

1. جوهر الأمر: متساوي المستطيلات ⟷ كروي

كل شيء يبدأ بتعيين واحد. الإطار المتساوي المستطيلات هو مجرد مستطيل حيث:

  • X (0 → العرض) يمسح خط الطول (yaw) من -180 درجة إلى +180 درجة
  • Y (0 → الارتفاع) يمسح خط العرض (pitch) من +90 درجة (أعلى/الذروة) إلى -90 درجة (أسفل/الحضيض)

SphericalCoordinates صغيرة عن قصد — فقط yaw و pitch. ينتمي Roll و FOV و zoom إلى الكاميرا، وليس إلى الاتجاه. إليك التحويل الذي تعتمد عليه الميزة بأكملها:

factory SphericalCoordinates.fromEquirectangular({
  required double x, required double y,
  required double videoWidth, required double videoHeight,
}) {
  final normalizedX = x / videoWidth;
  final normalizedY = y / videoHeight;
  // X maps to yaw: 0->-180°, 0.5->0°, 1.0->180°
  final yaw = (normalizedX - 0.5) * 360.0;
  // Y maps to pitch: 0->90° (top), 0.5->0° (horizon), 1.0->-90° (bottom)
  final pitch = (0.5 - normalizedY) * 180.0;
  return SphericalCoordinates(
    yaw: yaw.clamp(-180.0, 180.0),
    pitch: pitch.clamp(-90.0, 90.0),
  );
}

العكس هو الصورة المرآة (normalizedX = yaw/360 + 0.5)، ونحن نستخدمها باستمرار لرسم صندوق تحديد متعقب (bounding box) مرة أخرى على المشغل.

"انقر على ما تراه" — إسقاط الأشعة المنظوري

لا يتفاعل المستخدم مع المستطيل المتساوي المستطيلات. يتفاعلون مع عرض منظور مُصيّر. لذلك، عندما ينقرون على نقطة على المشغل، نحتاج إلى إسقاط شعاع عبر كاميرا ثقب دبوس افتراضية والعثور على الاتجاه الذي أصابوه على الكرة. هذا هو SphericalProjection.screenToSpherical:

final ndcX = (2.0 * screenPoint.dx / playerSize.width) - 1.0;
final ndcY = 1.0 - (2.0 * screenPoint.dy / playerSize.height);
final aspectRatio = playerSize.width / playerSize.height;
final tanHalfFov = math.tan(cameraFov * math.pi / 360.0); // half-angle
double rayX = ndcX * aspectRatio * tanHalfFov;
double rayY = ndcY * tanHalfFov;
double rayZ = 1.0;
// ...normalize, rotate the ray by pitch (around X) then yaw (around Y)...
final sphericalYaw   = math.atan2(finalRayX, finalRayZ) * 180.0 / math.pi;
final sphericalPitch = math.asin(finalRayY.clamp(-1.0, 1.0)) * 180.0 / math.pi;

يبدو .clamp(-1.0, 1.0) قبل asin مبالغًا فيه، لكن انحراف النقطة العائمة (floating-point drift) سيعطيك 1.0000001 وسترجع asin قيمة NaN — وقيمة NaN واحدة تفسد كل إطار لاحق. التثبيت الدفاعي حول الدوال المثلثية العكسية هو موضوع متكرر في قاعدة الشفرة هذه.

2. حركة الكاميرا السلسة: quaternions، وليس زوايا أويلر

النهج الساذج هو تخزين yaw/pitch عند كل إطار رئيسي واستيفاء الأرقام خطيًا. يبدو الأمر فظيعًا. استيفاء زوايا أويلر يسبب قفل محور الدوران (gimbal lock)، وسرعة زاويّة غير موحدة، وقفزات قبيحة بالقرب من القطبين.

نقوم باستيفاء الاتجاه كـ quaternion باستخدام SLERP (الاستيفاء الخطي الكروي). يتم تحويل كل اتجاه إلى quaternion عبر اتفاقية ZYX Euler:

Quaternion toQuaternion() {
  final yawRad = yaw * math.pi / 180.0;
  final pitchRad = pitch * math.pi / 180.0;
  final cy = math.cos(yawRad * 0.5); final sy = math.sin(yawRad * 0.5);
  final cp = math.cos(pitchRad * 0.5); final sp = math.sin(pitchRad * 0.5);
  return Quaternion(w: cy * cp, x: cy * sp, y: sy * cp, z: -sy * sp);
}

و SLERP نفسه يحتوي على صمامي الأمان بجودة الإنتاج التي يحتاجها كل تطبيق:

static Quaternion slerp(Quaternion a, Quaternion b, double t) {
  a = a.normalize(); b = b.normalize();
  var dotProduct = a.dot(b);
  if (dotProduct < 0.0) { b = -b; dotProduct = -dotProduct; }   // take the shortest hemisphere
  if (dotProduct > 0.9995) {                                     // near-parallel -> fall back to LERP
    return Quaternion(w: a.w + t*(b.w-a.w), x: a.x + t*(b.x-a.x),
                    y: a.y + t*(b.y-a.y), z: a.z + t*(b.z-a.z)).normalize();
  }
  final theta0 = math.acos(dotProduct);
  final theta = theta0 * t;
  final sinTheta = math.sin(theta);
  final sinTheta0 = math.sin(theta0);
  final s0 = math.cos(theta) - dotProduct * sinTheta / sinTheta0;
  final s1 = sinTheta / sinTheta0;
  return Quaternion(w: s0*a.w + s1*b.w, /* ...*/).normalize();
}

اثنان من التفاصيل غير القابلة للتفاوض:

  1. نصف الكرة الأقصر — تمثل q و -q نفس الاتجاه. إذا كان حاصل الضرب النقطي سالبًا، فإنك تعكس أحد المدخلات وإلا ستسلك الكاميرا الطريق الطويل حول الكرة.
  2. العودة إلى LERP عند التقارب التام — عندما يكون إطاران رئيسيان متطابقين تقريبًا، فإن sinTheta0 → 0 وتقوم بالقسمة على ما يقرب من الصفر. العودة إلى LERP العادي عندما يكون حاصل الضرب النقطي > 0.9995 يتجنب الانفجار.

قرار تصميم دقيق ولكنه مهم: نحن نقوم بـ SLERP للاتجاه، ولكن LERP لـ FOV/التكبير، ونقوم بـ LERP الزاوي لـ roll. الاستيفاء الكروي هو الأداة الصحيحة للاتجاه؛ التكبير هو مجرد قيمة عددية ويجب أن يتحرك خطيًا:

final qInterp = Quaternion.slerp(from.direction.toQuaternion(), to.direction.toQuaternion(), t);
final interpDirection = SphericalCoordinates.fromQuaternion(qInterp).normalize();
final interpFov  = _lerp(from.fov, to.fov, t);
final interpRoll = _lerpAngle(from.roll, to.roll, t); // wraps across ±180
final interpZoom = _lerp(from.zoom, to.zoom, t);

💡 عند التحويل مرة أخرى من quaternion إلى yaw/pitch، نستخدم atan2 بدلاً من asin "للاستقرار العددي (يتجنب مشاكل نطاق asin)." بعد SLERP، يمكن أن يكون quaternion غير طبيعي قليلاً جدًا، و atan2 يتعامل مع ذلك بسلاسة بينما يرمي asin قيمة NaN.

التخفيف (Easing): مطوّر يدويًا، عند الانتقال الوارد

يمتلك كل إطار رئيسي منحنى التخفيف للانتقال إليه. تتم كتابة المنحنيات يدويًا لتكون متطابقة على كل نظام أساسي:

case KeyframeEasing.linear:    return t;
case KeyframeEasing.easeInOut: return t * t * (3 - 2 * t); // smoothstep S-curve
case KeyframeEasing.easeIn:    return t * t;               // quadratic
case KeyframeEasing.easeOut:   return 1 - (1 - t) * (1 - t);

3. الحد الفاصل ±180 درجة: الخطأ الذي يطارد كل ميزة 360

هنا يكمن الفخ. يعيش الـ Yaw على دائرة: +179° و -179° يفصل بينهما 2°، وليس 358°. في اللحظة التي يسير فيها موضوعك عبر الجزء الخلفي من الكرة، تقوم الرياضيات الساذجة بتحريك الكاميرا إلى منتصف الطريق حول العالم. تظهر هذه المشكلة الواحدة في أربع طبقات مختلفة من النظام، وكل منها يحتاج إلى إصلاحه الخاص.

الطبقة 1 - العناصر الأولية (Primitives). مساعدان صغيران تعتمد عليهما قاعدة الشفرة بأكملها:

static double normalizeYaw(double yaw) {
  while (yaw > 180) { yaw -= 360; }
  while (yaw < -180) { yaw += 360; }
  return yaw;
}

// "CRITICAL for seam-safe tracking ... avoids 180° snaps"
static double shortestYawDelta(double from, double to) {
  double delta = to - from;
  if (delta > 180) delta -= 360;
  if (delta < -180) delta += 360;
  return delta;
}

الطبقة 2 - إنشاء الإطارات الرئيسية يجمع yaw مستمرًا (غير محدود) بحيث يقوم SLERP دائمًا بالاستيفاء بالطريقة الأقصر. بدلاً من تثبيت كل عينة عند ±180 درجة، نقوم بجمع أقصر الفروق وندع القيمة الجارية تتجاوز 180 درجة عن قصد:

final normalizedContinuousYaw = SphericalCoordinates.normalizeYaw(continuousYaw);
final yawDelta = SphericalCoordinates.shortestYawDelta(normalizedContinuousYaw, rawCoords.yaw);
continuousYaw = continuousYaw + yawDelta;            // may exceed ±180 on purpose
coords = SphericalCoordinates(yaw: continuousYaw, pitch: rawCoords.pitch);

الطبقة 3 - إعادة بناء صندوق التحديد (bounding box). عندما يرسم المستخدم مربعًا يمتد على الحد الفاصل، تبلغ الزوايا الأربع عن قيم yaw مثل +170°، +175°، -175°، -170°. يكتشف SphericalProjection ذلك (أي زاوية > 90° وأي زاوية < -90°) ويعيد بناء "bbox آمن للحد الفاصل" ذي yaw مستمر عن طريق تحويل القيم السالبة بمقدار +360 درجة قبل قياس مداه.

الطبقة 4 - المشغل المباشر يقوم بتنعيم الحركة نحو هدفه باستخدام shortestYawDelta بحيث لا يحدث أي اهتزاز عند السحب عبر الحد الفاصل.

إذا تذكرت شيئًا واحدًا حول بناء أدوات 360: الحد الفاصل ليس حالة حافة (edge case)، بل هو اهتمام شامل (cross-cutting concern). ضع ميزانية له في كل مكان يظهر فيه الـ yaw.

4. جعله يشبه Insta360/GoPro

الاستيفاء السلس يمنحك كاميرا صحيحة تقنيًا. لكنه لا يمنحك كاميرا جيدة. ثلاثة أجزاء من "هندسة الإحساس" تسد هذه الفجوة.

قيود الحركة — تثبيت السرعة والتسارع. بيانات التتبع الخام متذبذبة. نمرر الإطارات الرئيسية عبر MotionConstraints، الذي يحدد السرعة والحدة التي يمكن أن تتحرك بها الكاميرا الافتراضية (مثل maxYawSpeed = 120°/ث، maxPitchSpeed = 90°/ث، maxAcceleration = 180°/ث²)، مع إعدادات مسبقة سينمائية (cinematic)، ومتجاوبة (responsive)، وإنتاجية (production). هذا هو الفرق بين "كاميرا مراقبة تتبع ذبابة" و "مشغل بيد ثابتة".

FOV التكيفي — تكبير ليتناسب مع الموضوع. نقيس الحجم الزاوي للموضوع في الفضاء المتساوي المستطيلات ونختار FOV بحيث يملأ نسبة مستهدفة من الإطار:

final angularWidth  = (bboxWidth  / videoWidth)  * 360.0;
final angularHeight = (bboxHeight / videoHeight) * 180.0;
final objectAngularSize = math.max(angularWidth, angularHeight);
final idealFov = objectAngularSize / targetFillRatio; // targetFillRatio ≈ 0.4
return idealFov.clamp(minFov, maxFov);                 // 45°–120°

تقدم الحركة + مزيج الوجه والجسم. ندفع التأطير حوالي 15% من عرض bbox إلى الأمام في اتجاه الحركة، حتى لا يلتصق الموضوع بالحافة التي يسير نحوها. وعندما يفقد اكتشاف الوجه، لا ننتقل فجأة إلى مركز الجسم - بل نقوم بـ LERP من آخر مركز وجه معروف إلى مركز الجسم على مدى حوالي 10 إطارات.

5. خدعة الفيديو المزدوج: وكيل المعاينة مقابل الأصل كامل الدقة

هذا هو القرار المعماري الذي يجعل الأمر برمته قابلاً للاستخدام على الهواتف الحقيقية.

غالبًا ما يكون ملف GoPro/Insta360 360° هو 4096×2048 HEVC. تختنق أجهزة فك ترميز Android منخفضة التكلفة عند محاولة تشغيل ذلك بسلاسة، ناهيك عن تشغيل تتبع الكائن فوقه. لذلك، نقوم بتشغيل التتبع والمعاينة الحية على وكيل محول ترميزه (~1440×720، libx264 -preset veryfast -crf 28)، ونحتفظ بالأصل كامل الدقة للتصدير النهائي فقط.

// "more reliable than MediaMetadataRetriever for 360° videos
//  which often use unusual codecs (HEVC, high-res)"
final maxSafeWidth = 2048, maxSafeHeight = 1080;
// 2:1 4K (4096×2048) is unsafe -> trigger transcodeForPlayback()

المشكلة التي يجب عليك التعامل معها: يتم حساب صناديق تحديد الإطارات الرئيسية (keyframe bounding boxes) بإحداثيات الوكيل ويجب تحجيمها إلى الإحداثيات الأصلية قبل التصدير. إذا أخطأت في هذا التحجيم، فإن إطارات التصدير ستكون لمنطقة مختلفة قليلاً عما عاينه المستخدم. (نستخدم أيضًا ffprobe، وليس MediaMetadataRetriever، للميتا داتا واستخراج الإطار الأول - أداة استرجاع Android المدمجة غير مستقرة مع برامج الترميز غير العادية التي تنتجها كاميرات 360).

6. تصدير GPU: OpenGL + MediaCodec عبر MethodChannel

إعادة إسقاط كل إطار من كرة 4K عبر مرشح v360 لـ FFmpeg على وحدة المعالجة المركزية للهاتف بطيء جدًا. (نحتفظ بالفعل بسلسلة v360 — فهي توثق بدقة دلالات الإسقاط، v360=e:flat:... تعني "متساوي المستطيلات في الداخل، مسطح في الخارج" — لكنها ليست المسار المباشر).

يقوم التصدير الفعلي بتسليم الإطارات الرئيسية إلى مُصيّر OpenGL ES + MediaCodec أصلي عبر MethodChannel:

final result = await _methodChannel.invokeMethod('exportVideoGPU', {
  'videoPath': resolvedVideoPath
  'outputPath': outputPath
  'keyframes': keyframesList
  'initialOrientation': initialOrientationMap
  'width': settings.exportWidth
  'height': settings.exportHeight
  'fps': settings.outputFps.toInt()
  'includeAudio': settings.includeAudio
  if (isTrimmed) 'trimStartMs': trimStartMs
  if (isTrimmed) 'trimEndMs': trimEndMs
});

نقوم بحجب هذا خلف فحص القدرة (Android API 21+، مشفر H.264 للأجهزة، OpenGL ES 3.0) ونبث التقدم مرة أخرى عبر EventChannel. بعض المشاكل الصعبة التي تم التغلب عليها:

  • إزاحة yaw بمقدار -90°. اتفاقية Dart الخاصة بنا تضع yaw = 0 في منتصف الصورة المتساوية المستطيلات (u = 0.5)؛ يضع مصيّر OpenGL الـ yaw = 0 عند u = 0.75. لذا يتم إعادة تعيين كل yaw لإطار رئيسي بمقدار -90° (وإعادة لفه إلى ±180°) عند العبور عبر القناة — ويتم تطبيقه بشكل متماثل على كل من قناة التصدير وقناة المشغل المباشر، بحيث تتطابق المعاينة والتصدير.
  • عدد الإطارات الرئيسية المتدهور. صفر إطار رئيسي ← يتم توليف اثنين متطابقين من الاتجاه الأولي (تصدير ثابت). إطار رئيسي واحد ← تكراره. يريد المُصيّر دائمًا نقطة بداية ونهاية على الأقل.
  • التقطيع (Trimming) يعيد ضبط الطوابع الزمنية. تصدير مقطع يزيح كل إطار رئيسي بمقدار t − trimStartMs.

الـ FFmpeg post-pass الذي لا يتوقعه أحد

قد تعتقد أن تصدير GPU هو خط النهاية. ليس كذلك — لأن MediaMuxer الخاص بنظام Android يكتب ذرة moov في نهاية الملف ويمكن أن يحمل إزاحة PTS الصوتية للمصدر. والنتيجة: ExoPlayer (و video_player في Flutter) يجمد الصورة بينما يستمر الوقت في التشغيل. لذا بعد مرور GPU، يقوم FFmpeg بتنظيف سريع وبدون فقدان الجودة:

// copy video (no re-encode -> zero quality loss), re-encode audio to fix PTS,
// move moov atom to the front for instant playback.
-c:v copy -af "aresample=async=1:first_pts=0" -movflags +faststart

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

7. المعاينة المباشرة هي مشغل أصلي أيضًا

معاينة داخل المحرر ليست Flutter CustomPaint — إنها سطح OpenGL أصلي مدمج عبر AndroidView (بنوع عرض مثل 'your_app/spherical360player')، مع قناة method/event خاصة به لكل عرض. يقوم Dart Spherical360PlayerController بعكس yaw/pitch/roll/fov ويطبق نفس إزاحة -90 درجة قبل الإرسال.

تفاصيل الإحساس التي تجعلها ممتعة:

  • تنعيم مستقل عن معدل الإطارات — عامل = 1 - exp(-السرعة * دلتا الزمن) بحيث يبدو التخفيف هو نفسه عند 60 إطارًا في الثانية و 90 إطارًا في الثانية، مع تضمين yaw أقصر مسار آمن للحد الفاصل.
  • الزخم/القصور الذاتي بعد التمرير السريع، مع اضمحلال قائم على الاحتكاك — معطل أثناء التشغيل، لأنه أثناء التشغيل يجب أن تتبع الكاميرا الإطارات الرئيسية، وليس آخر حركة للمستخدم.
  • أداة GestureDetector واحدة يقوم onScaleUpdate الخاص بها بالتحريك الأفقي والتكبير/التصغير بالقرص معًا؛ النقر المزدوج يبدل بين التشغيل/الإيقاف المؤقت. FOV يتم تثبيته بين 30° و 120°، و pitch إلى ±90°، عند حدود المتحكم.

إصلاح صغير ولكنه مهم لتجربة المستخدم (UX): قبل التبديل من "التتبع" إلى "إنشاء الإطارات الرئيسية"، ندرج تأخيرًا بمقدار 120 مللي ثانية حتى يقوم Flutter بالرسم فعليًا "100%". بدون ذلك، يهبط إطار التقدم النهائي وتغيير الحالة في نفس دفعة المهام الصغيرة (microtask batch) ولا يرى المستخدم اكتمال العملية أبدًا.

آلة الحالة، باختصار

يتم تنسيق كل هذا بواسطة Riverpod ReframeNotifier الذي يدير تعداد الحالة (status enum):

خامل → تحميل الفيديو → تحويل الترميز
    → (كشف | تحديد الكائن | رسم bbox على المشغل | الكشف التلقائي على المشغل)
    → التتبع → توليد الإطارات الرئيسية → التحرير → التصدير → مكتمل | خطأ

تغذّي نقاط دخول متعددة نفس أداة التتبع: النقر للكشف (تراكب YOLO)، bbox يدوي، أو إيماءة الرسم المباشر على المشغل بأسلوب Insta360. كلها تتقارب على KeyframeGenerator.production().generate(...) → KeyframeSmoother.smooth(...)، ومن هناك يدفع المستوفي كل إطار معاين ومصدر.

الدروس المستفادة

  1. حافظ على نقاء الرياضيات. لا توجد واردات Flutter في طبقة الإسقاط/الاستيفاء. هذا هو السبب الوحيد الذي جعل هذا قابلًا للاختبار.
  2. استخدم Quaternion SLERP للاتجاه؛ LERP للقيم العددية. لا تستوفي زوايا أويلر، ولا تقم بـ SLERP لتكبيرك.
  3. الحد الفاصل ±180 درجة هو اهتمام شامل (cross-cutting concern)، وليس حالة حافة (edge case). يظهر مجددًا في العناصر الأولية، والتوليد، والإسقاط، والمشغل.
  4. وفّق بين اتفاقياتك مرة واحدة، بشكل متماثل. يتم تطبيق إزاحة yaw بمقدار -90 درجة بين Dart و OpenGL عند حدود القناة، بحيث تكون المعاينة == التصدير.
  5. وكيل للتفاعل، أصلي للإخراج. فقط تذكر أن تقوم بتحجيم إحداثياتك بين الاثنين.
  6. "انتهى التقديم" ≠ "يعمل بشكل صحيح." خصص ميزانية لعملية FFmpeg post-pass لإصلاح موضع ذرة moov و PTS الصوت، وتحقق قبل إضافة إشارات الصوت.
  7. الإحساس ميزة. قيود الحركة، وFOV التكيفي، وتقدم الحركة هي ما يفصل بين "إعادة التأطير التقنية" و "يبدو أن محترفًا قام بتصويره".

النتيجة: محرر إعادة تأطير 360 درجة متكامل يعمل على الجهاز — انقر على موضوع، ودعه يتتبع، وقم بتعديل الإطارات الرئيسية، وصدّر فيديو مسطحًا نظيفًا — مبني في Flutter مع طبقة GPU أصلية رقيقة تقوم فقط بالعمل الذي لا يستطيع Dart القيام به بالسرعة الكافية.

Flutter360VideoOpenGLQuaternions
Saurav Kumar Gupta's image

عن الكاتب

Saurav Kumar Gupta

AI & Cloud Solutions Expert at MicrocosmWorks

Building innovative AI-powered solutions and helping businesses transform through cutting-edge technology.

تريد معرفة المزيد؟

تواصل معنا لمناقشة كيف يمكننا مساعدتك في تنفيذ هذه الحلول لأعمالك.

تواصل معنا

الأسئلة الشائعة

Accurate reframing requires handling spherical projection, seamless rotation across the ±180° boundary, smooth camera interpolation, and synchronized GPU rendering to produce stable, cinematic video output.

Comments (0)

Share your thoughts and join the conversation

Leave a Comment

Your email will not be published

No comments yet

Be the first to share your thoughts!