MicrocosmWorks๋””์ง€ํ„ธ ์ฝ”์Šค๋ชจ์Šค ํ˜์‹  ๋ฐ ์„ค๊ณ„
์†Œ๊ฐœ์—ฐ๋ฝ์ฒ˜
MicrocosmWorks๋””์ง€ํ„ธ ์ฝ”์Šค๋ชจ์Šค๋ฅผ ํ˜์‹ ํ•˜๊ณ  ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค

์ค‘์š”ํ•œ IT ์†”๋ฃจ์…˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์ˆ , ๋ณด์•ˆ์— ์—ด์ •์ ์ด๋ฉฐ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ํ˜์‹ ์ ์ธ IT ์ธํ”„๋ผ๋ฅผ ํ†ตํ•ด ๋น„์ฆˆ๋‹ˆ์Šค ์„ฑ์žฅ์„ ๋•์Šต๋‹ˆ๋‹ค.

[email protected]
+91 7011868196
New Delhi, India

AI ์„ฑ์žฅ ํ—ˆ๋ธŒ

AI ํ—ˆ๋ธŒ์Šคํƒ€ํŠธ์—… ํ˜์‹ ๊ธฐ์—… ๊ฐ€์†๊ธฐ

์†”๋ฃจ์…˜

๋ชจ๋“  ์†”๋ฃจ์…˜์›ฐ๋‹ˆ์Šค ๋ฐ ํ”ผํŠธ๋‹ˆ์Šค ์•ฑAI ๋น„๋””์˜ค ํ”Œ๋žซํผAI ์—์ด์ „ํŠธ ๊ฐœ๋ฐœ

์ž์›

ํ†ต์ฐฐ๋ ฅ์‚ฐ์—… ๊ฐ€์ด๋“œ์‚ฌ์šฉ ์‚ฌ๋ก€ ์ฒญ์‚ฌ์ง„์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด์‚ฌ๋ก€ ์—ฐ๊ตฌ

ํšŒ์‚ฌ

ํšŒ์‚ฌ ์†Œ๊ฐœ์—ฐ๋ฝ์ฒ˜์šฐ๋ฆฌ์˜ ์ž‘์—…

์„œ๋น„์Šค

๋””์ง€ํ„ธ ์ปจ์„คํŒ…ํด๋ผ์šฐ๋“œ ์ธํ”„๋ผSaaS ๊ฐœ๋ฐœAI ๊ฐœ๋ฐœ๋น„๋””์˜ค ๊ธฐ์ˆ 
ERP ๊ฐœ๋ฐœZoho ๋งž์ถคํ™”Odoo ๊ฐœ๋ฐœSalesforce ํ†ตํ•ฉ๋งž์ถคํ˜• CRM ๊ฐœ๋ฐœ
QuickBooks ํ†ตํ•ฉIoT ์†”๋ฃจ์…˜๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ
์‚ฌ์ด๋ฒ„ ๋ณด์•ˆ ์ปจ์„คํŒ…IT ์ง€์› - L3

ยฉ 2026 MicrocosmWorks. ๋ชจ๋“  ๊ถŒ๋ฆฌ ๋ณด์œ .

๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ์„œ๋น„์Šค ์•ฝ๊ด€
ํ†ต์ฐฐ๋กœ ๋Œ์•„๊ฐ€๊ธฐ
AI Development

Flutter์—์„œ WiFi๋ฅผ ํ†ตํ•ด GoPro ์ œ์–ดํ•˜๊ธฐ: Open GoPro HTTP API, Keep-Alives ๋ฐ ์ŠคํŠธ๋ฆฌ๋ฐ ํŒŒ์ผ ์ „์†ก

Flutter์—์„œ WiFi๋ฅผ ํ†ตํ•ด GoPro์— ์—ฐ๊ฒฐํ•˜๊ธฐ: 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 ํด๋ผ์ด์–ธํŠธ, ๊ทธ๋ฆฌ๊ณ  ๋ฏธ๋ฆฌ ์•Œ์ง€ ๋ชปํ•˜๋ฉด ๋‹น์‹ ์˜ ํ•˜๋ฃจ๋ฅผ ์กฐ์šฉํžˆ ๋ง์น  ๋ช‡ ๊ฐ€์ง€ ์ฃผ์˜์‚ฌํ•ญ๋งŒ ์žˆ์œผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์™œ Bluetooth๊ฐ€ ์•„๋‹Œ WiFi์ธ๊ฐ€์š”?

