Files
sf-app-platform/apps/mobile_app/docs/videocall-integration.md

283 lines
9.8 KiB
Markdown
Raw Normal View History

# Integración Videollamadas — Juphoon jc_sdk
## Estado general
| Fase | Estado |
|---|---|
| 1. SDK wrapper (`videocall_sdk`) | ✅ Completado |
| 2. Configuración nativa (permisos) | ✅ Completado |
| 3. Configuración por entorno (AppKey) | ✅ Completado |
| 4. Feature videocall (UI + lógica) | ⏳ Pendiente |
| 5. Pruebas APP↔APP | ⏳ Pendiente |
| 6. Integración con backend (señalización) | ⏳ Pendiente |
| 7. Pruebas APP↔Reloj | ⏳ Pendiente |
| 8. Token auth (producción) | ⏳ Pendiente |
| 9. Push/background iOS | ⏳ Pendiente (sin doc del proveedor) |
| 10. Chat | ⏳ Pendiente (sin spec del proveedor) |
---
## Fase 1: SDK wrapper — ✅
Paquete `packages/videocall_sdk/` con wrap 100% de `jc_sdk` v2.16.5.
### Arquitectura (patrón sca_treezor)
- Constructor injection (no singletons estáticos)
- GetIt module (`videocallSdkModule(config)`)
- `VideocallSdkManager` orquestador de inicialización
- `VideocallSdkConfig` abstracto para config por entorno
- Riverpod providers + StreamProviders para UI reactiva
- Callbacks del SDK → Dart Streams
### Servicios (7 total, cobertura 100%)
- `VideocallClient` → JCClient (auth, login, logout, messaging)
- `VideocallCallService` → JCCall (llamadas 1-to-1)
- `VideocallDeviceService` → JCMediaDevice (cámara, mic, speaker)
- `VideocallChannelService` → JCMediaChannel (llamadas grupales)
- `VideocallPushService` → JCPush (push notifications)
- `VideocallNetService` → JCNet (estado de red)
- `VideocallLogService` → JCLog (logging)
### Estructura
```
packages/videocall_sdk/lib/src/
├── config/videocall_sdk_config.dart
├── di/videocall_sdk_module.dart
├── manager/videocall_sdk_manager.dart
├── models/ (call_state, call_direction, videocall_item, etc.)
├── services/ (7 servicios)
└── providers/videocall_providers.dart
```
---
## Fase 2: Permisos nativos — ✅
### Android (`AndroidManifest.xml`)
- [x] INTERNET (ya existía)
- [x] ACCESS_NETWORK_STATE (ya existía)
- [x] ACCESS_WIFI_STATE
- [x] CAMERA (ya existía)
- [x] RECORD_AUDIO
- [x] MODIFY_AUDIO_SETTINGS
- [x] BLUETOOTH
- [x] uses-feature: hardware.camera
- [x] uses-feature: hardware.camera.autofocus
- [x] uses-feature: hardware.bluetooth (optional)
### Android (`proguard-rules.pro`)
- [x] -keep com.juphoon.**
- [x] -keep com.justalk.**
- [x] -keepattributes InnerClasses
### iOS (`Info.plist`)
- [x] NSCameraUsageDescription (actualizado: QR + videollamadas)
- [x] NSMicrophoneUsageDescription
- [x] NSPhotoLibraryUsageDescription
### iOS (`Podfile`)
- [x] PERMISSION_CAMERA=1
- [x] PERMISSION_PHOTOS=1
- [x] PERMISSION_MICROPHONE=1
---
## Fase 3: Config por entorno — ✅
- [x] `juphoonAppKey` en development.json, staging.json, production.json
- [x] `Environment.juphoonAppKey` via `String.fromEnvironment()`
- [x] `SaveFamilyVideocallConfig` implementa `VideocallSdkConfig`
- [x] `videocallSdkModule(config)` llamado en `init_app.dart`
- [ ] AppKeys separadas por entorno (por ahora las 3 usan la misma clave de dev)
---
## Fase 4: Feature videocall — ⏳ SIGUIENTE
Feature en `modules/legacy/modules/device_management/lib/src/features/videocall/`
### Por hacer
- [ ] `videocall_builder.dart` — GoRouter builder
- [ ] `domain/entities/videocall_entity.dart` — Freezed entity
- [ ] `domain/entities/videocall_error.dart` — Error enum con i18n
- [ ] `presentation/state/videocall_view_model.dart` — Notifier
- [ ] `presentation/state/videocall_view_state.dart` — Freezed state
- [ ] `presentation/videocall_screen.dart` — Pantalla de llamada
- [ ] `presentation/widgets/local_video_view.dart` — Video local
- [ ] `presentation/widgets/remote_video_view.dart` — Video remoto
- [ ] `presentation/widgets/call_controls.dart` — Botones (colgar, mute, cámara)
- [ ] `presentation/widgets/incoming_call_dialog.dart` — Dialog llamada entrante
- [ ] Providers en `core/providers/`
- [ ] Ruta en GoRouter (`app_router.dart`)
- [ ] Runtime permissions (pedir cámara/mic en runtime)
---
## Fase 5: Pruebas APP↔APP — ⏳
- [ ] Login con 2 userIDs de prueba (`p_test1`, `p_test2`)
- [ ] Llamada de voz APP→APP
- [ ] Videollamada APP→APP
- [ ] Responder llamada entrante
- [ ] Rechazar llamada
- [ ] Colgar durante llamada
- [ ] Mute/unmute micrófono
- [ ] Cambiar cámara frontal/trasera
- [ ] Speaker on/off
- [ ] Llamada perdida (onMissedCallItem)
- [ ] Verificar desfase versión SDK (quickstart 1.0.2 vs pub.dev 2.16.5)
---
## Fase 6: Integración backend — ⏳
- [ ] Obtener documentación API REST del backend SaveFamily para señalización
- [ ] Endpoint para iniciar llamada → notificar al reloj
- [ ] Endpoint para recibir notificación de llamada entrante del reloj
- [ ] Formato userID definido con backend (`p_<cuenta>` vs `w_<IMEI>`)
- [ ] Sanitización de emails (@ → _ en userIDs)
---
## Fase 7: Pruebas APP↔Reloj — ⏳
- [ ] Llamada APP → Reloj
- [ ] Llamada Reloj → APP
- [ ] Videollamada grupal (JCMediaChannel)
- [ ] Límite 5 min de llamada
- [ ] Registro IMEI (protocolo RYIMEI) — lo hace el backend
---
## Fase 8: Token auth — ⏳
- [ ] Backend implementa generación de tokens Juphoon (usa AppSecret)
- [ ] App pide token al backend antes del login
- [ ] Token se pasa como `password` en `VideocallClient.login()`
- [ ] Activar Token鉴权 en consola Juphoon (ya está activo)
Nota: para dev/testing no es necesario — `autoCreateAccount = true` en LoginParam permite login con cualquier password.
---
## Fase 9: Push/Background iOS — ⏳ RIESGO
**Problema:** No hay documentación de cómo recibir llamadas con la app cerrada en iOS.
- [ ] Probar qué pasa cuando la app está cerrada y llega una llamada (fase 5)
- [ ] Si no funciona: investigar PushKit + CallKit
- [ ] Verificar si `JCPush` del SDK resuelve esto
- [ ] Consultar pestaña "消息通知服务" en la consola Juphoon
- [ ] Si es deal-breaker: escalar antes del pago ($8,835)
---
## Fase 10: Chat — ⏳ SIN SPEC
- [ ] Obtener especificación del módulo de chat del proveedor
- [ ] Determinar si usa JCMediaChannel (SDK) o API propia
- [ ] $2,950 pagados sin lista de features
---
## Credenciales Juphoon Cloud
| Campo | Valor | Quién lo usa |
|---|---|---|
| AppKey | `9efcf2d889dc8a0320925096` | App Flutter + Backend |
| AppSecret | `ui7pr73ggl5rr0gf01np` | Solo Backend |
| AES_KEY (IoT) | `8e3637pG7E9144E0` | Solo Backend |
| Consola | juphoon.com (+34 603675786) | Julián |
---
## Naming conventions (protocolo TCP)
| Tipo | Formato | Ejemplo |
|---|---|---|
| Watch userID | `w_` + IMEI | `w_000078932675810` |
| Mobile userID | `p_` + APP account | `p_abc10086` |
| Group room | `did` + `_group` | `0245423235_group` |
| Single room | `did` + `_` + APP account | `0245423235_abc10086` |
Nota: `@` y `.` se reemplazan por `_` en room numbers y userIDs.
---
## Documentación de referencia
- Quickstart V1.1: `~/Downloads/Video call API_ Juphoon Flutter SDK quickstart V1.1.pdf`
- TCP Protocol: `~/Downloads/Juphoon Video Call TCP Protocol.docx`
- Connection process: `~/Downloads/video call connection process Rev2.docx`
- Mutual dialing: `~/Downloads/video call mutual dialing process.docx`
- Schematics: `~/Downloads/schematics _2025.03.26 (2)/`
- pub.dev: https://pub.dev/packages/jc_sdk
- Consola: https://developer.juphoon.com
---
## Flujos de llamada (protocolo TCP + Juphoon SDK)
### APP → Reloj (outgoing)
1. App envía `VIDEO_CALL_REQUEST` al backend con `chatType`, `appAccount`, `roomNumber`, `sessionId`
2. Backend reenvía la notificación al reloj via TCP
3. App inicia audio/cámara y llama al watch account via SDK (`startCall(userId: "w_<IMEI>")`)
4. Reloj contesta → SDK notifica via `callItemUpdateStream` (estado `isTalking`)
5. App envía `VIDEO_CALL_ROOM_COUNT_REQUEST` con `type` (0/1), `count: 2`, `room_num`
### Reloj → APP (incoming)
1. Reloj envía notificación de llamada al backend
2. Backend notifica a la app (requiere app abierta con SDK inicializado, ver Fase 9)
3. SDK detecta llamada entrante via `callItemAddStream` con `CallDirection.incoming`
4. Usuario acepta → `answerCall()` → SDK conecta
5. Reloj reporta participantes al backend
### Colgar / Rechazar
- **Colgar (en llamada):** `hangUp()` en SDK + `VIDEO_CALL_CANCEL` al backend
- **Rechazar (incoming):** `hangUp()` en SDK + `VIDEO_CALL_REFUSE` al backend con `appAccount`, `roomNumber`
### Convenciones de nombres
| Campo | Formato | Ejemplo |
|---|---|---|
| Watch userID | `w_` + IMEI | `w_000078932675810` |
| Mobile userID | `p_` + email sanitizado | `p_user_example_com` |
| Room (single) | `deviceId` + `_` + appAccount | `0245423235_p_user_example_com` |
| Room (group) | `deviceId` + `_group` | `0245423235_group` |
| Session ID | `deviceId` + `_` + epoch en segundos | `0245423235_1714150800` |
Sanitización: `@` y `.` se reemplazan por `_` en userIDs y roomNumbers.
### Configuración del SDK por tipo de dispositivo
- RTOS watches: `MediaConfig.MODE_RTOS`
- Android watches: `MediaConfig.MODE_INTELLIGENT_HARDWARE`
- Se determina con `device.capabilities.system` (`isRtos` / `isAndroid`)
### Auto-login
- userId: `p_` + email sanitizado (ej: `p_julian_test_com`)
- password: `user.id` (UUID del usuario padre)
- En dev/testing `autoCreateAccount = true` permite login con cualquier password
---
## Limitaciones actuales
### Recepción de llamadas requiere app abierta
La app debe estar en primer plano con el SDK inicializado y el client logueado para recibir llamadas entrantes. Si el app está en background o cerrada, las llamadas no llegan. Esto se resuelve en Fase 9 (Push/Background).
### Sin timeout de llamada
El protocolo menciona un límite de 5 min por llamada, pero no está implementado en la app. El reloj podría manejar el corte por su lado.
---
## Pendientes por verificar
- **chatType**: El protocolo TCP usa `0` (single) y `1` (multi) como enteros. Nuestra app envía `"single"`/`"multi"` como strings en el JSON del comando. Verificar que el backend hace la conversión correctamente antes de enviar al reloj via TCP.