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. جميع الحقوق محفوظة.

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

التحكم في GoPro عبر WiFi في Flutter: Open GoPro HTTP API، وKeep-Alives، ونقل الملفات بالتدفق

الاتصال بـ GoPro عبر WiFi من Flutter باستخدام Open GoPro HTTP API، مع keep-alives ونقل الملفات بالتدفق.

Saurav Kumar Gupta's image Saurav Kumar Gupta
•
July 17, 2026
•
تم التحديث July 30, 2026
•
2 min read
GoPro camera connected to a Flutter app over Wi-Fi for remote control and media file transfer..webp
2 min read

لا يوجد SDK، ولا روتين إقران BLE — فقط IP الكاميرا الثابت، وعميل Dio، وعدد قليل من المشكلات التي ستفسد يومك بهدوء إذا لم تكن تعرف عنها.

لماذا WiFi، وليس Bluetooth؟

تبدأ معظم برامج "الاتصال بـ GoPro" باستخدام Bluetooth Low Energy: مسح، إقران، تبادل خصائص GATT، ثم استخدام BLE لتشغيل WiFi، ثم النقل عبر WiFi على أي حال. هذا يعمل، ولكنه يتطلب الكثير من الإجراءات — وBLE بطيء ومعقد لما يريده المستخدمون بالفعل، وهو التحكم في الكاميرا وسحب اللقطات منها بسرعة.

لذا في هذا التطبيق، نتخطى BLE بالكامل. ينضم المستخدم إلى نقطة وصول WiFi الخاصة بـ GoPro من إعدادات هاتفه، ومن تلك النقطة فصاعدًا، كل شيء هو HTTP عادي مقابل Open GoPro API الخاص بالكاميرا على عنوان ثابت:

http://10.5.5.9:8080

التحكم في الكاميرا، الإعدادات، قائمة الوسائط، وتنزيل الملفات كلها طلبات HTTP GET إلى هذا الـ IP. التكامل بالكامل 100% Dart مع حزمة dio — لا يوجد Kotlin/Swift أصلي على الإطلاق. هذا هو العنوان الرئيسي، وكما سنرى في النهاية، إنه أيضًا المكان الوحيد الذي يكون فيه التصميم هشًا.

التدفق:

الانضمام إلى GoPro WiFi AP (إعدادات الهاتف)
   → اكتشاف أن الكاميرا يمكن الوصول إليها (TCP probe إلى 10.5.5.9:8080)
   → الاتصال + تحديد الطراز (GET /gopro/camera/state)
   → بدء مؤقتات keep-alive (30 ثانية) + استقصاء الحالة (5 ثوانٍ)
   → التحكم: الغالق / الوضع / الإعدادات
   → سرد الوسائط (GET /gopro/media/list)
   → التنزيل بـ turbo + التقدم (dio.download)

1. اكتشاف الكاميرا بدون أذونات مسح WiFi

الغريزة الأولى هي تعداد WiFi SSIDs والبحث عن واحد يبدأ بـ GoPro. لا تفعل ذلك — فهذا يجلب معه أذونات الموقع وAPIs مسح WiFi الخاصة بالمنصة.

GoPro دائمًا موجودة على نفس الـ IP، لذا الكشف يكون مجرد: هل يمكنني فتح مقبس TCP إلى 10.5.5.9:8080؟ لا يوجد HTTP، ولا أذونات، ولا تحليل SSID.

static const String goProIp = '10.5.5.9';
static const int goProPort = 8080;
static const Duration checkInterval = Duration(seconds: 2);
static const Duration connectionTimeout = Duration(seconds: 3);

Future<void> _checkConnection() async {
  try {
    final socket = await Socket.connect(goProIp, goProPort, timeout: connectionTimeout);
    socket.destroy();
    if (_currentState != GoProWifiState.connected) {
      _currentState = GoProWifiState
.connected;
      _connectionStateController.add(_currentState);
    }
  } catch (e) {
    if (_currentState != GoProWifiState.disconnected) {
      _currentState = GoProWifiState.disconnected;
      _connectionStateController.add(_currentState);
    }
  }
}

نحن نستقصي ذلك كل ثانيتين ونبث تغييرات الحالة عبر stream. تشترك شاشة الاتصال وتتحول إلى "متصل" لحظة فتح الـ socket. نظرًا لعدم وجود اكتشاف حقيقي للجهاز، تقوم الخدمة بإنشاء CameraDevice افتراضي (معتمد على الـ IP الثابت) حتى لا تحتاج بقية تجريد الكاميرا إلى معرفة الفرق.