๋Œ€๋ถ€๋ถ„์˜ "GoPro ์—ฐ๊ฒฐ" ํŠœํ† ๋ฆฌ์–ผ์€ Bluetooth Low Energy๋กœ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค: ์Šค์บ”, ํŽ˜์–ด๋ง, GATT ํŠน์„ฑ ๊ตํ™˜, ๊ทธ๋ฆฌ๊ณ  BLE๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ WiFi๋ฅผ ์ผ  ๋‹ค์Œ, ์–ด์จŒ๋“  WiFi๋ฅผ ํ†ตํ•ด ์ „์†กํ•ฉ๋‹ˆ๋‹ค. ์ž‘๋™์€ ํ•˜์ง€๋งŒ ๋งŽ์€ ์ ˆ์ฐจ๊ฐ€ ํ•„์š”ํ•˜๋ฉฐ, BLE๋Š” ์‚ฌ์šฉ์ž๋“ค์ด ์‹ค์ œ๋กœ ์›ํ•˜๋Š” ๊ฒƒ, ์ฆ‰ ์นด๋ฉ”๋ผ๋ฅผ ์ œ์–ดํ•˜๊ณ  ์˜์ƒ์„ ๋น ๋ฅด๊ฒŒ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ ์žˆ์–ด ๋А๋ฆฌ๊ณ  ๋ฒˆ๊ฑฐ๋กญ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ์ด ์•ฑ์—์„œ๋Š” BLE๋ฅผ ์™„์ „ํžˆ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ํœด๋Œ€ํฐ ์„ค์ •์—์„œ GoPro์˜ WiFi AP์— ์—ฐ๊ฒฐํ•˜๊ณ , ๊ทธ ์‹œ์ ๋ถ€ํ„ฐ ๋ชจ๋“  ๊ฒƒ์ด ์นด๋ฉ”๋ผ์˜ Open GoPro API์— ๋Œ€ํ•œ ์ผ๋ฐ˜ HTTP ์š”์ฒญ์ž…๋‹ˆ๋‹ค (๊ณ ์ •๋œ ์ฃผ์†Œ):

http://10.5.5.9:8080

์นด๋ฉ”๋ผ ์ œ์–ด, ์„ค์ •, ๋ฏธ๋””์–ด ๋ชฉ๋ก ํ™•์ธ, ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ๋Š” ๋ชจ๋‘ ํ•ด๋‹น IP ์ฃผ์†Œ๋กœ ๋ณด๋‚ด๋Š” HTTP GET ์š”์ฒญ์ž…๋‹ˆ๋‹ค. ์ „์ฒด ํ†ตํ•ฉ์€ dio ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ 100% Dart๋กœ ์ด๋ฃจ์–ด์ง€๋ฉฐ, ๋„ค์ดํ‹ฐ๋ธŒ Kotlin/Swift ์ฝ”๋“œ๋Š” ์ „ํ˜€ ์—†์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ํ•ต์‹ฌ์ด๋ฉฐ, ๋งˆ์ง€๋ง‰์— ๋ณด๊ฒ ์ง€๋งŒ ์ด ์„ค๊ณ„๊ฐ€ ์ทจ์•ฝํ•œ ์œ ์ผํ•œ ๋ถ€๋ถ„์ด๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค.

ํ๋ฆ„:

GoPro WiFi AP ์—ฐ๊ฒฐ (ํœด๋Œ€ํฐ ์„ค์ •)
   โ†’ ์นด๋ฉ”๋ผ ์—ฐ๊ฒฐ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ๊ฐ์ง€ (10.5.5.9:8080์œผ๋กœ TCP ํ”„๋กœ๋ธŒ)
   โ†’ ์—ฐ๊ฒฐ + ๋ชจ๋ธ ์‹๋ณ„ (GET /gopro/camera/state)
   โ†’ Keep-alive (30์ดˆ) + ์ƒํƒœ ํด๋ง (5์ดˆ) ํƒ€์ด๋จธ ์‹œ์ž‘
   โ†’ ์ œ์–ด: ์…”ํ„ฐ / ๋ชจ๋“œ / ์„ค์ •
   โ†’ ๋ฏธ๋””์–ด ๋ชฉ๋ก (GET /gopro/media/list)
   โ†’ ํ„ฐ๋ณด + ์ง„ํ–‰ ์ƒํ™ฉ๊ณผ ํ•จ๊ป˜ ๋‹ค์šด๋กœ๋“œ (dio.download)

