# 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_` vs `w_`) - [ ] 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_")`) 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.