2. عميل HTTP: Dio ونقاط نهاية Open GoPro

العميل هو غلاف Dio رفيع. لاحظ مهلة الاتصال القصيرة (أنت على شبكة LAN — إذا كانت بطيئة، فهي معطلة) ومهلة استقبال سخية لمكالمات التحكم:

static const String baseUrl = 'http://10.5.5.9:8080';

GoProHttpClient() {
  _dio = Dio(BaseOptions(
    baseUrl: baseUrl,
    connectTimeout: const Duration(seconds: 5),
    receiveTimeout: const Duration(seconds: 10),
    headers: {'Accept': 'application/json'},
  ));
}

إن Open GoPro API موحد بشكل رائع: كل شيء هو HTTP GET، حتى الأوامر وتغييرات الإعدادات. فيما يلي نقاط النهاية التي يستخدمها هذا التطبيق بالفعل:

الغرضالأسلوب + المساراستعلام
بدء الغالق (تسجيل / صورة)GET /gopro/camera/shutter/start—
إيقاف الغالقGET /gopro/camera/shutter/stop—
Keep-aliveGET /gopro/camera/keep_alive—
الحالة الكاملة (الحالة + الإعدادات)GET /gopro/camera/state—
تعيين مجموعة إعدادات مسبقةGET /gopro/camera/presets/set_groupid (1000/1001/1002)
تغيير إعدادGET /gopro/camera/settingsetting, option
قائمة الوسائطGET /gopro/media/list—
تنزيل ملفGET /videos/DCIM/{dir}/{file}(bytes)
صورة مصغرة / screennailGET /gopro/media/thumbnail / .../screennailpath
نقل turboGET /gopro/media/turbo_transferp (1/0)
حذف مجموعة .360 (قديمة)GET /gp/gpControl/command/storage/delete/groupp

تفصيل لطيف: عائلتا /gopro/... الحديثة و /gp/gpControl/... القديمة تتعايشان. إزالة ملفات المجموعة chaptered.360/GS، التي لا يستطيع الـ API الجديد التعامل معها بشكل نظيف، هو الموقف الوحيد الذي نلجأ فيه إلى نقطة النهاية القديمة.

نمط الشفاء الذاتي 403 (أنظف خدعة هنا)

لا يمكنك معرفة أي الإعدادات صالحة في الوضع الحالي للكاميرا مقدمًا — دقة قانونية في 16:9 تكون غير قانونية في 9:16، تعتمد خيارات FPS على الدقة، وهكذا. تخبرك GoPro بذلك بالطريقة الصعبة: تعيد HTTP 403 مع قائمة الخيارات التي كانت ستكون صالحة، وHTTP 500 عندما تكون مشغولة مؤقتًا فقط.

لذلك نقوم بتحليل جسم 403 إلى خطأ مكتوب ونعيد محاولة الـ 500:

Future<void> setSetting(int settingId, int optionValue) async {
  const maxRetries = 3;
  for (int attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      await _dio.get('/gopro/camera/setting',
          queryParameters: {'setting': settingId, 'option': optionValue});
      return;
    } catch (e) {
      if (e is DioException && e.type == DioExceptionType.badResponse) {
        final statusCode = e.response?.statusCode;
        if (statusCode == 403) {
          final body = e
.response?.data;
          List<int> availableIds = [];
          if (body is Map<String, dynamic>) {
            final options = body['available_options'];
            if (options is List) {
              availableIds = options
                  .whereType<Map<String, dynamic>>()
                  .map((o) => o['id'] as int? ?? -1)
                  .where((id) => id >= 0).toList();
            }
          }
          throw SettingRejectedError(
            settingId: settingId, rejectedOption: optionValue,
            availableOptionIds: availableIds);
        }
        if (statusCode == 500 && attempt < maxRetries) {
          await Future.delayed(Duration(milliseconds: attempt * 500)); // linear backoff
          continue;
        }
      }
      rethrow;
    }
  }
}

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

3. Keep-alive: المؤقت الذي يجب ألا تنساه