1. WiFi ์Šค์บ” ๊ถŒํ•œ ์—†์ด ์นด๋ฉ”๋ผ ๊ฐ์ง€ํ•˜๊ธฐ

์ฒซ ๋ฒˆ์งธ ๋ณธ๋Šฅ์€ WiFi SSID๋ฅผ ์—ด๊ฑฐํ•˜๊ณ  GoPro๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒƒ์„ ์ฐพ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค โ€” ๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด ์œ„์น˜ ๊ถŒํ•œ๊ณผ ํ”Œ๋žซํผ๋ณ„ WiFi ์Šค์บ” API๊ฐ€ ํ•„์š”ํ•ด์ง‘๋‹ˆ๋‹ค.

GoPro๋Š” ํ•ญ์ƒ ๋™์ผํ•œ IP์— ์žˆ์œผ๋ฏ€๋กœ, ๊ฐ์ง€๋Š” ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค: 10.5.5.9:8080์œผ๋กœ TCP ์†Œ์ผ“์„ ์—ด ์ˆ˜ ์žˆ๋Š”๊ฐ€? 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'},
  ));
}

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)
์ธ๋„ค์ผ / ์Šคํฌ๋ฆฐ๋„ค์ผGET /gopro/media/thumbnail / .../screennailpath
ํ„ฐ๋ณด ์ „์†กGET /gopro/media/turbo_transferp (1/0)
.360 ๊ทธ๋ฃน ์‚ญ์ œ (๋ ˆ๊ฑฐ์‹œ)GET /gp/gpControl/command/storage/delete/groupp

ํฅ๋ฏธ๋กœ์šด ์ ์€ ํ˜„๋Œ€์ ์ธ /gopro/... ๊ณ„์—ด๊ณผ ๋ ˆ๊ฑฐ์‹œ /gp/gpControl/... ๊ณ„์—ด์ด ๊ณต์กดํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด API๊ฐ€ ๊น”๋”ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์—†๋Š” chaptered.360/GS ๊ทธ๋ฃน ํŒŒ์ผ ์ œ๊ฑฐ๋Š” ์šฐ๋ฆฌ๊ฐ€ ์˜ค๋ž˜๋œ ์—”๋“œํฌ์ธํŠธ์— ์˜์กดํ•˜๋Š” ์œ ์ผํ•œ ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค.

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

์นด๋ฉ”๋ผ๊ฐ€ ๋ณด๊ณ ํ•œ ์˜ต์…˜์€ ์„œ๋น„์Šค ๊ณ„์ธต์—์„œ SettingRejectedError๋ฅผ ๊ฐ์ง€ํ•  ๊ฒฝ์šฐ ์ž์ฒด ๊ธฐ๋Šฅ ๋ชฉ๋ก์„ ๋‹ค์‹œ ์ž‘์„ฑํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ๊ฐ ๋ชจ๋ธ์— ๋Œ€ํ•ด, ์šฐ๋ฆฌ๋Š” ๋‚™๊ด€์ ์ธ ํ•˜๋“œ ์ฝ”๋”ฉ๋œ ๊ธฐ๋Šฅ ํ…Œ์ด๋ธ”๋กœ ์‹œ์ž‘ํ•˜๊ณ  ๋Ÿฐํƒ€์ž„ ๋‚ด๋‚ด ์นด๋ฉ”๋ผ๊ฐ€ ์šฐ๋ฆฌ๋ฅผ ์ˆ˜์ •ํ•˜๋„๋ก ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” GoPro์˜ ์ „์ฒด ์„ค์ • ๋งคํŠธ๋ฆญ์Šค๋ฅผ ๋ฏธ๋ฆฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๋ ค๋Š” ์‹œ๋„๋ณด๋‹ค ๋” ์‹ค์šฉ์ ์ด๊ณ  ์ž์ฒด ์ˆ˜์ •์ ์ž…๋‹ˆ๋‹ค.

3. Keep-alive: ์žŠ์ง€ ๋ง์•„์•ผ ํ•  ํƒ€์ด๋จธ

