MicrocosmWorksInnover et Architecturer le Cosmos Numérique
Ă€ proposContact
MicrocosmWorksInnover et architecturer des cosmos numériques

Fournir des solutions informatiques qui comptent. Nous sommes passionnés par la technologie, la sécurité et aidons les entreprises à croître grâce à une infrastructure informatique fiable et innovante.

[email protected]
+91 7011868196
New Delhi, India

Hub de Croissance IA

Hub IAInnovation pour les startupsAccélérateur d'entreprise

Solutions

Toutes les solutionsApplications de bien-être et de fitnessPlateforme vidéo IADéveloppement d'agents IA

Ressources

PerspectivesGuides de l'industriePlans d'utilisationModèles d'architectureÉtudes de cas

Entreprise

Ă€ propos de nousContactNotre travail

Services

Consultation numériqueInfrastructure cloudDéveloppement SaaSDéveloppement IATechnologie vidéo
Développement ERPPersonnalisation ZohoDéveloppement OdooIntégration SalesforceDéveloppement CRM personnalisé
Intégration QuickBooksSolutions IoTDéveloppement Blockchain
Consultation en cybersécuritéSupport IT - L3

© 2026 MicrocosmWorks. Tous droits réservés.

Politique de confidentialitéConditions d'utilisation
Retour aux Perspectives
AI Development

ContrĂ´ler une GoPro via WiFi dans Flutter : L'API HTTP Open GoPro, les Keep-Alives et le Transfert de Fichiers en Streaming

Connexion Ă  une GoPro via WiFi depuis Flutter en utilisant l'API HTTP Open GoPro, avec des keep-alives et un transfert de fichiers en streaming.

Saurav Kumar Gupta's image Saurav Kumar Gupta
•
July 17, 2026
•
Mis Ă  jour 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

Pas de SDK, pas de danse d'appairage BLE — juste l'IP fixe de la caméra, un client Dio, et une poignée de pièges qui ruineront discrètement votre journée si vous ne les connaissez pas.

Pourquoi le WiFi, pas le Bluetooth ?

La plupart des tutoriels "se connecter à une GoPro" commencent par le Bluetooth Low Energy : scanner, appairer, échanger les caractéristiques GATT, puis utiliser le BLE pour activer le WiFi, puis transférer via WiFi de toute façon. Cela fonctionne, mais c'est beaucoup de cérémonie — et le BLE est lent et compliqué pour ce que les utilisateurs veulent réellement, à savoir contrôler la caméra et en extraire rapidement les séquences.

Ainsi, dans cette application, nous sautons entièrement le BLE. L'utilisateur se connecte au point d'accès WiFi de la GoPro depuis les paramètres de son téléphone, et à partir de ce moment, tout est en HTTP simple contre l'API Open GoPro de la caméra à une adresse fixe :

http://10.5.5.9:8080

Le contrôle de la caméra, les paramètres, la liste des médias et le téléchargement de fichiers sont tous des requêtes HTTP GET vers cette IP. L'intégration entière est 100% Dart avec le package dio — zéro Kotlin/Swift natif. C'est le point principal, et comme nous le verrons à la fin, c'est aussi le seul endroit où la conception est fragile.

Le flux :

rejoindre le GoPro WiFi AP (paramètres du téléphone)
   → dĂ©tecter que la camĂ©ra est accessible (TCP probe vers 10.5.5.9:8080)
   → connecter + identifier le modèle (GET /gopro/camera/state)
   → dĂ©marrer les minuteurs keep-alive (30s) + status poll (5s)
   → contrĂ´le : obturateur / mode / paramètres
   → lister les mĂ©dias (GET /gopro/media/list)
   → tĂ©lĂ©charger avec turbo + progression (dio.download)

1. Détection de la caméra sans autorisations de WiFi-scan

Le premier réflexe est d'énumérer les SSID WiFi et de chercher un commençant par GoPro. Ne le faites pas — cela entraîne des autorisations de localisation et des API de WiFi-scan spécifiques à la plateforme.

GoPro est toujours à la même IP, donc la détection est simplement : puis-je ouvrir un socket TCP vers 10.5.5.9:8080 ? Pas de HTTP, pas d'autorisations, pas de parsing de 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);
    }
  }
}