بعد حوالي 60 ثانية من عدم النشاط، تنهي نقطة وصول GoPro WiFi AP جلستك. تموت اتصالك الجيد تمامًا بهدوء في منتصف الجلسة إذا لم تنشطه. وبالتالي، نبدأ مؤقتين عند الاتصال:

  • Keep-alive كل 30 ثانية ← GET /gopro/camera/keep_alive
  • استقصاء الحالة كل 5 ثوانٍ ← GET /gopro/camera/state

void _startKeepAlive() {
  _keepAliveTimer?.cancel();
  _keepAliveTimer = Timer.periodic(
    GoProHttpConstants.keepAliveInterval, // 30s
    (_) => _sendKeepAlive(),
  );
}

Future<void> _sendKeepAlive() async {
  if (!isConnected || _isDisposed) return;
  try {
    await _httpClient.keepAlive();
  } catch (e) {
    if (!_isDisposed) _handleDisconnection(); // a failed keep-alive == we're disconnected
  }
}

فشل الـ keep-alive هو أيضًا إشارة قطع الاتصال لدينا — إنها الطريقة الأكثر موثوقية لملاحظة أن المستخدم خرج من نطاق WiFi.

4. التحكم في الكاميرا: الغالق، الوضع، وMAX 2 lens dance

الغالق هو الجزء السهل — ابدأ، انتظر 500 مللي ثانية، أعد استقصاء الحالة. التقاط الصور هو نفس shutter/start؛ سواء حصلت على فيديو أو صورة يعتمد على الوضع الحالي.

تبديل الوضع هو الجزء المثير للاهتمام، لأن "الوضع" في GoPro MAX 2 هو في الواقع إعدادان: مجموعة إعدادات مسبقة (Video = 1000، Photo = 1001، Timelapse = 1002) بالإضافة إلى عدسة (Setting 194: 0 = single-lens/HERO، 1 = 360). للحصول على هذا بشكل صحيح تطلب العديد من القواعد غير البديهية المضمنة في الكود:

  1. تسلسل تغييرات الوضع. يتم وضع نقرات الوضع الفرعي السريعة في قائمة انتظار عبر سلسلة Completer حتى لا تتسابق مع بعضها البعض في حالة غير متناسقة.
  2. أوقف الغالق أولاً. يرفض الفيرموير تغييرات الإعدادات المسبقة بـ HTTP 400 أثناء التسجيل، لذلك نوقف الترميز بشكل استباقي قبل التبديل.
  3. الترتيب والتأخيرات مهمة. على MAX 2: اضبط مجموعة الإعدادات المسبقة أولاً (+400 مللي ثانية)، ثم العدسة عبر Setting 194 (+1500 مللي ثانية). هذا التوقف لمدة 1.5 ثانية حقيقي — إنه انتقال تجميع العدسة المادية.
  4. ثق بالحالة المقصودة، وليس بالحالة المستقصاة. نتتبع _lastIntendedIs360 / _lastIntendedPresetGroup ونقارن معها، لأن استقصاء الحالة كل 5 ثوانٍ يمكن أن يعطيك لقطة قديمة في منتصف الانتقال.

هناك أيضًا خاصية GoPro مخادعة حقًا تستحق الذكر: تعتمد IDs الدقة على نسبة العرض إلى الارتفاع. 4K هو الخيار 1 في 16:9، و109 في 9:16، و112 في 4:3. لذلك بعد تغيير نسبة العرض إلى الارتفاع، نعيد الاستعلام عن القدرات — وإلا فإن نفس الـ ID سيعني دقة مختلفة.

5. سرد الوسائط

GET /gopro/media/list يعيد بنية مجمعة حسب الدليل مع مفاتيح موجزة مشهورة: d = directory (الدليل)، fs = files (الملفات)، n = filename (اسم الملف)، s = size (الحجم)، cre/mod = unix-second timestamps (أختام زمنية بوحدة ثانية يونكس)، g = group id (معرف المجموعة)، glrv = low-res-proxy size (حجم الوكيل منخفض الدقة).

final media = response['media'] as List<dynamic>?;
for (final dir in media) {
  final directory = dirMap['d'] as String? ?? '';
  final filesList = dirMap['fs'] as List<dynamic>?;
for (final fileJson in filesList) {
    files.add(GoProCameraFile.fromJson(directory, fileMap));
  }
}
files.sort((a, b) => b.modifiedAt.compareTo(a.modifiedAt)); // newest first