์•ฝ 60์ดˆ๊ฐ„ ํ™œ๋™์ด ์—†์œผ๋ฉด GoPro WiFi AP๊ฐ€ ์„ธ์…˜์„ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค. ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์œผ๋ฉด ์™„๋ฒฝํ•˜๊ฒŒ ์ž˜ ์ž‘๋™ํ•˜๋˜ ์—ฐ๊ฒฐ์ด ์„ธ์…˜ ์ค‘๊ฐ„์— ์กฐ์šฉํžˆ ๋Š์–ด์ง‘๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์—ฐ๊ฒฐ ์‹œ ๋‘ ๊ฐœ์˜ ํƒ€์ด๋จธ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค:

  • 30์ดˆ๋งˆ๋‹ค Keep-alive โ†’ 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 ๋ Œ์ฆˆ ์ „ํ™˜

์…”ํ„ฐ๋Š” ์‰ฌ์šด ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค โ€” ์‹œ์ž‘ํ•˜๊ณ  500ms ๊ธฐ๋‹ค๋ฆฐ ํ›„ ์ƒํƒœ๋ฅผ ๋‹ค์‹œ ํด๋งํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์ง„ ์บก์ฒ˜๋„ ๋™์ผํ•œ ์…”ํ„ฐ/์‹œ์ž‘ ๋ช…๋ น์ž…๋‹ˆ๋‹ค; ๋น„๋””์˜ค๋ฅผ ์–ป์„์ง€ ์‚ฌ์ง„์„ ์–ป์„์ง€๋Š” ํ˜„์žฌ ๋ชจ๋“œ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค.

๋ชจ๋“œ ์ „ํ™˜์€ ํฅ๋ฏธ๋กœ์šด ๋ถ€๋ถ„์ธ๋ฐ, GoPro MAX 2์˜ "๋ชจ๋“œ"๋Š” ์‚ฌ์‹ค ๋‘ ๊ฐ€์ง€ ์„ค์ •์œผ๋กœ ์ด๋ฃจ์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค: ํ”„๋ฆฌ์…‹ ๊ทธ๋ฃน (Video = 1000, Photo = 1001, Timelapse = 1002)๊ณผ ๋ Œ์ฆˆ (์„ค์ • 194: 0 = ๋‹จ์ผ ๋ Œ์ฆˆ/HERO, 1 = 360)์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ ์ œ๋Œ€๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ์—๋Š” ์ฝ”๋“œ์— ์ˆจ๊ฒจ์ง„ ๋ช‡ ๊ฐ€์ง€ ๋น„์ง๊ด€์ ์ธ ๊ทœ์น™์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค:

  1. ๋ชจ๋“œ ๋ณ€๊ฒฝ์„ ์ง๋ ฌํ™”ํ•ฉ๋‹ˆ๋‹ค. ๋น ๋ฅธ ์„œ๋ธŒ ๋ชจ๋“œ ํƒญ์€ Completer ์ฒด์ธ์„ ํ†ตํ•ด ํ์— ์ถ”๊ฐ€๋˜์–ด ์„œ๋กœ ๊ฒฝ์Ÿํ•˜์—ฌ ์ผ๊ด€์„ฑ ์—†๋Š” ์ƒํƒœ๋กœ ๋“ค์–ด๊ฐ€์ง€ ์•Š๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
  2. ์…”ํ„ฐ๋ฅผ ๋จผ์ € ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. ํŽŒ์›จ์–ด๋Š” ๋…นํ™” ์ค‘ ํ”„๋ฆฌ์…‹ ๋ณ€๊ฒฝ์„ HTTP 400์œผ๋กœ ๊ฑฐ๋ถ€ํ•˜๋ฏ€๋กœ, ์ „ํ™˜ํ•˜๊ธฐ ์ „์— ์ธ์ฝ”๋”ฉ์„ ์‚ฌ์ „์— ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค.
  3. ์ˆœ์„œ์™€ ์ง€์—ฐ ์‹œ๊ฐ„์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. MAX 2์—์„œ๋Š”: ํ”„๋ฆฌ์…‹ ๊ทธ๋ฃน์„ ๋จผ์ € ์„ค์ •ํ•˜๊ณ  (+400 ms), ๊ทธ ๋‹ค์Œ ์„ค์ • 194๋ฅผ ํ†ตํ•ด ๋ Œ์ฆˆ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค (+1500 ms). ์ด 1.5์ดˆ์˜ ์ผ์‹œ ์ •์ง€๋Š” ์‹ค์ œ ๋ฌผ๋ฆฌ์ ์ธ ๋ Œ์ฆˆ ์–ด์…ˆ๋ธ”๋ฆฌ ์ „ํ™˜ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
  4. ํด๋ง๋œ ์ƒํƒœ๊ฐ€ ์•„๋‹Œ ์˜๋„๋œ ์ƒํƒœ๋ฅผ ์‹ ๋ขฐํ•ฉ๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” _lastIntendedIs360 / _lastIntendedPresetGroup์„ ์ถ”์ ํ•˜๊ณ  ์ด๋“ค๊ณผ ๋น„๊ตํ•˜๋Š”๋ฐ, 5์ดˆ ์ƒํƒœ ํด๋ง์ด ์ „ํ™˜ ์ค‘์— ์˜ค๋ž˜๋œ ์Šค๋ƒ…์ƒท์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์–ธ๊ธ‰ํ•  ๋งŒํ•œ ์ •๋ง ๊ต๋ฌ˜ํ•œ GoPro์˜ ํŠน์ดํ•œ ์ ๋„ ์žˆ์Šต๋‹ˆ๋‹ค: ํ•ด์ƒ๋„ ID๋Š” ํ™”๋ฉด ๋น„์œจ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค. 4K๋Š” 16:9์—์„œ ์˜ต์…˜ 1, 9:16์—์„œ 109, 4:3์—์„œ 112์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ™”๋ฉด ๋น„์œจ์„ ๋ณ€๊ฒฝํ•œ ํ›„์—๋Š” ๊ธฐ๋Šฅ์„ ๋‹ค์‹œ ์ฟผ๋ฆฌํ•ฉ๋‹ˆ๋‹ค โ€” ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ๋™์ผํ•œ ID๊ฐ€ ๋‹ค๋ฅธ ํ•ด์ƒ๋„๋ฅผ ์˜๋ฏธํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