Nous interrogeons cela toutes les 2 secondes et diffusons les changements d'état via un stream. L'écran de connexion s'abonne et passe à "connecté" dès l'ouverture du socket. Comme il n'y a pas de véritable découverte de périphérique, le service synthétise un CameraDevice virtuel (indexé sur l'IP fixe) afin que le reste de l'abstraction de la caméra n'ait pas besoin de connaître la différence.

2. Le client HTTP : Dio et les endpoints Open GoPro

Le client est un mince wrapper Dio. Notez le court timeout de connexion (vous êtes sur un LAN — si c'est lent, c'est cassé) et un généreux receive timeout pour les appels de contrôle :

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'},
  ));
}

L'API Open GoPro est délicieusement uniforme : tout est une requête HTTP GET, même les commandes et les changements de paramètres. Voici les endpoints que cette application utilise réellement :

ObjectifMéthode + CheminRequête
Démarrer l'obturateur (enregistrer / photo)GET /gopro/camera/shutter/start—
Arrêter l'obturateurGET /gopro/camera/shutter/stop—
Keep-aliveGET /gopro/camera/keep_alive—
État complet (statut + paramètres)GET /gopro/camera/state—
Définir le groupe de préréglagesGET /gopro/camera/presets/set_groupid (1000/1001/1002)
Modifier un paramètreGET /gopro/camera/settingsetting, option
Liste des médiasGET /gopro/media/list—
Télécharger un fichierGET /videos/DCIM/{dir}/{file}(bytes)
Vignette / screennailGET /gopro/media/thumbnail / .../screennailpath
Transfert turboGET /gopro/media/turbo_transferp (1/0)
Supprimer le groupe .360 (hérité)GET /gp/gpControl/command/storage/delete/groupp

Un joli détail : la famille moderne /gopro/... et la famille héritée /gp/gpControl/... coexistent. La suppression des fichiers de groupe chaptered.360/GS, que la nouvelle API ne peut pas gérer proprement, est la seule situation où nous recourons à l'endpoint ancien.

Le modèle d'auto-réparation 403 (la meilleure astuce ici)

Vous ne pouvez pas savoir à l'avance quels paramètres sont valides dans le mode actuel de la caméra — une résolution légale en 16:9 est illégale en 9:16, les options de FPS dépendent de la résolution, et ainsi de suite. GoPro vous le dit à la dure : elle renvoie HTTP 403 avec la liste des options qui auraient été valides, et HTTP 500 quand elle est juste momentanément occupée.

Nous parsons donc le corps du 403 en une erreur typée et retentons le 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;
        }
      }
    }
  }
}

Les options signalées par la caméra sont utilisées par la couche de service pour réécrire sa propre liste de capacités si elle détecte une SettingRejectedError. Pour chaque modèle, nous commençons avec une table de capacités optimiste et codée en dur et permettons à la caméra de nous corriger tout au long de l'exécution. C'est plus pratique et auto-correcteur que d'essayer de simuler l'intégralité de la matrice de paramètres de GoPro à l'avance.

3. Keep-alive : le minuteur Ă  ne pas oublier

Après environ 60 secondes d'inactivité, le GoPro WiFi AP met fin à votre session. Votre connexion parfaitement fonctionnelle meurt discrètement au milieu d'une session si vous ne la maintenez pas. Ainsi, nous démarrons deux minuteurs à la connexion :

  • Keep-alive toutes les 30 s → GET /gopro/camera/keep_alive
  • Status poll toutes les 5 s → GET /gopro/camera/state

void _startKeepAlive() {
  _keepAliveTimer?.cancel();
  _keepAliveTimer = Timer.periodic(
    GoProHttpConstants.keepAliveInterval,
    (_) => _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
  }
}

Un keep-alive échoué est aussi notre signal de déconnexion — c'est le moyen le plus fiable de remarquer que l'utilisateur est sorti de la portée WiFi.

4. Contrôle de la caméra : obturateur, mode, et la danse de l'objectif MAX 2

L'obturateur est la partie facile — démarrer, attendre 500 ms, réinterroger l'état. La capture de photo est le même shutter/start; que vous obteniez une vidéo ou une photo dépend du mode actuel.

