דון בפרויקט שלך
MicrocosmWorksחדשנות ותכנון קוסמוס דיגיטלי
אודותצור קשר
MicrocosmWorksמחדשים ומתכננים קוסמוס דיגיטלי

מספקים פתרונות IT חשובים. אנו נלהבים מטכנולוגיה, אבטחה ועוזרים לעסקים לצמוח באמצעות תשתית IT אמינה וחדשנית.

hello@microcosmworks.com
+91 7011868196
New Delhi, India

פתרונות

בנייההנדסת מוצרי AIהנדסת מוצרי SaaSפיתוח תוכנה מותאמת אישית
מודרניזציהמודרניזציה של תוכנהמודרניזציה של AIמודרניזציה של אפליקציות ענן
הרחבהמערכות Backend ומבוזרותהנדסת ביצועי ענןהנדסת אמינות וביצועיםתשתית AI
הגדלהצוותי הנדסת מוצר
כל הפתרונותפיתוח סוכני AIפלטפורמת וידאו AIאפליקציות בריאות וכושר

שירותים

ייעוץ דיגיטליתשתית ענןפיתוח SaaSפיתוח AIטכנולוגיית וידאו
פיתוח ERPהתאמה אישית של Zohoפיתוח Odooאינטגרציה של Salesforceפיתוח CRM מותאם אישית
אינטגרציה של QuickBooksפתרונות IoTפיתוח בלוקצ'יין
ייעוץ סייברתמיכה טכנית - L3

מרכז צמיחה AI

מרכז AIחדשנות סטארטאפמאיץ ארגוני

משאבים

תובנותמדריכי תעשייהתוכניות מקרה שימושתבניות ארכיטקטורהמחקרי מקרה

חברה

אודותינוצור קשרדון בפרויקט שלךהעבודה שלנו

© 2026 MicrocosmWorks. כל הזכויות שמורות.

מדיניות פרטיותתנאי שירות
חזרה לתובנות
AI Development

שליטה ב-GoPro דרך WiFi עם Flutter: HTTP API והעברת קבצים

חיבור ל-GoPro דרך WiFi מ-Flutter באמצעות ה-Open GoPro HTTP API, עם שמירה על חיבור והעברת קבצים בסטרימינג.

Saurav Kumar Gupta's image Saurav Kumar Gupta
•
July 17, 2026
•
עודכן October 4, 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 רגיל מול ה-API של Open GoPro של המצלמה בכתובת קבועה:

http://10.5.5.9:8080

שליטה במצלמה, הגדרות, רישום מדיה והורדת קבצים הם כולם בקשות HTTP GET לאותו IP. האינטגרציה כולה היא 100% Dart עם חבילת ה-dio — אפס Kotlin/Swift מקומי. זו הכותרת, וכפי שנראה בסוף, זהו גם המקום היחיד שבו העיצוב שביר.

התהליך:

הצטרפות ל-GoPro WiFi AP (הגדרות טלפון)
   → זיהוי שהמצלמה נגישה (בדיקת TCP ל-10.5.5.9:8080)
   → התחברות + זיהוי דגם (GET /gopro/camera/state)
   → הפעלת טיימרים לשמירה על חיבור (30 שניות) + סקר מצב (5 שניות)
   → שליטה: תריס / מצב / הגדרות
   → רישום מדיה (GET /gopro/media/list)
   → הורדה עם טורבו + התקדמות (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);
    }
  }
}

אנחנו מבצעים סקר כל 2 שניות ומשדרים שינויי מצב דרך זרם. מסך החיבור נרשם ומתהפך ל"מחובר" ברגע שהשקע נפתח. מכיוון שאין גילוי התקנים אמיתי, השירות מסנתז 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'},
  ));
}

ה-API של Open GoPro אחיד להפליא: הכל הוא בקשת HTTP GET, אפילו פקודות ושינויי הגדרות. הנה נקודות הקצה שהאפליקציה הזו משתמשת בהן בפועל:

מטרהמתודה + נתיבשאילתה
הפעלת תריס (הקלטה / צילום)GET /gopro/camera/shutter/start—
עצירת תריסGET /gopro/camera/shutter/stop—
שמירה על חיבורGET /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
העברת טורבוGET /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 שניות של חוסר פעילות, נקודת הגישה של ה-WiFi ב-GoPro מנתקת את הסשן שלכם. החיבור שלכם, שהיה תקין לחלוטין, מת בשקט באמצע סשן אם לא "תדקרו" אותו. לכן, אנו מפעילים שני טיימרים בהתחברות:

  • שמירה על חיבור כל 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

התריס הוא החלק הקל — התחלה, המתנה של 500 מילישניות, סקר חוזר של המצב. לכידת תמונה היא אותו תריס/התחלה; אם תקבלו וידאו או תמונה תלוי במצב הנוכחי.

החלפת מצבים היא המקום שבו זה נהיה מעניין, מכיוון ש"מצב" ב-GoPro MAX 2 הוא למעשה שתי הגדרות: קבוצת הגדרות מוגדרות מראש (וידאו = 1000, תמונה = 1001, Timelapse = 1002) בתוספת עדשה (הגדרה 194: 0 = עדשה בודדת/HERO, 1 = 360). כדי לעשות זאת נכון נדרשו מספר כללים לא ברורים שהוטמעו בקוד:

  1. סדרת שינויי מצב. לחיצות מהירות על תת-מצבים נכנסות לתור דרך שרשרת Completer כך שהן לא יוכלו להתחרות זו בזו למצב לא עקבי.
  2. עצרו את התריס קודם. הקושחה דוחה שינויים בקביעות מוגדרות מראש עם HTTP 400 בזמן הקלטה, אז אנחנו עוצרים באופן יזום את הקידוד לפני המעבר.
  3. סדר ועיכובים חשובים. ב-MAX 2: הגדירו את קבוצת הקביעות המוגדרות מראש תחילה (+400 אלפיות השנייה), ואז את העדשה דרך הגדרה 194 (+1500 אלפיות השנייה). ההשהיה של 1.5 שניות אמיתית — זוהי העברת מכלול העדשה הפיזי.
  4. סמכו על המצב המתוכנן, לא על המצב שנסקר. אנחנו עוקבים אחר _lastIntendedIs360 / _lastIntendedPresetGroup ומשווים אליהם, מכיוון שסקר המצב של 5 שניות יכול למסור לכם תמונת מצב מיושנת באמצע המעבר.

יש גם מוזרות של GoPro ששווה לציין: מזהי רזולוציה תלויים ביחס הגובה-רוחב. 4K היא אפשרות 1 ב-16:9, 109 ב-9:16, ו-112 ב-4:3. לכן, לאחר שינוי יחס הגובה-רוחב אנו שולחים שאילתה מחדש ליכולות — אותו מזהה היה אחרת אומר רזולוציה שונה.

5. רישום מדיה

GET /gopro/media/list מחזיר מבנה מקובץ לפי ספריות עם מפתחות תמציתיים מפורסמים: d = ספרייה, fs = קבצים, n = שם קובץ, s = גודל, cre/mod = חותמות זמן ביוניקס-שניות, g = מזהה קבוצה, glrv = גודל פרוקסי ברזולוציה נמוכה.

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, GX = וידאו HERO, GT = Timelapse, .360 = וידאו כדורי, .GPR/.RAW = תמונה גולמית. קידומות אלו חשובות בהמשך, מכיוון שקבצי .360 מתנהגים באופן שונה מכל השאר.

6. הורדות בסטרימינג: טורבו, התקדמות וקבצים נלווים

העברת הבייטים בפועל היא dio.download() ישירות לנתיב קובץ, עם זמן קבלת פסיקה ארוך מכיוון שקליפ .360 בגודל 1 GB לוקח זמן:

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_transfer?p=1) — GoPro עוברת ל-WiFi בתדר 5 GHz להעברות מהירות יותר באופן ניכר — ואנו מכבים אותו בבלוק finally כך שהוא תמיד ינוקה, גם במקרה של שגיאה.