5. ๋ฏธ๋””์–ด ๋ชฉ๋ก

GET /gopro/media/list๋Š” ๋งค์šฐ ๊ฐ„๊ฒฐํ•œ ํ‚ค๋กœ ๋””๋ ‰ํ† ๋ฆฌ๋ณ„๋กœ ๊ทธ๋ฃนํ™”๋œ ๊ตฌ์กฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค: d = ๋””๋ ‰ํ† ๋ฆฌ, fs = ํŒŒ์ผ, n = ํŒŒ์ผ๋ช…, s = ํฌ๊ธฐ, cre/mod = ์œ ๋‹‰์Šค ์ดˆ ํƒ€์ž„์Šคํƒฌํ”„, g = ๊ทธ๋ฃน ID, 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 = ํƒ€์ž„๋žฉ์Šค, .360 = ๊ตฌํ˜• ๋น„๋””์˜ค, .GPR/.RAW = ์›๋ณธ ์‚ฌ์ง„. ์ด ์ ‘๋‘์‚ฌ๋“ค์€ ๋‚˜์ค‘์— ์ค‘์š”ํ•ด์ง€๋Š”๋ฐ, .360 ํŒŒ์ผ์€ ๋‹ค๋ฅธ ๋ชจ๋“  ํŒŒ์ผ๊ณผ ๋‹ค๋ฅด๊ฒŒ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

6. ์ŠคํŠธ๋ฆฌ๋ฐ ๋‹ค์šด๋กœ๋“œ: ํ„ฐ๋ณด, ์ง„ํ–‰ ์ƒํ™ฉ, ๊ทธ๋ฆฌ๊ณ  ๋™๋ฐ˜ ํŒŒ์ผ

์‹ค์ œ ๋ฐ”์ดํŠธ ์ „์†ก์€ dio.download()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํŒŒ์ผ ๊ฒฝ๋กœ๋กœ ์ง์ ‘ ์ด๋ฃจ์–ด์ง€๋ฉฐ, 1GB .360 ํด๋ฆฝ์€ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋ฏ€๋กœ ๊ธด ์ˆ˜์‹  ํƒ€์ž„์•„์›ƒ์ด ์„ค์ •๋ฉ๋‹ˆ๋‹ค:

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๋Š” ํ›จ์”ฌ ๋น ๋ฅธ ์ „์†ก์„ ์œ„ํ•ด 5 GHz WiFi๋กœ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค โ€” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ํ•ญ์ƒ ์ •๋ฆฌ๋˜๋„๋ก finally ๋ธ”๋ก์—์„œ ๋•๋‹ˆ๋‹ค.