Le changement de mode devient intéressant, car un "mode" sur une GoPro MAX 2 est en réalité deux paramètres : un groupe de préréglages (Vidéo = 1000, Photo = 1001, Timelapse = 1002) plus un objectif (Setting 194 : 0 = objectif unique/HERO, 1 = 360). Réussir cela a nécessité plusieurs règles non évidentes intégrées au code :

  1. Sérialiser les changements de mode. Les pressions rapides sur les sous-modes sont mises en file d'attente via une chaîne Completer afin qu'elles ne puissent pas entrer en concurrence et provoquer un état incohérent.
  2. Arrêter l'obturateur en premier. Le firmware rejette les changements de préréglages avec HTTP 400 pendant l'enregistrement, nous arrêtons donc proactivement l'encodage avant de changer.
  3. L'ordre et les délais sont importants. Sur MAX 2 : définir d'abord le groupe de préréglages (+400 ms), puis l'objectif via Setting 194 (+1500 ms). Cette pause de 1,5 seconde est réelle — c'est la transition physique de l'assemblage de l'objectif.
  4. Faire confiance à l'état voulu, pas à l'état interrogé. Nous suivons _lastIntendedIs360 / _lastIntendedPresetGroup et comparons avec ceux-ci, car le status poll de 5 secondes peut vous donner une capture d'écran périmée en pleine transition.

Il y a aussi une particularité GoPro vraiment sournoise qui mérite d'être signalée : les ID de résolution dépendent du rapport d'aspect. 4K est l'option 1 en 16:9, 109 en 9:16 et 112 en 4:3. Donc, après avoir changé le rapport d'aspect, nous réinterrogeons les capacités — le même ID signifierait autrement une résolution différente.

5. Liste des médias

GET /gopro/media/list renvoie une structure groupée par répertoire avec des clés notoirement concises : d = répertoire, fs = fichiers, n = nom de fichier, s = taille, cre/mod = horodatages Unix en secondes, g = id de groupe, glrv = taille du proxy basse résolution.

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

Le type de média est déduit du préfixe + extension du nom de fichier : GS = vidéo 360, GX = vidéo HERO, GT = timelapse, .360 = vidéo sphérique, .GPR/.RAW = photo RAW. Ces préfixes sont importants plus tard, car les fichiers .360 se comportent différemment de tous les autres.

6. Téléchargements en streaming : turbo, progression et fichiers compagnons

Le transfert d'octets réel est dio.download() directement vers un chemin de fichier, avec un long timeout de réception car un clip .360 de 1 Go prend un certain temps :

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)),
  );
}

Autour de ce cœur, le service de transfert orchestre les détails du monde réel :

Transfert turbo. Avant le téléchargement, nous activons le mode turbo (turbo_transfer?p=1) — GoPro passe au WiFi 5 GHz pour des transferts nettement plus rapides — et nous le désactivons dans un bloc finally afin qu'il soit toujours nettoyé, même en cas d'erreur.

Progression + vitesse, régulée. Le calcul de la vitesse de transfert à chaque callback onReceiveProgress est inutile et saccadé, nous le recalculons donc seulement toutes les 500 ms :

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));
}

L'annulation est un flag coopératif _isCancelled ; en cas d'annulation, nous supprimons le fichier partiel et émettons un événement annulé.

Fichiers compagnons. C'est la partie que vous ne trouverez pas dans la documentation. Pour un fichier .MP4 à objectif unique, nous téléchargeons également le .LRV (un proxy basse résolution que GoPro enregistre en même temps que la vidéo complète) et la vignette .THM, puis nous indexons le LRV pour une lecture rapide dans l'application. Deux pièges de dénomination se présentent ici :

  • HERO8+ renomme le proxy : GX######.MP4 / GH######.MP4 devient GL######.LRV (le deuxième caractère est remplacĂ© par L), avec un fallback de mĂŞme nom pour les camĂ©ras plus anciennes.
  • Les fichiers .360 ne prennent pas du tout en charge les endpoints thumbnail ou screennail. Nous utilisons le compagnon .THM par dĂ©faut, et en dernier recours, nous tĂ©lĂ©chargeons le fichier lui-mĂŞme. Et comme un .360 est un double-fisheye, nous recadrons la vignette sur l'objectif avant — sur un background isolate via compute(), de sorte que l'UI ne se bloque jamais.