התקדמות + מהירות, מווסת. חישוב מהירות העברה בכל קריאת 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 שיתופי; בביטול אנו מוחקים את הקובץ החלקי ושולחים אירוע ביטול.

קבצים נלווים. זה החלק שלא תמצאו בתיעוד. עבור קובץ .MP4 עם עדשה בודדת אנו שולפים גם את ה-.LRV (פרוקסי ברזולוציה נמוכה ש-GoPro מקליטה לצד הווידאו המלא) ואת התמונה הממוזערת .THM, ואז מבצעים אינדוקס ל-LRV להפעלת מדיה מהירה בתוך האפליקציה. שתי מלכודות שמותיות מכאיבות כאן:

  • HERO8+ משנים את שם הפרוקסי: GX######.MP4 / GH######.MP4 הופך ל-GL######.LRV (התו השני משתנה ל-L), עם גיבוי של שם זהה למצלמות ישנות יותר.
  • קבצי .360 אינם תומכים כלל בנקודות הקצה של thumbnail או screennail. אנו נופלים חזרה לקובץ הנלווה .THM, וכמוצא אחרון מורידים את הקובץ עצמו. ומכיוון ש-.360 הוא "dual-fisheye" (עין דג כפולה), אנו חותכים את התמונה הממוזערת לעדשה הקדמית — ברקע נפרד באמצעות compute(), כך שממשק המשתמש לעולם לא נתקע.

מצב ההעברה נחשף באמצעות Riverpod notifier שנרשם לזרימת הקבצים וההעברות, שומר העברה שהסתיימה גלויה למשך 2 שניות, ומציג שגיאות למשך 5. ראוי לציין: הבקרה והעברת הקבצים משתמשות בשני לקוחות Dio נפרדים לאותה מצלמה, כך שהורדה יכולה להתבצע בזמן שסקר המצב וה-keep-alive ממשיכים.

7. שתי מלכודות שלא נמצאות באף מדריך

HTTP טקסט פשוט חסום כברירת מחדל ב-Android מודרני

http://10.5.5.9:8080 הוא טקסט פשוט, ו-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 הקבועה לנקודת הגישה. אבל זהו החלק השביר ביותר בארכיטקטורה, ובמכשירים/גרסאות Android מסוימות זה הדבר הראשון שנשבר. אם אתם משיקים את זה, הוסיפו את קישור הרשת — זה המקום היחיד שבו הישארות ב-100% Dart עולה לכם באמינות.

לקחים שנלמדו

  1. אתם לא צריכים BLE. אם המשתמש יכול להצטרף לנקודת הגישה, ה-Open GoPro HTTP API מעניק לכם שליטה מלאה והעברות מהירות ב-Dart טהור.
  2. זהו על ידי בדיקת TCP, לא סריקת SSID — ללא הרשאת מיקום, ללא קוד פלטפורמה.
  3. Keep-alive כל 30 שניות או שהסשן ימות. התייחסו ל-keep-alive כושל כאות הניתוק שלכם.
  4. תנו למצלמה לתקן אתכם. לולאת הריפוי העצמי של 403-available_options עדיפה על מידול מטריצת ההגדרות כולה של GoPro.
  5. החלפת מצבים היא מכונת מצבים — סדרו אותה, עצרו את ההקלטה תחילה, כבדו את השהיות מעבר העדשה, וסמכו על המצב המתוכנן על פני המצב שנסקר.
  6. קבצים נלווים ומוזרויות .360 הם המקום בו הקשיים חבויים — שינוי שם LRV, אין נקודת קצה לתמונות ממוזערות, חיתוך dual-fisheye.
  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.

רוצים ללמוד עוד?

צרו קשר לדון כיצד נוכל לעזור ליישם פתרונות אלו עבור העסק שלכם.

צרו קשר

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!