์ง„ํ–‰ ์ƒํ™ฉ + ์†๋„, ์ œํ•œ. ๋ชจ๋“  onReceiveProgress ์ฝœ๋ฐฑ์—์„œ ์ „์†ก ์†๋„๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๊ฒƒ์€ ๋‚ญ๋น„์ ์ด๊ณ  ๋ถˆ์•ˆ์ •ํ•˜๋ฏ€๋กœ, ์šฐ๋ฆฌ๋Š” 500ms๋งˆ๋‹ค ํ•œ ๋ฒˆ์”ฉ๋งŒ ๋‹ค์‹œ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค:

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 ํŒŒ์ผ์€ ์ธ๋„ค์ผ ๋˜๋Š” ์Šคํฌ๋ฆฐ๋„ค์ผ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ „ํ˜€ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” .THM ๋™๋ฐ˜ ํŒŒ์ผ๋กœ ํด๋ฐฑํ•˜๊ณ , ๋งˆ์ง€๋ง‰ ์ˆ˜๋‹จ์œผ๋กœ ํŒŒ์ผ ์ž์ฒด๋ฅผ ๋‹ค์šด๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  .360์€ ๋“€์–ผ ์–ด์•ˆ ๋ Œ์ฆˆ์ด๊ธฐ ๋•Œ๋ฌธ์—, ์ธ๋„ค์ผ์„ ์ „๋ฉด ๋ Œ์ฆˆ์— ๋งž์ถฐ ์ž๋ฆ…๋‹ˆ๋‹ค โ€” compute()๋ฅผ ํ†ตํ•ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๊ฒฉ๋ฆฌ ํ™˜๊ฒฝ์—์„œ ์ฒ˜๋ฆฌํ•˜์—ฌ UI๊ฐ€ ๋Š๊ธฐ์ง€ ์•Š๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

์ „์†ก ์ƒํƒœ๋Š” ํŒŒ์ผ ๋ฐ ์ „์†ก ์ŠคํŠธ๋ฆผ์„ ๊ตฌ๋…ํ•˜๊ณ , ์™„๋ฃŒ๋œ ์ „์†ก์„ 2์ดˆ ๋™์•ˆ ํ‘œ์‹œํ•˜๋ฉฐ, 5์ดˆ ๋™์•ˆ ์˜ค๋ฅ˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” Riverpod notifier๋ฅผ ํ†ตํ•ด ๋…ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ์ฃผ๋ชฉํ•  ์ ์€ ์ œ์–ด ๋ฐ ํŒŒ์ผ ์ „์†ก์ด ๋™์ผํ•œ ์นด๋ฉ”๋ผ์— ๋Œ€ํ•ด ๋‘ ๊ฐœ์˜ ๋…๋ฆฝ์ ์ธ Dio ํด๋ผ์ด์–ธํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, ์ƒํƒœ ํด๋ง๊ณผ Keep-alive๊ฐ€ ๊ณ„์†๋˜๋Š” ๋™์•ˆ์—๋„ ๋‹ค์šด๋กœ๋“œ๊ฐ€ ์‹คํ–‰๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

7. ์–ด๋–ค ํŠœํ† ๋ฆฌ์–ผ์—๋„ ์—†๋Š” ๋‘ ๊ฐ€์ง€ ์ฃผ์˜์‚ฌํ•ญ

์ตœ์‹  Android์—์„œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ Cleartext HTTP๊ฐ€ ์ฐจ๋‹จ๋ฉ๋‹ˆ๋‹ค

http://10.5.5.9:8080์€ ์ผ๋ฐ˜ ํ…์ŠคํŠธ์ด๋ฉฐ, Android 9 ์ด์ƒ์—์„œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ Cleartext ํŠธ๋ž˜ํ”ฝ์„ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค. ๋ช…์‹œ์ ์œผ๋กœ ํ—ˆ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

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

๊ธฐ๋ณธ ์„ค์ •์ด Cleartext๋ฅผ ํ—ˆ์šฉํ•˜๋Š” network_security_config.xml๋„ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค (์ž์ฒด ํด๋ผ์šฐ๋“œ/๋ฐฑ์—”๋“œ ๋„๋ฉ”์ธ์—๋Š” ์—ฌ์ „ํžˆ HTTPS๋ฅผ ๊ฐ•์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค). ์ด๋ฅผ ๋†“์น˜๋ฉด ๋ชจ๋“  GoPro ํ˜ธ์ถœ์ด ํ˜ผ๋ž€์Šค๋Ÿฌ์šด ์—ฐ๊ฒฐ ์˜ค๋ฅ˜๋กœ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.