L'état du transfert est exposé via un Riverpod notifier qui s'abonne aux streams de fichiers et de transferts, maintient un transfert terminé visible pendant 2 secondes et signale les erreurs pendant 5. À noter : le contrôle et le transfert de fichiers utilisent deux clients Dio indépendants vers la même caméra, de sorte qu'un téléchargement peut s'exécuter pendant que le status poll et le keep-alive continuent.

7. Deux pièges qui ne figurent dans aucun tutoriel

Le HTTP en clair est bloqué par défaut sur les Android modernes

http://10.5.5.9:8080 est en texte clair, et Android 9+ bloque le trafic cleartext par défaut. Vous devez l'autoriser explicitement :

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

plus un fichier network_security_config.xml dont la configuration de base autorise le cleartext (vous pouvez toujours forcer HTTPS pour vos propres domaines cloud/backend). Manquez cela et chaque appel GoPro échouera avec une erreur de connexion confuse.

La liaison réseau manquante — le point fragile de la conception

Voici la partie honnête. Sur Android moderne, lorsque vous vous connectez au GoPro AP, le téléphone maintient généralement la connexion cellulaire comme réseau par défaut car le GoPro AP n'a pas d'accès Internet. Cela signifie qu'une requête HTTP vers 10.5.5.9 peut être routée via la connexion cellulaire et échouer — même si vous êtes "connecté" à la caméra.

La solution robuste est native : appelez ConnectivityManager.bindProcessToNetwork(goProNetwork) avant les transferts et bindProcessToNetwork(null) après. Un chemin GoPro purement Dart n'a pas d'équivalent — et cela a tendance à fonctionner quand même car le système d'exploitation route généralement l'adresse fixe 10.5.5.9 vers l'AP. Mais c'est la partie la plus fragile de l'architecture, et sur certains appareils/versions d'Android, c'est la première chose qui tombe en panne. Si vous déployez cela, ajoutez la liaison réseau — c'est le seul endroit où rester 100% Dart vous coûte en fiabilité.

Leçons apprises

  1. Vous n'avez pas besoin de BLE. Si l'utilisateur peut rejoindre l'AP, l'API HTTP Open GoPro vous donne un contrĂ´le total et des transferts rapides en pur Dart.
  2. Détecter par TCP probe, pas par scan SSID — pas d'autorisation de localisation, pas de code de plateforme.
  3. Keep-alive toutes les 30 s ou la session meurt. Traitez un keep-alive échoué comme votre signal de déconnexion.
  4. Laissez la caméra vous corriger. La boucle d'auto-réparation 403-available_options est plus efficace que de modéliser l'intégralité de la matrice de paramètres de GoPro.
  5. Le changement de mode est une state machine — sérialisez-le, arrêtez l'enregistrement en premier, respectez les délais de transition de l'objectif, et faites confiance à l'état voulu plutôt qu'à l'état interrogé.
  6. Les fichiers compagnons et les particularités du .360 sont là où les difficultés sont cachées — renommage du LRV, pas d'endpoint de vignette, recadrage double-fisheye.
  7. Deux pièges d'infrastructure vous couleront silencieusement : la configuration du trafic cleartext et (surtout) la liaison du processus au réseau de la caméra afin que les requêtes ne s'échappent pas via la connexion cellulaire.

Le résultat : un téléphone qui se connecte à une GoPro via WiFi, pilote toutes les commandes et diffuse des gigabytes de séquences avec une progression en direct — et presque tout cela est juste du Dart bien structuré qui communique avec http://10.5.5.9:8080.
 

FlutterGoProHTTP APIStreaming
Saurav Kumar Gupta's image

Ă€ propos de l'auteur

Saurav Kumar Gupta

AI & Cloud Solutions Expert at MicrocosmWorks

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

Vous souhaitez en savoir plus ?

Contactez-nous pour discuter de la façon dont nous pouvons vous aider à mettre en œuvre ces solutions pour votre entreprise.

Contactez-nous

Questions Fréquemment Posées

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!