يتم استنتاج نوع الوسائط من بادئة اسم الملف + الامتداد: GS = 360 video (فيديو 360)، GX = HERO video (فيديو HERO)، GT = timelapse (تايم لابس)، .360 = spherical video (فيديو كروي)، .GPR/.RAW = raw photo (صورة خام). هذه البادئات مهمة لاحقًا، لأن ملفات .360 تتصرف بشكل مختلف عن أي شيء آخر.

6. تنزيلات التدفق: turbo، والتقدم، والملفات المرافقة

نقل البايت الفعلي هو dio.download() مباشرة إلى مسار ملف، مع مهلة استقبال طويلة لأن مقطع .360 بحجم 1 غيغابايت يستغرق بعض الوقت:

Future<void> downloadMediaToFile(
  String directory, String filename, String destPath,
  {void Function(int received, int total)? onProgress}) async {
  await _dio.download(
    '/videos/DCIM/$directory/$filename',
    destPath,
    onReceiveProgress: onProgress,
    options: Options(receiveTimeout: const Duration(minutes: 30)),
  );
}

حول هذا الأساس، تقوم خدمة النقل بتنسيق التفاصيل الواقعية:

نقل Turbo. قبل التنزيل، نقوم بتشغيل وضع Turbo (turbo_transfer?p=1) — حيث تتحول GoPro إلى WiFi بتردد 5 غيغاهرتز لنقل أسرع بشكل ملحوظ — ونقوم بإيقافه في block 'finally' بحيث يتم تنظيفه دائمًا، حتى في حالة وجود خطأ.

التقدم + السرعة، مقيدة. حساب سرعة النقل في كل callback لـ onReceiveProgress يعد مضيعة ومتقلبًا، لذلك نعيد حسابه فقط كل 500 مللي ثانية:

onProgress: (received, total) {
  if (_isCancelled) return;
  final now = DateTime.now();
  String? speed;
  if (now.difference(lastSpeedUpdate).inMilliseconds >= 500) {
    final bytesPerSecond = (received - lastBytes) /
        (now.difference(lastSpeedUpdate).inMilliseconds / 1000);
    speed = _formatSpeed(bytesPerSecond.round());
    lastSpeedUpdate = now; lastBytes = received;
  }
  _transferController.add(GoProFileTransfer(
    status: FileTransferStatus.downloading,
    progress: total > 0 ? received / total : 0,
    speed: speed, currentBytes: received, totalBytes: total));
}

الإلغاء هو علامة _isCancelled تعاونية؛ عند الإلغاء، نحذف الملف الجزئي ونصدر حدث إلغاء.

الملفات المرافقة. هذا هو الجزء الذي لن تجده في الوثائق (docs). بالنسبة لملف .MP4 أحادي العدسة، نقوم أيضًا بسحب .LRV (وكيل منخفض الدقة تسجله GoPro جنبًا إلى جنب مع الفيديو الكامل) والصورة المصغرة .THM، ثم نفهرس الـ LRV للتشغيل السريع داخل التطبيق. هناك مشكلتان في التسمية تبرزان هنا:

  • HERO8+ تعيد تسمية الوكيل: GX######.MP4 / GH######.MP4 يصبح GL######.LRV (الحرف الثاني يتغير إلى L)، مع خيار احتياطي بنفس الاسم للكاميرات القديمة.
  • ملفات .360 لا تدعم نقاط نهاية الصورة المصغرة أو الـ screennail على الإطلاق. نلجأ إلى الملف المرافق .THM، وكحل أخير نقوم بتنزيل الملف نفسه. ولأن .360 هو ثنائي عين السمكة (dual-fisheye)، فإننا نقص الصورة المصغرة لتناسب العدسة الأمامية — في عزلة خلفية عبر compute()، حتى لا تتوقف واجهة المستخدم (UI) أبدًا.

يتم عرض حالة النقل من خلال Riverpod notifier الذي يشترك في ملفات وstreams النقل، ويحافظ على النقل المكتمل مرئيًا لمدة ثانيتين، ويعرض الأخطاء لمدة 5 ثوانٍ. جدير بالذكر: التحكم ونقل الملفات يستخدمان عميلين Dio مستقلين لنفس الكاميرا، لذلك يمكن تشغيل التنزيل بينما يستمر استقصاء الحالة وkeep-alive.

7. مشكلتان غير موجودتين في أي دليل تعليمي

يتم حظر Cleartext HTTP افتراضيًا على أجهزة Android الحديثة

http://10.5.5.9:8080 هو نص عادي (plaintext)، وتمنع Android 9+ حركة مرور Cleartext تلقائيًا. يجب عليك السماح بها صراحةً:

<!-- AndroidManifest.xml -->
<application android:usesCleartextTraffic="true" ... >

بالإضافة إلى ملف network_security_config.xml الذي يسمح إعداده الأساسي بـ Cleartext (لا يزال بإمكانك فرض HTTPS لنطاقات السحابة/الواجهة الخلفية الخاصة بك). إذا فاتك هذا، فستفشل جميع مكالمات GoPro بخطأ اتصال محير.

ربط الشبكة المفقود — النقطة الهشة في التصميم

هذا هو الجزء الصادق. على أجهزة Android الحديثة، عند الانضمام إلى GoPro AP، عادة ما يحافظ الهاتف على الشبكة الخلوية كشبكة افتراضية لأن GoPro AP لا تحتوي على إنترنت. هذا يعني أن طلب HTTP إلى 10.5.5.9 يمكن أن يتم توجيهه عبر الشبكة الخلوية ويفشل — حتى لو كنت "متصلاً" بالكاميرا.

الحل القوي هو الحل الأصلي: استدعاء ConnectivityManager.bindProcessToNetwork(goProNetwork) قبل النقل وbindProcessToNetwork(null) بعد ذلك. مسار GoPro النقي المكتوب بـ Dart لا يوجد له مكافئ — ويميل إلى العمل على أي حال لأن نظام التشغيل يوجه بشكل عام عنوان 10.5.5.9 الثابت إلى الـ AP. لكن هذا هو الجزء الأكثر هشاشة في البنية، وفي بعض الأجهزة/إصدارات Android، هو أول ما يتعطل. إذا قمت بنشر هذا، أضف ربط الشبكة — إنه المكان الوحيد الذي يكلفك فيه البقاء 100% Dart الموثوقية.

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

  1. لا تحتاج إلى BLE. إذا تمكن المستخدم من الانضمام إلى الـ AP، فإن Open GoPro HTTP API يمنحك تحكمًا كاملاً ونقلًا سريعًا في Dart النقي.
  2. اكتشف بواسطة TCP probe، وليس مسح SSID — لا يوجد إذن موقع، ولا كود منصة.
  3. Keep-alive كل 30 ثانية وإلا تموت الجلسة. تعامل مع فشل keep-alive كإشارة قطع الاتصال الخاصة بك.
  4. دع الكاميرا تصححك. حلقة الشفاء الذاتي 403-available_options تتفوق على نمذجة مصفوفة إعدادات GoPro بأكملها.
  5. تبديل الوضع هو آلة حالات (state machine) — قم بتسلسله، أوقف التسجيل أولاً، احترم تأخيرات انتقال العدسة، وثق بالحالة المقصودة على الحالة المستقصاة.
  6. الملفات المرافقة وغرائب .360 هي حيث تكمن المشاكل الخفية — إعادة تسمية LRV، لا توجد نقطة نهاية للصور المصغرة، واقتصاص ثنائي عين السمكة.
  7. مشكلتان في البنية التحتية ستغرقانك بصمت: تكوين حركة مرور Cleartext، و(خاصة) ربط العملية بشبكة الكاميرا حتى لا تتسرب الطلبات عبر الشبكة الخلوية.

المكافأة: هاتف يتصل بـ GoPro عبر WiFi، يدير كل تحكم، ويبث جيجابايت من اللقطات منه مع تقدم مباشر — وكل ذلك تقريبًا مجرد Dart منظم جيدًا يتحدث إلى http://10.5.5.9:8080.
 

FlutterGoProHTTP APIStreaming
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.

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

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

تواصل معنا

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

Yes. Once the phone is connected to the GoPro's WiFi network, the Open GoPro HTTP API allows you to control the camera, change settings, list media, and download files using standard HTTP requests.

GoPro automatically ends inactive WiFi sessions after about 60 seconds. Sending periodic keep-alive requests maintains the connection and prevents unexpected disconnections during camera control or file transfers.

Flutter apps can use the Open GoPro HTTP API with Dio for streaming downloads, enable Turbo Transfer for faster speeds, and track download progress in real time.

A 403 response indicates the requested setting isn't valid for the camera's current mode. The API also returns supported options, allowing the application to adapt automatically.

On Android, requests may be routed over cellular instead of the GoPro's WiFi network. Binding the app to the GoPro network improves connection reliability during camera control and media transfers.

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!