๋ˆ„๋ฝ๋œ ๋„คํŠธ์›Œํฌ ๋ฐ”์ธ๋”ฉ โ€” ์„ค๊ณ„์˜ ์ทจ์•ฝ์ 

์†”์งํžˆ ๋ง์”€๋“œ๋ฆฌ์ž๋ฉด, ์ตœ์‹  Android์—์„œ GoPro AP์— ์—ฐ๊ฒฐํ•˜๋ฉด ํœด๋Œ€ํฐ์€ ์ผ๋ฐ˜์ ์œผ๋กœ GoPro AP์— ์ธํ„ฐ๋„ท์ด ์—†๊ธฐ ๋•Œ๋ฌธ์— ์…€๋ฃฐ๋Ÿฌ๋ฅผ ๊ธฐ๋ณธ ๋„คํŠธ์›Œํฌ๋กœ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์นด๋ฉ”๋ผ์— "์—ฐ๊ฒฐ"๋˜์–ด ์žˆ์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  10.5.5.9๋กœ์˜ HTTP ์š”์ฒญ์ด ์…€๋ฃฐ๋Ÿฌ๋ฅผ ํ†ตํ•ด ๋ผ์šฐํŒ…๋˜์–ด ์‹คํŒจํ•  ์ˆ˜ ์žˆ์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

๊ฒฌ๊ณ ํ•œ ํ•ด๊ฒฐ์ฑ…์€ ๋„ค์ดํ‹ฐ๋ธŒ์ž…๋‹ˆ๋‹ค: ์ „์†ก ์ „์— ConnectivityManager.bindProcessToNetwork(goProNetwork)๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , ์ „์†ก ํ›„์— bindProcessToNetwork(null)์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ์ˆœ์ˆ˜ Dart GoPro ๊ฒฝ๋กœ๋Š” ๋™๋“ฑํ•œ ๊ฒƒ์ด ์—†์œผ๋ฉฐ โ€” OS๊ฐ€ ์ผ๋ฐ˜์ ์œผ๋กœ ๊ณ ์ •๋œ 10.5.5.9 ์ฃผ์†Œ๋ฅผ AP๋กœ ๋ผ์šฐํŒ…ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์–ด์จŒ๋“  ์ž‘๋™ํ•˜๋Š” ๊ฒฝํ–ฅ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๊ฒƒ์ด ์•„ํ‚คํ…์ฒ˜์˜ ๊ฐ€์žฅ ์ทจ์•ฝํ•œ ๋ถ€๋ถ„์ด๋ฉฐ, ์ผ๋ถ€ ์žฅ์น˜/Android ๋ฒ„์ „์—์„œ๋Š” ๊ฐ€์žฅ ๋จผ์ € ๊ณ ์žฅ๋‚˜๋Š” ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์„ ์ถœ์‹œํ•˜๋ ค๋ฉด ๋„คํŠธ์›Œํฌ ๋ฐ”์ธ๋”ฉ์„ ์ถ”๊ฐ€ํ•˜์‹ญ์‹œ์˜ค โ€” 100% Dart๋ฅผ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์•ˆ์ •์„ฑ์„ ํฌ์ƒ์‹œํ‚ค๋Š” ์œ ์ผํ•œ ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค.

๋ฐฐ์šด ์ 

  1. BLE๋Š” ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ AP์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด, Open GoPro HTTP API๋Š” ์ˆœ์ˆ˜ Dart๋กœ ์™„์ „ํ•œ ์ œ์–ด์™€ ๋น ๋ฅธ ์ „์†ก์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  2. SSID ์Šค์บ”์ด ์•„๋‹Œ TCP ํ”„๋กœ๋ธŒ๋กœ ๊ฐ์ง€ํ•˜์„ธ์š” โ€” ์œ„์น˜ ๊ถŒํ•œ, ํ”Œ๋žซํผ ์ฝ”๋“œ๊ฐ€ ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค.
  3. 30์ดˆ๋งˆ๋‹ค Keep-alive๋ฅผ ๋ณด๋‚ด์ง€ ์•Š์œผ๋ฉด ์„ธ์…˜์ด ์ข…๋ฃŒ๋ฉ๋‹ˆ๋‹ค. Keep-alive ์‹คํŒจ๋ฅผ ์—ฐ๊ฒฐ ํ•ด์ œ ์‹ ํ˜ธ๋กœ ๊ฐ„์ฃผํ•˜์„ธ์š”.
  4. ์นด๋ฉ”๋ผ๊ฐ€ ๋‹น์‹ ์„ ์ˆ˜์ •ํ•˜๊ฒŒ ํ•˜์‹ญ์‹œ์˜ค. 403-available_options ์ž์ฒด ๋ณต๊ตฌ ๋ฃจํ”„๊ฐ€ GoPro์˜ ์ „์ฒด ์„ค์ • ๋งคํŠธ๋ฆญ์Šค๋ฅผ ๋ชจ๋ธ๋งํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค.
  5. ๋ชจ๋“œ ์ „ํ™˜์€ ์ƒํƒœ ๋จธ์‹ ์ž…๋‹ˆ๋‹ค โ€” ์ง๋ ฌํ™”ํ•˜๊ณ , ๋จผ์ € ๋…นํ™”๋ฅผ ์ค‘์ง€ํ•˜๊ณ , ๋ Œ์ฆˆ ์ „ํ™˜ ์ง€์—ฐ์„ ์ค€์ˆ˜ํ•˜๋ฉฐ, ํด๋ง๋œ ์ƒํƒœ๋ณด๋‹ค ์˜๋„๋œ ์ƒํƒœ๋ฅผ ์‹ ๋ขฐํ•˜์‹ญ์‹œ์˜ค.
  6. ๋™๋ฐ˜ ํŒŒ์ผ๊ณผ .360 ํŠน์ด์‚ฌํ•ญ์€ ์ˆจ๊ฒจ์ง„ ๋ฌธ์ œ์ ๋“ค์ž…๋‹ˆ๋‹ค โ€” LRV ์ด๋ฆ„ ๋ณ€๊ฒฝ, ์ธ๋„ค์ผ ์—”๋“œํฌ์ธํŠธ ์—†์Œ, ๋“€์–ผ ์–ด์•ˆ ๋ Œ์ฆˆ ํฌ๋กญ.
  7. ๋‘ ๊ฐ€์ง€ ์ธํ”„๋ผ ์ฃผ์˜์‚ฌํ•ญ์ด ์กฐ์šฉํžˆ ๋‹น์‹ ์„ ์ขŒ์ดˆ์‹œํ‚ฌ ๊ฒƒ์ž…๋‹ˆ๋‹ค: Cleartext ํŠธ๋ž˜ํ”ฝ ์„ค์ •, ๊ทธ๋ฆฌ๊ณ  (ํŠนํžˆ) ์š”์ฒญ์ด ์…€๋ฃฐ๋Ÿฌ๋ฅผ ํ†ตํ•ด ๋‚˜๊ฐ€์ง€ ์•Š๋„๋ก ํ”„๋กœ์„ธ์Šค๋ฅผ ์นด๋ฉ”๋ผ ๋„คํŠธ์›Œํฌ์— ๋ฐ”์ธ๋”ฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ทธ ๊ฒฐ๊ณผ: WiFi๋ฅผ ํ†ตํ•ด GoPro์— ์—ฐ๊ฒฐํ•˜๊ณ , ๋ชจ๋“  ์ œ์–ด๋ฅผ ์ˆ˜ํ–‰ํ•˜๋ฉฐ, ์‹ค์‹œ๊ฐ„ ์ง„ํ–‰ ์ƒํ™ฉ๊ณผ ํ•จ๊ป˜ ๊ธฐ๊ฐ€๋ฐ”์ดํŠธ์˜ ์˜์ƒ์„ ์ŠคํŠธ๋ฆฌ๋ฐ์œผ๋กœ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ํœด๋Œ€ํฐ ์•ฑ โ€” ๊ทธ๋ฆฌ๊ณ  ์ด ๋ชจ๋“  ๊ฒƒ์ด http://10.5.5.9:8080๊ณผ ํ†ต์‹ ํ•˜๋Š” ์ž˜ ๊ตฌ์กฐํ™”๋œ Dart ์ฝ”๋“œ์— ๋ถˆ๊ณผํ•ฉ๋‹ˆ๋‹ค.
 

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!