diff --git a/apps/mobile_app/docs/videocall-integration.md b/apps/mobile_app/docs/videocall-integration.md index cf2e1091..434ad5af 100644 --- a/apps/mobile_app/docs/videocall-integration.md +++ b/apps/mobile_app/docs/videocall-integration.md @@ -213,3 +213,70 @@ Nota: `@` y `.` se reemplazan por `_` en room numbers y userIDs. - 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. diff --git a/modules/legacy/modules/device_management/lib/src/features/device_management/device_management_screen.dart b/modules/legacy/modules/device_management/lib/src/features/device_management/device_management_screen.dart index fb2927ee..8e0c9cde 100644 --- a/modules/legacy/modules/device_management/lib/src/features/device_management/device_management_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/device_management/device_management_screen.dart @@ -128,6 +128,15 @@ class DeviceManagementScreen extends ConsumerWidget { text: context.translate(I18n.callWatch), ), gap, + AppMenuButton( + color: primaryColor, + onPressed: () => + navigationContract.pushTo(AppRoutes.videocall), + icon: Icons.videocam_outlined, + iconSize: SizeUtils.getByScreen(small: 42, big: 40), + text: context.translate(I18n.videocallTitle), + ), + gap, AppMenuButton( color: primaryColor, onPressed: () => diff --git a/modules/legacy/modules/device_management/lib/src/features/videocall/data/datasources/videocall_signaling_datasource.dart b/modules/legacy/modules/device_management/lib/src/features/videocall/data/datasources/videocall_signaling_datasource.dart index 5f3400ef..16857286 100644 --- a/modules/legacy/modules/device_management/lib/src/features/videocall/data/datasources/videocall_signaling_datasource.dart +++ b/modules/legacy/modules/device_management/lib/src/features/videocall/data/datasources/videocall_signaling_datasource.dart @@ -1,22 +1,30 @@ +import '../../domain/entities/videocall_chat_type.dart'; + abstract class VideocallSignalingDatasource { Future initiateCall({ - required String deviceId, + required String deviceIdentificator, + required VideocallChatType chatType, + required String appAccount, + required String roomNumber, + required String sessionId, + }); + + Future cancelCall({ + required String deviceIdentificator, + required VideocallChatType chatType, + }); + + Future refuseCall({ + required String deviceIdentificator, + required VideocallChatType chatType, required String appAccount, required String roomNumber, }); - Future cancelCall({required String deviceId}); - - Future refuseCall({ - required String deviceId, - required String roomNumber, - }); - - Future getRoomParticipantCount({required String roomNumber}); - - Future reportParticipantCount({ - required String roomNumber, + Future reportRoomCount({ + required String deviceIdentificator, + required VideocallChatType chatType, required int count, - required int type, + required String roomNumber, }); } diff --git a/modules/legacy/modules/device_management/lib/src/features/videocall/data/datasources/videocall_signaling_datasource_impl.dart b/modules/legacy/modules/device_management/lib/src/features/videocall/data/datasources/videocall_signaling_datasource_impl.dart index 7b4fe069..bd1293f2 100644 --- a/modules/legacy/modules/device_management/lib/src/features/videocall/data/datasources/videocall_signaling_datasource_impl.dart +++ b/modules/legacy/modules/device_management/lib/src/features/videocall/data/datasources/videocall_signaling_datasource_impl.dart @@ -1,50 +1,86 @@ -import 'package:sf_infrastructure/sf_infrastructure.dart'; +import 'package:legacy_device_state/legacy_device_state.dart'; +import '../../domain/entities/videocall_chat_type.dart'; import 'videocall_signaling_datasource.dart'; class VideocallSignalingDatasourceImpl implements VideocallSignalingDatasource { - VideocallSignalingDatasourceImpl(this._repository); + VideocallSignalingDatasourceImpl(this._commandsRepository); - final SaveFamilyRepository _repository; + final CommandsRepository _commandsRepository; @override Future initiateCall({ - required String deviceId, + required String deviceIdentificator, + required VideocallChatType chatType, required String appAccount, required String roomNumber, + required String sessionId, }) async { - throw UnimplementedError( - 'Backend signaling API not yet available. Waiting for endpoint spec.'); + await _commandsRepository.send( + request: SendCommandRequestModel( + device: deviceIdentificator, + command: DeviceCommand.videoCallRequest, + data: { + 'chatType': chatType.value, + 'appAccount': appAccount, + 'roomNumber': roomNumber, + 'sessionId': sessionId, + }, + ), + ); } @override - Future cancelCall({required String deviceId}) async { - throw UnimplementedError( - 'Backend signaling API not yet available. Waiting for endpoint spec.'); + Future cancelCall({ + required String deviceIdentificator, + required VideocallChatType chatType, + }) async { + await _commandsRepository.send( + request: SendCommandRequestModel( + device: deviceIdentificator, + command: DeviceCommand.videoCallCancel, + data: {'chatType': chatType.value}, + ), + ); } @override Future refuseCall({ - required String deviceId, + required String deviceIdentificator, + required VideocallChatType chatType, + required String appAccount, required String roomNumber, }) async { - throw UnimplementedError( - 'Backend signaling API not yet available. Waiting for endpoint spec.'); + await _commandsRepository.send( + request: SendCommandRequestModel( + device: deviceIdentificator, + command: DeviceCommand.videoCallRefuse, + data: { + 'chatType': chatType.value, + 'appAccount': appAccount, + 'roomNumber': roomNumber, + }, + ), + ); } @override - Future getRoomParticipantCount({required String roomNumber}) async { - throw UnimplementedError( - 'Backend signaling API not yet available. Waiting for endpoint spec.'); - } - - @override - Future reportParticipantCount({ - required String roomNumber, + Future reportRoomCount({ + required String deviceIdentificator, + required VideocallChatType chatType, required int count, - required int type, + required String roomNumber, }) async { - throw UnimplementedError( - 'Backend signaling API not yet available. Waiting for endpoint spec.'); + await _commandsRepository.send( + request: SendCommandRequestModel( + device: deviceIdentificator, + command: DeviceCommand.videoCallRoomCountRequest, + data: { + 'type': chatType == VideocallChatType.single ? 0 : 1, + 'count': count, + 'room_num': roomNumber, + }, + ), + ); } } diff --git a/modules/legacy/modules/device_management/lib/src/features/videocall/data/repositories/videocall_signaling_repository_impl.dart b/modules/legacy/modules/device_management/lib/src/features/videocall/data/repositories/videocall_signaling_repository_impl.dart index e58f0ef9..3cfe51e3 100644 --- a/modules/legacy/modules/device_management/lib/src/features/videocall/data/repositories/videocall_signaling_repository_impl.dart +++ b/modules/legacy/modules/device_management/lib/src/features/videocall/data/repositories/videocall_signaling_repository_impl.dart @@ -1,3 +1,4 @@ +import '../../domain/entities/videocall_chat_type.dart'; import '../../domain/repositories/videocall_signaling_repository.dart'; import '../datasources/videocall_signaling_datasource.dart'; @@ -8,45 +9,59 @@ class VideocallSignalingRepositoryImpl implements VideocallSignalingRepository { @override Future initiateCall({ - required String deviceId, + required String deviceIdentificator, + required VideocallChatType chatType, + required String appAccount, + required String roomNumber, + required String sessionId, + }) { + return _datasource.initiateCall( + deviceIdentificator: deviceIdentificator, + chatType: chatType, + appAccount: appAccount, + roomNumber: roomNumber, + sessionId: sessionId, + ); + } + + @override + Future cancelCall({ + required String deviceIdentificator, + required VideocallChatType chatType, + }) { + return _datasource.cancelCall( + deviceIdentificator: deviceIdentificator, + chatType: chatType, + ); + } + + @override + Future refuseCall({ + required String deviceIdentificator, + required VideocallChatType chatType, required String appAccount, required String roomNumber, }) { - return _datasource.initiateCall( - deviceId: deviceId, + return _datasource.refuseCall( + deviceIdentificator: deviceIdentificator, + chatType: chatType, appAccount: appAccount, roomNumber: roomNumber, ); } @override - Future cancelCall({required String deviceId}) { - return _datasource.cancelCall(deviceId: deviceId); - } - - @override - Future refuseCall({ - required String deviceId, - required String roomNumber, - }) { - return _datasource.refuseCall(deviceId: deviceId, roomNumber: roomNumber); - } - - @override - Future getRoomParticipantCount({required String roomNumber}) { - return _datasource.getRoomParticipantCount(roomNumber: roomNumber); - } - - @override - Future reportParticipantCount({ - required String roomNumber, + Future reportRoomCount({ + required String deviceIdentificator, + required VideocallChatType chatType, required int count, - required int type, + required String roomNumber, }) { - return _datasource.reportParticipantCount( - roomNumber: roomNumber, + return _datasource.reportRoomCount( + deviceIdentificator: deviceIdentificator, + chatType: chatType, count: count, - type: type, + roomNumber: roomNumber, ); } } diff --git a/modules/legacy/modules/device_management/lib/src/features/videocall/domain/entities/videocall_chat_type.dart b/modules/legacy/modules/device_management/lib/src/features/videocall/domain/entities/videocall_chat_type.dart new file mode 100644 index 00000000..6e174d83 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/videocall/domain/entities/videocall_chat_type.dart @@ -0,0 +1,6 @@ +enum VideocallChatType { + single, + multi; + + String get value => name; +} diff --git a/modules/legacy/modules/device_management/lib/src/features/videocall/domain/entities/videocall_error.dart b/modules/legacy/modules/device_management/lib/src/features/videocall/domain/entities/videocall_error.dart index b34b5be4..2e520376 100644 --- a/modules/legacy/modules/device_management/lib/src/features/videocall/domain/entities/videocall_error.dart +++ b/modules/legacy/modules/device_management/lib/src/features/videocall/domain/entities/videocall_error.dart @@ -20,4 +20,5 @@ enum VideocallScreenMode { outgoing, incoming, inCall, + groupCall, } diff --git a/modules/legacy/modules/device_management/lib/src/features/videocall/domain/repositories/videocall_signaling_repository.dart b/modules/legacy/modules/device_management/lib/src/features/videocall/domain/repositories/videocall_signaling_repository.dart index d462c64c..03d3d84e 100644 --- a/modules/legacy/modules/device_management/lib/src/features/videocall/domain/repositories/videocall_signaling_repository.dart +++ b/modules/legacy/modules/device_management/lib/src/features/videocall/domain/repositories/videocall_signaling_repository.dart @@ -1,22 +1,30 @@ +import '../entities/videocall_chat_type.dart'; + abstract class VideocallSignalingRepository { Future initiateCall({ - required String deviceId, + required String deviceIdentificator, + required VideocallChatType chatType, + required String appAccount, + required String roomNumber, + required String sessionId, + }); + + Future cancelCall({ + required String deviceIdentificator, + required VideocallChatType chatType, + }); + + Future refuseCall({ + required String deviceIdentificator, + required VideocallChatType chatType, required String appAccount, required String roomNumber, }); - Future cancelCall({required String deviceId}); - - Future refuseCall({ - required String deviceId, - required String roomNumber, - }); - - Future getRoomParticipantCount({required String roomNumber}); - - Future reportParticipantCount({ - required String roomNumber, + Future reportRoomCount({ + required String deviceIdentificator, + required VideocallChatType chatType, required int count, - required int type, + required String roomNumber, }); } diff --git a/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/group_call_controller.dart b/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/group_call_controller.dart index dd9a5af2..42b9b4f7 100644 --- a/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/group_call_controller.dart +++ b/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/group_call_controller.dart @@ -9,6 +9,12 @@ import 'group_call_state.dart'; part 'group_call_controller.g.dart'; +const _channelCapacity = 6; +const _channelHeartbeatTime = 20; +const _channelHeartbeatTimeout = 60; +const _channelFramerate = 24; +const _channelVideoRatio = 1.78; + @riverpod class GroupCallController extends _$GroupCallController { late VideocallChannelService _channelService; @@ -51,7 +57,16 @@ class GroupCallController extends _$GroupCallController { await _channelService.enableUploadAudioStream(true); await _channelService.enableUploadVideoStream(true); - final ok = await _channelService.join(channelId); + final joinParam = JoinParam(); + joinParam.capacity = _channelCapacity; + joinParam.heartbeatTime = _channelHeartbeatTime; + joinParam.heartbeatTimeout = _channelHeartbeatTimeout; + joinParam.framerate = _channelFramerate; + joinParam.videoRatio = _channelVideoRatio; + joinParam.smooth = true; + joinParam.maxResolution = 0; + + final ok = await _channelService.join(channelId, joinParam: joinParam); if (!ref.mounted) return; if (!ok) { diff --git a/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/group_call_controller.g.dart b/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/group_call_controller.g.dart index 5cd2f170..884b0372 100644 --- a/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/group_call_controller.g.dart +++ b/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/group_call_controller.g.dart @@ -42,7 +42,7 @@ final class GroupCallControllerProvider } String _$groupCallControllerHash() => - r'0d3c5bd234ef3ed76b0b0f7666ddb73c8b98be55'; + r'e0c417dc7b6669fb801b17d8b04ed6dc814b42f3'; abstract class _$GroupCallController extends $Notifier { GroupCallState build(); diff --git a/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/videocall_controller.dart b/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/videocall_controller.dart index 6cf749ee..cf427d29 100644 --- a/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/videocall_controller.dart +++ b/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/videocall_controller.dart @@ -1,26 +1,40 @@ import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sf_shared/sf_shared.dart'; import 'package:videocall_sdk/videocall_sdk.dart' hide VideocallState; +import '../../domain/entities/videocall_chat_type.dart'; import '../../domain/entities/videocall_error.dart'; +import '../../domain/repositories/videocall_signaling_repository.dart'; +import '../../providers/videocall_signaling_repository_provider.dart'; +import 'group_call_controller.dart'; import 'videocall_state.dart'; part 'videocall_controller.g.dart'; +const _cameraWidth = 640; +const _cameraHeight = 360; +const _cameraFps = 30; +const _videoAngle = 0; +const _maxCalls = 1; + @riverpod class VideocallController extends _$VideocallController { late VideocallSdkManager _manager; late VideocallCallService _callService; late VideocallDeviceService _deviceService; late VideocallClient _client; + late VideocallSignalingRepository _signaling; StreamSubscription? _callAddSub; StreamSubscription? _callUpdateSub; StreamSubscription<({int reason, String description})>? _callRemoveSub; StreamSubscription? _missedCallSub; StreamSubscription? _clientStateSub; + bool _wasInCall = false; @override VideocallState build() { @@ -28,44 +42,72 @@ class VideocallController extends _$VideocallController { _callService = ref.read(videocallCallServiceProvider); _deviceService = ref.read(videocallDeviceServiceProvider); _client = ref.read(videocallClientProvider); + _signaling = ref.read(videocallSignalingRepositoryProvider); final device = ref.read(selectedDeviceProvider).value; final deviceId = device?.identificator ?? ''; - ref.onDispose(_disposeSubscriptions); + ref.onDispose(() { + _callAddSub?.cancel(); + _callUpdateSub?.cancel(); + _callRemoveSub?.cancel(); + _missedCallSub?.cancel(); + _clientStateSub?.cancel(); + if (_wasInCall) { + _callService.hangUp(); + _deviceService.stopAudio(); + _deviceService.stopCamera(); + } + }); Future.microtask(_initSdk); return VideocallState(deviceId: deviceId); } Future _initSdk() async { - if (_manager.isInitialized) { - _subscribeToStreams(); - await _configureDevice(); + debugPrint('[Videocall] _initSdk: isInitialized=${_manager.isInitialized}'); + if (!_manager.isInitialized) { + final ok = await _manager.initialize(); + debugPrint('[Videocall] _initSdk: initialize result=$ok'); if (!ref.mounted) return; - state = state.copyWith(isSdkReady: true); - return; - } - - final ok = await _manager.initialize(); - if (!ref.mounted) return; - - if (!ok) { - state = state.copyWith(errorEvent: VideocallErrorEvent.sdkInitialization); - return; + if (!ok) { + state = state.copyWith( + errorEvent: VideocallErrorEvent.sdkInitialization, + ); + return; + } } _subscribeToStreams(); await _configureDevice(); if (!ref.mounted) return; - state = state.copyWith(isSdkReady: true); + await _autoLogin(); + } + + Future _autoLogin() async { + final user = await ref.read(userInfoProvider.future); + final sanitizedEmail = user.email.replaceAll('@', '_').replaceAll('.', '_'); + final userId = 'p_$sanitizedEmail'; + debugPrint('[Videocall] _autoLogin: userId=$userId'); + final ok = await _client.login(userId: userId, password: user.id); + debugPrint( + '[Videocall] _autoLogin: login result=$ok, mounted=${ref.mounted}', + ); + if (!ref.mounted) return; + if (!ok) { + debugPrint('[Videocall] _autoLogin: login FAILED'); + state = state.copyWith(errorEvent: VideocallErrorEvent.authentication); + return; + } + debugPrint('[Videocall] _autoLogin: setting isSdkReady=true'); + state = state.copyWith(localUserId: userId, isSdkReady: true); } Future _configureDevice() async { _deviceService.setDefaultSpeakerOn(true); - _deviceService.setCameraProperty(640, 360, 30); - _deviceService.setVideoAngle(0); - _callService.setMaxCallNum(1); + _deviceService.setCameraProperty(_cameraWidth, _cameraHeight, _cameraFps); + _deviceService.setVideoAngle(_videoAngle); + _callService.setMaxCallNum(_maxCalls); _callService.setTermWhenNetDisconnected(true); final device = ref.read(selectedDeviceProvider).value; @@ -88,29 +130,81 @@ class VideocallController extends _$VideocallController { _clientStateSub = _client.stateStream.listen(_onClientStateChange); } - Future login({required String userId, required String password}) async { - final ok = await _client.login(userId: userId, password: password); - if (!ref.mounted) return false; - if (!ok) { - state = state.copyWith(errorEvent: VideocallErrorEvent.authentication); + String _sanitize(String value) => + value.replaceAll('@', '_').replaceAll('.', '_'); + + String _buildRoomNumber( + String deviceId, + String appAccount, + VideocallChatType chatType, + ) { + final sanitizedAccount = _sanitize(appAccount); + return chatType == VideocallChatType.multi + ? '${deviceId}_group' + : '${deviceId}_$sanitizedAccount'; + } + + String _buildSessionId(String deviceId) { + return '${deviceId}_${DateTime.now().millisecondsSinceEpoch ~/ 1000}'; + } + + Future _requestMediaPermissions() async { + final statuses = await [Permission.camera, Permission.microphone].request(); + final cameraGranted = statuses[Permission.camera]?.isGranted ?? false; + final micGranted = statuses[Permission.microphone]?.isGranted ?? false; + if (!cameraGranted) { + state = state.copyWith(errorEvent: VideocallErrorEvent.cameraPermission); + return false; + } + if (!micGranted) { + state = state.copyWith( + errorEvent: VideocallErrorEvent.microphonePermission, + ); return false; } - state = state.copyWith(localUserId: userId); return true; } Future startCall(String remoteUserId) async { + final permissionsGranted = await _requestMediaPermissions(); + if (!permissionsGranted || !ref.mounted) return; + _wasInCall = true; + final device = ref.read(selectedDeviceProvider).value; + final deviceId = device?.identificator ?? ''; final targetUserId = remoteUserId.isNotEmpty ? remoteUserId : 'w_${device?.imei ?? ''}'; + final chatType = state.chatType; + final roomNumber = _buildRoomNumber(deviceId, state.localUserId, chatType); + final sessionId = _buildSessionId(deviceId); + state = state.copyWith( remoteUserId: targetUserId, screenMode: VideocallScreenMode.outgoing, errorEvent: null, ); + try { + await _signaling.initiateCall( + deviceIdentificator: deviceId, + chatType: chatType, + appAccount: state.localUserId, + roomNumber: roomNumber, + sessionId: sessionId, + ); + } catch (error) { + debugPrint('[Videocall] initiateCall signaling failed: $error'); + if (!ref.mounted) return; + state = state.copyWith( + screenMode: VideocallScreenMode.idle, + errorEvent: VideocallErrorEvent.callStart, + ); + return; + } + if (!ref.mounted) return; + await _deviceService.startAudio(); await _deviceService.startCamera(); @@ -125,6 +219,10 @@ class VideocallController extends _$VideocallController { if (!ok) { await _deviceService.stopAudio(); await _deviceService.stopCamera(); + await _signaling.cancelCall( + deviceIdentificator: deviceId, + chatType: chatType, + ); state = state.copyWith( screenMode: VideocallScreenMode.idle, errorEvent: VideocallErrorEvent.callStart, @@ -132,7 +230,70 @@ class VideocallController extends _$VideocallController { } } + Future startGroupCall() async { + final permissionsGranted = await _requestMediaPermissions(); + if (!permissionsGranted || !ref.mounted) return; + + final device = ref.read(selectedDeviceProvider).value; + final deviceId = device?.identificator ?? ''; + final chatType = VideocallChatType.multi; + final roomNumber = _buildRoomNumber(deviceId, state.localUserId, chatType); + final sessionId = _buildSessionId(deviceId); + + state = state.copyWith( + chatType: chatType, + screenMode: VideocallScreenMode.groupCall, + errorEvent: null, + ); + + try { + await _signaling.initiateCall( + deviceIdentificator: deviceId, + chatType: chatType, + appAccount: state.localUserId, + roomNumber: roomNumber, + sessionId: sessionId, + ); + } catch (error) { + debugPrint('[Videocall] initiateCall group signaling failed: $error'); + if (!ref.mounted) return; + state = state.copyWith( + chatType: VideocallChatType.single, + screenMode: VideocallScreenMode.idle, + errorEvent: VideocallErrorEvent.callStart, + ); + return; + } + if (!ref.mounted) return; + + ref.read(groupCallControllerProvider.notifier).joinChannel(roomNumber); + } + + Future leaveGroupCall() async { + ref.read(groupCallControllerProvider.notifier).leaveChannel(); + final deviceId = state.deviceId; + final chatType = state.chatType; + try { + await _signaling.cancelCall( + deviceIdentificator: deviceId, + chatType: chatType, + ); + } catch (error) { + debugPrint('[Videocall] cancelCall group signaling failed: $error'); + } + if (!ref.mounted) return; + state = state.copyWith( + chatType: VideocallChatType.single, + screenMode: VideocallScreenMode.idle, + successEvent: VideocallSuccessEvent.callEnded, + ); + } + Future answerCall() async { + final permissionsGranted = await _requestMediaPermissions(); + if (!permissionsGranted || !ref.mounted) return; + _wasInCall = true; + await _deviceService.startAudio(); await _deviceService.startCamera(); @@ -155,6 +316,15 @@ class VideocallController extends _$VideocallController { await _stopVideoViews(); await _deviceService.stopAudio(); await _deviceService.stopCamera(); + try { + await _signaling.cancelCall( + deviceIdentificator: state.deviceId, + chatType: state.chatType, + ); + } catch (error) { + debugPrint('[Videocall] cancelCall signaling failed: $error'); + } + if (!ref.mounted) return; state = state.copyWith( screenMode: VideocallScreenMode.idle, currentCall: null, @@ -163,8 +333,22 @@ class VideocallController extends _$VideocallController { } Future rejectCall() async { + final deviceId = state.deviceId; + final chatType = state.chatType; + final roomNumber = _buildRoomNumber(deviceId, state.localUserId, chatType); await _callService.hangUp(); if (!ref.mounted) return; + try { + await _signaling.refuseCall( + deviceIdentificator: deviceId, + chatType: chatType, + appAccount: state.localUserId, + roomNumber: roomNumber, + ); + } catch (error) { + debugPrint('[Videocall] refuseCall signaling failed: $error'); + } + if (!ref.mounted) return; state = state.copyWith( screenMode: VideocallScreenMode.idle, currentCall: null, @@ -231,6 +415,22 @@ class VideocallController extends _$VideocallController { ); } + Future _reportRoomCount() async { + final deviceId = state.deviceId; + final chatType = state.chatType; + final roomNumber = _buildRoomNumber(deviceId, state.localUserId, chatType); + try { + await _signaling.reportRoomCount( + deviceIdentificator: deviceId, + chatType: chatType, + count: 2, + roomNumber: roomNumber, + ); + } catch (error) { + debugPrint('[Videocall] reportRoomCount failed: $error'); + } + } + void _onCallItemAdd(VideocallItem item) { if (!ref.mounted) return; if (item.direction == CallDirection.incoming) { @@ -249,6 +449,9 @@ class VideocallController extends _$VideocallController { void _onCallItemUpdate(VideocallItem item) { if (!ref.mounted) return; + debugPrint( + '[Videocall] _onCallItemUpdate: state=${item.state}, isTalking=${item.isTalking}, uploadSelf=${item.uploadVideoStreamSelf}, uploadOther=${item.uploadVideoStreamOther}', + ); state = state.copyWith(currentCall: item); final isTalking = item.isTalking; @@ -258,6 +461,7 @@ class VideocallController extends _$VideocallController { screenMode: VideocallScreenMode.inCall, successEvent: VideocallSuccessEvent.connected, ); + _reportRoomCount(); } if (item.uploadVideoStreamSelf && state.localCanvas == null) { @@ -309,17 +513,4 @@ class VideocallController extends _$VideocallController { void clearSuccess() { state = state.copyWith(successEvent: null); } - - void _disposeSubscriptions() { - _callAddSub?.cancel(); - _callUpdateSub?.cancel(); - _callRemoveSub?.cancel(); - _missedCallSub?.cancel(); - _clientStateSub?.cancel(); - if (state.screenMode != VideocallScreenMode.idle) { - _callService.hangUp(); - _deviceService.stopAudio(); - _deviceService.stopCamera(); - } - } } diff --git a/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/videocall_controller.g.dart b/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/videocall_controller.g.dart index 0419f618..3988de46 100644 --- a/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/videocall_controller.g.dart +++ b/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/videocall_controller.g.dart @@ -42,7 +42,7 @@ final class VideocallControllerProvider } String _$videocallControllerHash() => - r'910674a27ecef98e5df917d193f97fca1c60ac02'; + r'46fef79b569fea891291d092f187d30f191719ae'; abstract class _$VideocallController extends $Notifier { VideocallState build(); diff --git a/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/videocall_state.dart b/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/videocall_state.dart index ff868354..0b6c206b 100644 --- a/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/videocall_state.dart +++ b/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/videocall_state.dart @@ -1,6 +1,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:videocall_sdk/videocall_sdk.dart'; +import '../../domain/entities/videocall_chat_type.dart'; import '../../domain/entities/videocall_error.dart'; part 'videocall_state.freezed.dart'; @@ -12,6 +13,7 @@ abstract class VideocallState with _$VideocallState { @Default('') String localUserId, @Default('') String remoteUserId, @Default(VideocallScreenMode.idle) VideocallScreenMode screenMode, + @Default(VideocallChatType.single) VideocallChatType chatType, @Default(false) bool isSdkReady, @Default(true) bool isMicEnabled, @Default(true) bool isSpeakerEnabled, diff --git a/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/videocall_state.freezed.dart b/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/videocall_state.freezed.dart index 1e5c0fdb..a76e31cb 100644 --- a/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/videocall_state.freezed.dart +++ b/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/providers/videocall_state.freezed.dart @@ -14,7 +14,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$VideocallState { - String get deviceId; String get localUserId; String get remoteUserId; VideocallScreenMode get screenMode; bool get isSdkReady; bool get isMicEnabled; bool get isSpeakerEnabled; bool get isFrontCamera; bool get isRemoteVideoAvailable; VideocallItem? get currentCall; JCMediaDeviceVideoCanvas? get localCanvas; JCMediaDeviceVideoCanvas? get remoteCanvas; VideocallErrorEvent? get errorEvent; VideocallSuccessEvent? get successEvent; + String get deviceId; String get localUserId; String get remoteUserId; VideocallScreenMode get screenMode; VideocallChatType get chatType; bool get isSdkReady; bool get isMicEnabled; bool get isSpeakerEnabled; bool get isFrontCamera; bool get isRemoteVideoAvailable; VideocallItem? get currentCall; JCMediaDeviceVideoCanvas? get localCanvas; JCMediaDeviceVideoCanvas? get remoteCanvas; VideocallErrorEvent? get errorEvent; VideocallSuccessEvent? get successEvent; /// Create a copy of VideocallState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -25,16 +25,16 @@ $VideocallStateCopyWith get copyWith => _$VideocallStateCopyWith @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is VideocallState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.localUserId, localUserId) || other.localUserId == localUserId)&&(identical(other.remoteUserId, remoteUserId) || other.remoteUserId == remoteUserId)&&(identical(other.screenMode, screenMode) || other.screenMode == screenMode)&&(identical(other.isSdkReady, isSdkReady) || other.isSdkReady == isSdkReady)&&(identical(other.isMicEnabled, isMicEnabled) || other.isMicEnabled == isMicEnabled)&&(identical(other.isSpeakerEnabled, isSpeakerEnabled) || other.isSpeakerEnabled == isSpeakerEnabled)&&(identical(other.isFrontCamera, isFrontCamera) || other.isFrontCamera == isFrontCamera)&&(identical(other.isRemoteVideoAvailable, isRemoteVideoAvailable) || other.isRemoteVideoAvailable == isRemoteVideoAvailable)&&(identical(other.currentCall, currentCall) || other.currentCall == currentCall)&&(identical(other.localCanvas, localCanvas) || other.localCanvas == localCanvas)&&(identical(other.remoteCanvas, remoteCanvas) || other.remoteCanvas == remoteCanvas)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is VideocallState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.localUserId, localUserId) || other.localUserId == localUserId)&&(identical(other.remoteUserId, remoteUserId) || other.remoteUserId == remoteUserId)&&(identical(other.screenMode, screenMode) || other.screenMode == screenMode)&&(identical(other.chatType, chatType) || other.chatType == chatType)&&(identical(other.isSdkReady, isSdkReady) || other.isSdkReady == isSdkReady)&&(identical(other.isMicEnabled, isMicEnabled) || other.isMicEnabled == isMicEnabled)&&(identical(other.isSpeakerEnabled, isSpeakerEnabled) || other.isSpeakerEnabled == isSpeakerEnabled)&&(identical(other.isFrontCamera, isFrontCamera) || other.isFrontCamera == isFrontCamera)&&(identical(other.isRemoteVideoAvailable, isRemoteVideoAvailable) || other.isRemoteVideoAvailable == isRemoteVideoAvailable)&&(identical(other.currentCall, currentCall) || other.currentCall == currentCall)&&(identical(other.localCanvas, localCanvas) || other.localCanvas == localCanvas)&&(identical(other.remoteCanvas, remoteCanvas) || other.remoteCanvas == remoteCanvas)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent)); } @override -int get hashCode => Object.hash(runtimeType,deviceId,localUserId,remoteUserId,screenMode,isSdkReady,isMicEnabled,isSpeakerEnabled,isFrontCamera,isRemoteVideoAvailable,currentCall,localCanvas,remoteCanvas,errorEvent,successEvent); +int get hashCode => Object.hash(runtimeType,deviceId,localUserId,remoteUserId,screenMode,chatType,isSdkReady,isMicEnabled,isSpeakerEnabled,isFrontCamera,isRemoteVideoAvailable,currentCall,localCanvas,remoteCanvas,errorEvent,successEvent); @override String toString() { - return 'VideocallState(deviceId: $deviceId, localUserId: $localUserId, remoteUserId: $remoteUserId, screenMode: $screenMode, isSdkReady: $isSdkReady, isMicEnabled: $isMicEnabled, isSpeakerEnabled: $isSpeakerEnabled, isFrontCamera: $isFrontCamera, isRemoteVideoAvailable: $isRemoteVideoAvailable, currentCall: $currentCall, localCanvas: $localCanvas, remoteCanvas: $remoteCanvas, errorEvent: $errorEvent, successEvent: $successEvent)'; + return 'VideocallState(deviceId: $deviceId, localUserId: $localUserId, remoteUserId: $remoteUserId, screenMode: $screenMode, chatType: $chatType, isSdkReady: $isSdkReady, isMicEnabled: $isMicEnabled, isSpeakerEnabled: $isSpeakerEnabled, isFrontCamera: $isFrontCamera, isRemoteVideoAvailable: $isRemoteVideoAvailable, currentCall: $currentCall, localCanvas: $localCanvas, remoteCanvas: $remoteCanvas, errorEvent: $errorEvent, successEvent: $successEvent)'; } @@ -45,11 +45,11 @@ abstract mixin class $VideocallStateCopyWith<$Res> { factory $VideocallStateCopyWith(VideocallState value, $Res Function(VideocallState) _then) = _$VideocallStateCopyWithImpl; @useResult $Res call({ - String deviceId, String localUserId, String remoteUserId, VideocallScreenMode screenMode, bool isSdkReady, bool isMicEnabled, bool isSpeakerEnabled, bool isFrontCamera, bool isRemoteVideoAvailable, VideocallItem? currentCall, JCMediaDeviceVideoCanvas? localCanvas, JCMediaDeviceVideoCanvas? remoteCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent + String deviceId, String localUserId, String remoteUserId, VideocallScreenMode screenMode, VideocallChatType chatType, bool isSdkReady, bool isMicEnabled, bool isSpeakerEnabled, bool isFrontCamera, bool isRemoteVideoAvailable, VideocallItem? currentCall, JCMediaDeviceVideoCanvas? localCanvas, JCMediaDeviceVideoCanvas? remoteCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent }); - +$VideocallItemCopyWith<$Res>? get currentCall; } /// @nodoc @@ -62,13 +62,14 @@ class _$VideocallStateCopyWithImpl<$Res> /// Create a copy of VideocallState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? deviceId = null,Object? localUserId = null,Object? remoteUserId = null,Object? screenMode = null,Object? isSdkReady = null,Object? isMicEnabled = null,Object? isSpeakerEnabled = null,Object? isFrontCamera = null,Object? isRemoteVideoAvailable = null,Object? currentCall = freezed,Object? localCanvas = freezed,Object? remoteCanvas = freezed,Object? errorEvent = freezed,Object? successEvent = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? deviceId = null,Object? localUserId = null,Object? remoteUserId = null,Object? screenMode = null,Object? chatType = null,Object? isSdkReady = null,Object? isMicEnabled = null,Object? isSpeakerEnabled = null,Object? isFrontCamera = null,Object? isRemoteVideoAvailable = null,Object? currentCall = freezed,Object? localCanvas = freezed,Object? remoteCanvas = freezed,Object? errorEvent = freezed,Object? successEvent = freezed,}) { return _then(_self.copyWith( deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable as String,localUserId: null == localUserId ? _self.localUserId : localUserId // ignore: cast_nullable_to_non_nullable as String,remoteUserId: null == remoteUserId ? _self.remoteUserId : remoteUserId // ignore: cast_nullable_to_non_nullable as String,screenMode: null == screenMode ? _self.screenMode : screenMode // ignore: cast_nullable_to_non_nullable -as VideocallScreenMode,isSdkReady: null == isSdkReady ? _self.isSdkReady : isSdkReady // ignore: cast_nullable_to_non_nullable +as VideocallScreenMode,chatType: null == chatType ? _self.chatType : chatType // ignore: cast_nullable_to_non_nullable +as VideocallChatType,isSdkReady: null == isSdkReady ? _self.isSdkReady : isSdkReady // ignore: cast_nullable_to_non_nullable as bool,isMicEnabled: null == isMicEnabled ? _self.isMicEnabled : isMicEnabled // ignore: cast_nullable_to_non_nullable as bool,isSpeakerEnabled: null == isSpeakerEnabled ? _self.isSpeakerEnabled : isSpeakerEnabled // ignore: cast_nullable_to_non_nullable as bool,isFrontCamera: null == isFrontCamera ? _self.isFrontCamera : isFrontCamera // ignore: cast_nullable_to_non_nullable @@ -81,7 +82,19 @@ as VideocallErrorEvent?,successEvent: freezed == successEvent ? _self.successEve as VideocallSuccessEvent?, )); } +/// Create a copy of VideocallState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$VideocallItemCopyWith<$Res>? get currentCall { + if (_self.currentCall == null) { + return null; + } + return $VideocallItemCopyWith<$Res>(_self.currentCall!, (value) { + return _then(_self.copyWith(currentCall: value)); + }); +} } @@ -163,10 +176,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String deviceId, String localUserId, String remoteUserId, VideocallScreenMode screenMode, bool isSdkReady, bool isMicEnabled, bool isSpeakerEnabled, bool isFrontCamera, bool isRemoteVideoAvailable, VideocallItem? currentCall, JCMediaDeviceVideoCanvas? localCanvas, JCMediaDeviceVideoCanvas? remoteCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String deviceId, String localUserId, String remoteUserId, VideocallScreenMode screenMode, VideocallChatType chatType, bool isSdkReady, bool isMicEnabled, bool isSpeakerEnabled, bool isFrontCamera, bool isRemoteVideoAvailable, VideocallItem? currentCall, JCMediaDeviceVideoCanvas? localCanvas, JCMediaDeviceVideoCanvas? remoteCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _VideocallState() when $default != null: -return $default(_that.deviceId,_that.localUserId,_that.remoteUserId,_that.screenMode,_that.isSdkReady,_that.isMicEnabled,_that.isSpeakerEnabled,_that.isFrontCamera,_that.isRemoteVideoAvailable,_that.currentCall,_that.localCanvas,_that.remoteCanvas,_that.errorEvent,_that.successEvent);case _: +return $default(_that.deviceId,_that.localUserId,_that.remoteUserId,_that.screenMode,_that.chatType,_that.isSdkReady,_that.isMicEnabled,_that.isSpeakerEnabled,_that.isFrontCamera,_that.isRemoteVideoAvailable,_that.currentCall,_that.localCanvas,_that.remoteCanvas,_that.errorEvent,_that.successEvent);case _: return orElse(); } @@ -184,10 +197,10 @@ return $default(_that.deviceId,_that.localUserId,_that.remoteUserId,_that.screen /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String deviceId, String localUserId, String remoteUserId, VideocallScreenMode screenMode, bool isSdkReady, bool isMicEnabled, bool isSpeakerEnabled, bool isFrontCamera, bool isRemoteVideoAvailable, VideocallItem? currentCall, JCMediaDeviceVideoCanvas? localCanvas, JCMediaDeviceVideoCanvas? remoteCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String deviceId, String localUserId, String remoteUserId, VideocallScreenMode screenMode, VideocallChatType chatType, bool isSdkReady, bool isMicEnabled, bool isSpeakerEnabled, bool isFrontCamera, bool isRemoteVideoAvailable, VideocallItem? currentCall, JCMediaDeviceVideoCanvas? localCanvas, JCMediaDeviceVideoCanvas? remoteCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent) $default,) {final _that = this; switch (_that) { case _VideocallState(): -return $default(_that.deviceId,_that.localUserId,_that.remoteUserId,_that.screenMode,_that.isSdkReady,_that.isMicEnabled,_that.isSpeakerEnabled,_that.isFrontCamera,_that.isRemoteVideoAvailable,_that.currentCall,_that.localCanvas,_that.remoteCanvas,_that.errorEvent,_that.successEvent);case _: +return $default(_that.deviceId,_that.localUserId,_that.remoteUserId,_that.screenMode,_that.chatType,_that.isSdkReady,_that.isMicEnabled,_that.isSpeakerEnabled,_that.isFrontCamera,_that.isRemoteVideoAvailable,_that.currentCall,_that.localCanvas,_that.remoteCanvas,_that.errorEvent,_that.successEvent);case _: throw StateError('Unexpected subclass'); } @@ -204,10 +217,10 @@ return $default(_that.deviceId,_that.localUserId,_that.remoteUserId,_that.screen /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String deviceId, String localUserId, String remoteUserId, VideocallScreenMode screenMode, bool isSdkReady, bool isMicEnabled, bool isSpeakerEnabled, bool isFrontCamera, bool isRemoteVideoAvailable, VideocallItem? currentCall, JCMediaDeviceVideoCanvas? localCanvas, JCMediaDeviceVideoCanvas? remoteCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String deviceId, String localUserId, String remoteUserId, VideocallScreenMode screenMode, VideocallChatType chatType, bool isSdkReady, bool isMicEnabled, bool isSpeakerEnabled, bool isFrontCamera, bool isRemoteVideoAvailable, VideocallItem? currentCall, JCMediaDeviceVideoCanvas? localCanvas, JCMediaDeviceVideoCanvas? remoteCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent)? $default,) {final _that = this; switch (_that) { case _VideocallState() when $default != null: -return $default(_that.deviceId,_that.localUserId,_that.remoteUserId,_that.screenMode,_that.isSdkReady,_that.isMicEnabled,_that.isSpeakerEnabled,_that.isFrontCamera,_that.isRemoteVideoAvailable,_that.currentCall,_that.localCanvas,_that.remoteCanvas,_that.errorEvent,_that.successEvent);case _: +return $default(_that.deviceId,_that.localUserId,_that.remoteUserId,_that.screenMode,_that.chatType,_that.isSdkReady,_that.isMicEnabled,_that.isSpeakerEnabled,_that.isFrontCamera,_that.isRemoteVideoAvailable,_that.currentCall,_that.localCanvas,_that.remoteCanvas,_that.errorEvent,_that.successEvent);case _: return null; } @@ -219,13 +232,14 @@ return $default(_that.deviceId,_that.localUserId,_that.remoteUserId,_that.screen class _VideocallState implements VideocallState { - const _VideocallState({this.deviceId = '', this.localUserId = '', this.remoteUserId = '', this.screenMode = VideocallScreenMode.idle, this.isSdkReady = false, this.isMicEnabled = true, this.isSpeakerEnabled = true, this.isFrontCamera = true, this.isRemoteVideoAvailable = false, this.currentCall, this.localCanvas, this.remoteCanvas, this.errorEvent, this.successEvent}); + const _VideocallState({this.deviceId = '', this.localUserId = '', this.remoteUserId = '', this.screenMode = VideocallScreenMode.idle, this.chatType = VideocallChatType.single, this.isSdkReady = false, this.isMicEnabled = true, this.isSpeakerEnabled = true, this.isFrontCamera = true, this.isRemoteVideoAvailable = false, this.currentCall, this.localCanvas, this.remoteCanvas, this.errorEvent, this.successEvent}); @override@JsonKey() final String deviceId; @override@JsonKey() final String localUserId; @override@JsonKey() final String remoteUserId; @override@JsonKey() final VideocallScreenMode screenMode; +@override@JsonKey() final VideocallChatType chatType; @override@JsonKey() final bool isSdkReady; @override@JsonKey() final bool isMicEnabled; @override@JsonKey() final bool isSpeakerEnabled; @@ -247,16 +261,16 @@ _$VideocallStateCopyWith<_VideocallState> get copyWith => __$VideocallStateCopyW @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _VideocallState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.localUserId, localUserId) || other.localUserId == localUserId)&&(identical(other.remoteUserId, remoteUserId) || other.remoteUserId == remoteUserId)&&(identical(other.screenMode, screenMode) || other.screenMode == screenMode)&&(identical(other.isSdkReady, isSdkReady) || other.isSdkReady == isSdkReady)&&(identical(other.isMicEnabled, isMicEnabled) || other.isMicEnabled == isMicEnabled)&&(identical(other.isSpeakerEnabled, isSpeakerEnabled) || other.isSpeakerEnabled == isSpeakerEnabled)&&(identical(other.isFrontCamera, isFrontCamera) || other.isFrontCamera == isFrontCamera)&&(identical(other.isRemoteVideoAvailable, isRemoteVideoAvailable) || other.isRemoteVideoAvailable == isRemoteVideoAvailable)&&(identical(other.currentCall, currentCall) || other.currentCall == currentCall)&&(identical(other.localCanvas, localCanvas) || other.localCanvas == localCanvas)&&(identical(other.remoteCanvas, remoteCanvas) || other.remoteCanvas == remoteCanvas)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _VideocallState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.localUserId, localUserId) || other.localUserId == localUserId)&&(identical(other.remoteUserId, remoteUserId) || other.remoteUserId == remoteUserId)&&(identical(other.screenMode, screenMode) || other.screenMode == screenMode)&&(identical(other.chatType, chatType) || other.chatType == chatType)&&(identical(other.isSdkReady, isSdkReady) || other.isSdkReady == isSdkReady)&&(identical(other.isMicEnabled, isMicEnabled) || other.isMicEnabled == isMicEnabled)&&(identical(other.isSpeakerEnabled, isSpeakerEnabled) || other.isSpeakerEnabled == isSpeakerEnabled)&&(identical(other.isFrontCamera, isFrontCamera) || other.isFrontCamera == isFrontCamera)&&(identical(other.isRemoteVideoAvailable, isRemoteVideoAvailable) || other.isRemoteVideoAvailable == isRemoteVideoAvailable)&&(identical(other.currentCall, currentCall) || other.currentCall == currentCall)&&(identical(other.localCanvas, localCanvas) || other.localCanvas == localCanvas)&&(identical(other.remoteCanvas, remoteCanvas) || other.remoteCanvas == remoteCanvas)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent)); } @override -int get hashCode => Object.hash(runtimeType,deviceId,localUserId,remoteUserId,screenMode,isSdkReady,isMicEnabled,isSpeakerEnabled,isFrontCamera,isRemoteVideoAvailable,currentCall,localCanvas,remoteCanvas,errorEvent,successEvent); +int get hashCode => Object.hash(runtimeType,deviceId,localUserId,remoteUserId,screenMode,chatType,isSdkReady,isMicEnabled,isSpeakerEnabled,isFrontCamera,isRemoteVideoAvailable,currentCall,localCanvas,remoteCanvas,errorEvent,successEvent); @override String toString() { - return 'VideocallState(deviceId: $deviceId, localUserId: $localUserId, remoteUserId: $remoteUserId, screenMode: $screenMode, isSdkReady: $isSdkReady, isMicEnabled: $isMicEnabled, isSpeakerEnabled: $isSpeakerEnabled, isFrontCamera: $isFrontCamera, isRemoteVideoAvailable: $isRemoteVideoAvailable, currentCall: $currentCall, localCanvas: $localCanvas, remoteCanvas: $remoteCanvas, errorEvent: $errorEvent, successEvent: $successEvent)'; + return 'VideocallState(deviceId: $deviceId, localUserId: $localUserId, remoteUserId: $remoteUserId, screenMode: $screenMode, chatType: $chatType, isSdkReady: $isSdkReady, isMicEnabled: $isMicEnabled, isSpeakerEnabled: $isSpeakerEnabled, isFrontCamera: $isFrontCamera, isRemoteVideoAvailable: $isRemoteVideoAvailable, currentCall: $currentCall, localCanvas: $localCanvas, remoteCanvas: $remoteCanvas, errorEvent: $errorEvent, successEvent: $successEvent)'; } @@ -267,11 +281,11 @@ abstract mixin class _$VideocallStateCopyWith<$Res> implements $VideocallStateCo factory _$VideocallStateCopyWith(_VideocallState value, $Res Function(_VideocallState) _then) = __$VideocallStateCopyWithImpl; @override @useResult $Res call({ - String deviceId, String localUserId, String remoteUserId, VideocallScreenMode screenMode, bool isSdkReady, bool isMicEnabled, bool isSpeakerEnabled, bool isFrontCamera, bool isRemoteVideoAvailable, VideocallItem? currentCall, JCMediaDeviceVideoCanvas? localCanvas, JCMediaDeviceVideoCanvas? remoteCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent + String deviceId, String localUserId, String remoteUserId, VideocallScreenMode screenMode, VideocallChatType chatType, bool isSdkReady, bool isMicEnabled, bool isSpeakerEnabled, bool isFrontCamera, bool isRemoteVideoAvailable, VideocallItem? currentCall, JCMediaDeviceVideoCanvas? localCanvas, JCMediaDeviceVideoCanvas? remoteCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent }); - +@override $VideocallItemCopyWith<$Res>? get currentCall; } /// @nodoc @@ -284,13 +298,14 @@ class __$VideocallStateCopyWithImpl<$Res> /// Create a copy of VideocallState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? deviceId = null,Object? localUserId = null,Object? remoteUserId = null,Object? screenMode = null,Object? isSdkReady = null,Object? isMicEnabled = null,Object? isSpeakerEnabled = null,Object? isFrontCamera = null,Object? isRemoteVideoAvailable = null,Object? currentCall = freezed,Object? localCanvas = freezed,Object? remoteCanvas = freezed,Object? errorEvent = freezed,Object? successEvent = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? deviceId = null,Object? localUserId = null,Object? remoteUserId = null,Object? screenMode = null,Object? chatType = null,Object? isSdkReady = null,Object? isMicEnabled = null,Object? isSpeakerEnabled = null,Object? isFrontCamera = null,Object? isRemoteVideoAvailable = null,Object? currentCall = freezed,Object? localCanvas = freezed,Object? remoteCanvas = freezed,Object? errorEvent = freezed,Object? successEvent = freezed,}) { return _then(_VideocallState( deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable as String,localUserId: null == localUserId ? _self.localUserId : localUserId // ignore: cast_nullable_to_non_nullable as String,remoteUserId: null == remoteUserId ? _self.remoteUserId : remoteUserId // ignore: cast_nullable_to_non_nullable as String,screenMode: null == screenMode ? _self.screenMode : screenMode // ignore: cast_nullable_to_non_nullable -as VideocallScreenMode,isSdkReady: null == isSdkReady ? _self.isSdkReady : isSdkReady // ignore: cast_nullable_to_non_nullable +as VideocallScreenMode,chatType: null == chatType ? _self.chatType : chatType // ignore: cast_nullable_to_non_nullable +as VideocallChatType,isSdkReady: null == isSdkReady ? _self.isSdkReady : isSdkReady // ignore: cast_nullable_to_non_nullable as bool,isMicEnabled: null == isMicEnabled ? _self.isMicEnabled : isMicEnabled // ignore: cast_nullable_to_non_nullable as bool,isSpeakerEnabled: null == isSpeakerEnabled ? _self.isSpeakerEnabled : isSpeakerEnabled // ignore: cast_nullable_to_non_nullable as bool,isFrontCamera: null == isFrontCamera ? _self.isFrontCamera : isFrontCamera // ignore: cast_nullable_to_non_nullable @@ -304,7 +319,19 @@ as VideocallSuccessEvent?, )); } +/// Create a copy of VideocallState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$VideocallItemCopyWith<$Res>? get currentCall { + if (_self.currentCall == null) { + return null; + } + return $VideocallItemCopyWith<$Res>(_self.currentCall!, (value) { + return _then(_self.copyWith(currentCall: value)); + }); +} } // dart format on diff --git a/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/videocall_screen.dart b/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/videocall_screen.dart index eac54c3a..7cfd453f 100644 --- a/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/videocall_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/videocall_screen.dart @@ -1,17 +1,20 @@ -import 'package:flutter/foundation.dart'; +import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:legacy_theme/legacy_theme.dart'; +import 'package:legacy_ui/legacy_ui.dart'; import 'package:navigation/navigation.dart'; import 'package:sf_localizations/sf_localizations.dart'; import 'package:sf_shared/sf_shared.dart'; +import 'package:utils/utils.dart'; import '../domain/entities/videocall_error.dart'; +import 'providers/group_call_controller.dart'; import 'providers/videocall_controller.dart'; import 'providers/videocall_state.dart'; import 'widgets/call_controls_widget.dart'; -import 'widgets/call_status_indicator.dart'; import 'widgets/incoming_call_overlay.dart'; +import 'widgets/participant_grid_widget.dart'; import 'widgets/video_view_widget.dart'; class VideocallScreen extends ConsumerStatefulWidget { @@ -24,14 +27,6 @@ class VideocallScreen extends ConsumerStatefulWidget { } class _VideocallScreenState extends ConsumerState { - final _userIdController = TextEditingController(); - - @override - void dispose() { - _userIdController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { final state = ref.watch(videocallControllerProvider); @@ -61,156 +56,264 @@ class _VideocallScreenState extends ConsumerState { (_, next) { if (next == null) return; if (next == VideocallSuccessEvent.callEnded) { - showInfoDialog(context, I18n.videoCall); + showInfoDialog(context, I18n.videocallCallEnded); } vm.clearSuccess(); }, ); - return Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - body: SafeArea( - child: switch (state.screenMode) { - VideocallScreenMode.idle => _IdleView( - state: state, - controller: _userIdController, - vm: vm, - ), - VideocallScreenMode.outgoing => _OutgoingView(state: state, vm: vm), - VideocallScreenMode.incoming => _IncomingView(state: state, vm: vm), - VideocallScreenMode.inCall => _InCallView(state: state, vm: vm), - }, + final device = ref.watch(selectedDeviceProvider).value; + final deviceName = device?.carrierName ?? ''; + + final isInActiveCall = state.screenMode != VideocallScreenMode.idle; + + final body = switch (state.screenMode) { + VideocallScreenMode.idle => !state.isSdkReady + ? const LegacyLoadingIndicator() + : _IdleBody(deviceName: deviceName, vm: vm), + VideocallScreenMode.outgoing || VideocallScreenMode.inCall => + _ActiveCallView(state: state, vm: vm, deviceName: deviceName), + VideocallScreenMode.incoming => _IncomingView(state: state, vm: vm), + VideocallScreenMode.groupCall => _GroupCallView(vm: vm), + }; + + return PopScope( + canPop: !isInActiveCall, + onPopInvokedWithResult: (didPop, _) { + if (!didPop) { + if (state.screenMode == VideocallScreenMode.groupCall) { + vm.leaveGroupCall(); + } else { + vm.hangUp(); + } + } + }, + child: LegacyPageLayout( + title: context.translate(I18n.videocallTitle), + body: body, ), ); } } -class _IdleView extends StatelessWidget { - const _IdleView({ - required this.state, - required this.controller, - required this.vm, - }); +class _IdleBody extends StatelessWidget { + const _IdleBody({required this.deviceName, required this.vm}); - final VideocallState state; - final TextEditingController controller; + final String deviceName; final VideocallController vm; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - - if (!state.isSdkReady) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator(color: colorScheme.onSurface), - const SizedBox(height: 16), - Text( - context.translate(I18n.videocallInitializingSdk), - style: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.7)), - ), - ], - ), - ); - } + final horizontalPadding = SizeUtils.getByScreen(big: 22, small: 21); return Padding( - padding: const EdgeInsets.all(24), + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(Icons.videocam, color: colorScheme.onSurface, size: 64), + const SizedBox(height: 8), + Text( + deviceName, + style: TextStyle( + fontSize: SizeUtils.getByScreen(small: 14, big: 13), + color: colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + const SizedBox(height: 12), + _VideocallOptionCard( + image: Image.asset( + 'assets/shared/images/iso_sf.png', + width: SizeUtils.getByScreen(small: 48, big: 44), + height: SizeUtils.getByScreen(small: 48, big: 44), + ), + text: deviceName, + onTap: () => vm.startCall(''), + ), + const SizedBox(height: 12), + _VideocallOptionCard( + icon: Icon( + Icons.groups_outlined, + size: SizeUtils.getByScreen(small: 36, big: 32), + color: context.sfColors.legacyPrimary, + ), + text: context.translate(I18n.videocallGroupCall), + onTap: () => vm.startGroupCall(), + ), const SizedBox(height: 24), Text( - context.translate(I18n.videocallTitle), + context.translate(I18n.videocallDisclaimer), style: TextStyle( - color: colorScheme.onSurface, - fontSize: 24, - fontWeight: FontWeight.bold, + fontSize: SizeUtils.getByScreen(small: 12, big: 11), + color: colorScheme.onSurface.withValues(alpha: 0.5), ), ), - const SizedBox(height: 32), - TextField( - controller: controller, - style: TextStyle(color: colorScheme.onSurface), - decoration: InputDecoration( - hintText: context.translate(I18n.videocallRecipientUserId), - hintStyle: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.38)), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: colorScheme.onSurface.withValues(alpha: 0.24)), - borderRadius: BorderRadius.circular(12), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: context.sfColors.legacyPrimary, - ), - borderRadius: BorderRadius.circular(12), - ), - ), - ), - const SizedBox(height: 24), - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton.icon( - onPressed: () { - final userId = controller.text.trim(); - if (userId.isEmpty) return; - vm.startCall(userId); - }, - icon: const Icon(Icons.videocam), - label: Text(context.translate(I18n.videocallStart)), - style: ElevatedButton.styleFrom( - backgroundColor: context.sfColors.legacyPrimary, - foregroundColor: colorScheme.onPrimary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - ), - const SizedBox(height: 16), - if (kDebugMode && state.localUserId.isEmpty) - TextButton( - onPressed: () async { - await vm.login(userId: 'p_test1', password: 'test123'); - }, - child: Text( - 'Login como p_test1 (testing)', - style: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.38)), - ), - ), - if (state.localUserId.isNotEmpty) - Text( - context.translate(I18n.videocallLoggedInAs, args: {'userId': state.localUserId}), - style: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.38), fontSize: 12), - ), ], ), ); } } -class _OutgoingView extends StatelessWidget { - const _OutgoingView({required this.state, required this.vm}); +class _VideocallOptionCard extends StatelessWidget { + const _VideocallOptionCard({ + this.image, + this.icon, + required this.text, + required this.onTap, + }); - final VideocallState state; - final VideocallController vm; + final Widget? image; + final Widget? icon; + final String text; + final VoidCallback onTap; @override Widget build(BuildContext context) { - return Center( - child: CallStatusIndicator( - screenMode: state.screenMode, - remoteUserId: state.remoteUserId, - onCancel: vm.hangUp, + final colorScheme = Theme.of(context).colorScheme; + + return Material( + color: colorScheme.surfaceContainerLowest, + borderRadius: BorderRadius.circular(12), + elevation: 0.5, + shadowColor: colorScheme.shadow.withValues(alpha: 0.3), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: SizeUtils.getByScreen(small: 16, big: 15), + vertical: SizeUtils.getByScreen(small: 14, big: 13), + ), + child: Row( + children: [ + SizedBox( + width: SizeUtils.getByScreen(small: 48, big: 44), + height: SizeUtils.getByScreen(small: 48, big: 44), + child: Center(child: image ?? icon), + ), + SizedBox(width: SizeUtils.getByScreen(small: 16, big: 15)), + Expanded( + child: Text( + text, + style: TextStyle( + fontSize: SizeUtils.getByScreen(small: 16, big: 15), + fontWeight: FontWeight.w500, + color: colorScheme.onSurface, + ), + ), + ), + ], + ), + ), ), ); } } +class _ActiveCallView extends StatelessWidget { + const _ActiveCallView({ + required this.state, + required this.vm, + required this.deviceName, + }); + + final VideocallState state; + final VideocallController vm; + final String deviceName; + + @override + Widget build(BuildContext context) { + final isOutgoing = state.screenMode == VideocallScreenMode.outgoing; + final hasRemoteVideo = state.remoteCanvas != null; + + return Stack( + fit: StackFit.expand, + children: [ + if (hasRemoteVideo) + VideoViewWidget(canvas: state.remoteCanvas!) + else if (state.localCanvas != null) + VideoViewWidget(canvas: state.localCanvas!) + else + const ColoredBox(color: Colors.black), + + if (hasRemoteVideo && state.localCanvas != null) + Positioned( + top: 8, + right: 16, + child: Container( + width: 100, + height: 140, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withValues(alpha: 0.3), width: 1), + ), + clipBehavior: Clip.antiAlias, + child: VideoViewWidget( + canvas: state.localCanvas!, + width: 100, + height: 140, + ), + ), + ), + + Positioned( + top: 8, + left: 16, + right: 16, + child: Row( + children: [ + Image.asset( + 'assets/shared/images/iso_sf.png', + width: 36, + height: 36, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + deviceName, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + if (isOutgoing) + Text( + context.translate(I18n.videocallConnecting), + style: TextStyle( + color: Colors.white.withValues(alpha: 0.7), + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ), + + Positioned( + left: 0, + right: 0, + bottom: 0, + child: CallControlsWidget( + isMicEnabled: state.isMicEnabled, + isSpeakerEnabled: state.isSpeakerEnabled, + isFrontCamera: state.isFrontCamera, + onToggleMic: vm.toggleMic, + onToggleSpeaker: vm.toggleSpeaker, + onSwitchCamera: vm.switchCamera, + onHangUp: vm.hangUp, + ), + ), + ], + ); + } +} + class _IncomingView extends StatelessWidget { const _IncomingView({required this.state, required this.vm}); @@ -228,83 +331,59 @@ class _IncomingView extends StatelessWidget { } } -class _InCallView extends StatelessWidget { - const _InCallView({required this.state, required this.vm}); +class _GroupCallView extends ConsumerWidget { + const _GroupCallView({required this.vm}); - final VideocallState state; final VideocallController vm; @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; + Widget build(BuildContext context, WidgetRef ref) { + final groupState = ref.watch(groupCallControllerProvider); + final groupVm = ref.read(groupCallControllerProvider.notifier); return Stack( fit: StackFit.expand, children: [ - if (state.remoteCanvas != null) - VideoViewWidget(canvas: state.remoteCanvas!) - else - Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.person, color: colorScheme.onSurface.withValues(alpha: 0.24), size: 96), - const SizedBox(height: 8), - Text( - context.translate(I18n.videocallWaitingRemoteVideo), - style: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.38)), - ), - ], - ), - ), - - if (state.localCanvas != null) + const ColoredBox(color: Colors.black), + ParticipantGridWidget(participants: groupState.participants), + if (groupState.localCanvas != null) Positioned( - top: 16, + top: 8, right: 16, child: Container( - width: 120, - height: 160, + width: 100, + height: 140, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - border: Border.all(color: colorScheme.onSurface.withValues(alpha: 0.24), width: 1), + border: Border.all( + color: Colors.white.withValues(alpha: 0.3), + width: 1, + ), ), clipBehavior: Clip.antiAlias, child: VideoViewWidget( - canvas: state.localCanvas!, - width: 120, - height: 160, + canvas: groupState.localCanvas!, + width: 100, + height: 140, ), ), ), - Positioned( left: 0, right: 0, bottom: 0, - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - colorScheme.surface.withValues(alpha: 0.8), - Colors.transparent, - ], - ), - ), - child: CallControlsWidget( - isMicEnabled: state.isMicEnabled, - isSpeakerEnabled: state.isSpeakerEnabled, - isFrontCamera: state.isFrontCamera, - onToggleMic: vm.toggleMic, - onToggleSpeaker: vm.toggleSpeaker, - onSwitchCamera: vm.switchCamera, - onHangUp: vm.hangUp, - ), + child: CallControlsWidget( + isMicEnabled: groupState.isMicEnabled, + isSpeakerEnabled: true, + isFrontCamera: true, + onToggleMic: groupVm.toggleMic, + onToggleSpeaker: () {}, + onSwitchCamera: groupVm.switchCamera, + onHangUp: vm.leaveGroupCall, ), ), ], ); } } + diff --git a/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/widgets/call_controls_widget.dart b/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/widgets/call_controls_widget.dart index 556c6395..7f4b2917 100644 --- a/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/widgets/call_controls_widget.dart +++ b/modules/legacy/modules/device_management/lib/src/features/videocall/presentation/widgets/call_controls_widget.dart @@ -23,39 +23,52 @@ class CallControlsWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _ControlButton( - icon: isMicEnabled ? Icons.mic : Icons.mic_off, - label: context.translate(isMicEnabled ? I18n.videocallMicOn : I18n.videocallMicOff), - isActive: isMicEnabled, - onPressed: onToggleMic, - ), - _ControlButton( - icon: isSpeakerEnabled - ? Icons.volume_up - : Icons.volume_off, - label: context.translate(isSpeakerEnabled ? I18n.videocallSpeaker : I18n.videocallEarpiece), - isActive: isSpeakerEnabled, - onPressed: onToggleSpeaker, - ), - _ControlButton( - icon: Icons.cameraswitch, - label: context.translate(isFrontCamera ? I18n.videocallCameraFront : I18n.videocallCameraBack), - isActive: true, - onPressed: onSwitchCamera, - ), - _ControlButton( - icon: Icons.call_end, - label: context.translate(I18n.videocallHangUp), - isActive: false, - isDestructive: true, - onPressed: onHangUp, - ), - ], + return SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _ControlButton( + icon: isMicEnabled ? Icons.mic_none : Icons.mic_off, + label: context.translate(I18n.videocallMute), + isActive: !isMicEnabled, + onPressed: onToggleMic, + ), + _ControlButton( + icon: isSpeakerEnabled ? Icons.volume_up : Icons.hearing, + label: context.translate(I18n.videocallSpeakerphone), + isActive: isSpeakerEnabled, + onPressed: onToggleSpeaker, + ), + _ControlButton( + icon: Icons.videocam, + label: context.translate(I18n.videocallCameraOff), + isActive: true, + onPressed: () {}, + ), + ], + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spacer(), + _HangUpButton(onPressed: onHangUp), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: _SwitchCameraButton(onPressed: onSwitchCamera), + ), + ), + ], + ), + ], + ), ), ); } @@ -66,31 +79,19 @@ class _ControlButton extends StatelessWidget { required this.icon, required this.label, required this.isActive, - this.isDestructive = false, required this.onPressed, }); final IconData icon; final String label; final bool isActive; - final bool isDestructive; final VoidCallback onPressed; @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - final backgroundColor = isDestructive - ? colorScheme.error - : isActive - ? colorScheme.onSurface.withValues(alpha: 0.2) - : colorScheme.onSurface.withValues(alpha: 0.4); - - final iconColor = isDestructive - ? Colors.white - : isActive - ? colorScheme.onSurface - : colorScheme.onSurface.withValues(alpha: 0.7); + final backgroundColor = isActive + ? Colors.white.withValues(alpha: 0.3) + : Colors.white.withValues(alpha: 0.15); return Column( mainAxisSize: MainAxisSize.min, @@ -103,16 +104,60 @@ class _ControlButton extends StatelessWidget { customBorder: const CircleBorder(), child: Padding( padding: const EdgeInsets.all(16), - child: Icon(icon, color: iconColor, size: 28), + child: Icon(icon, color: Colors.white, size: 28), ), ), ), - const SizedBox(height: 4), + const SizedBox(height: 6), Text( label, - style: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.7), fontSize: 11), + style: TextStyle(color: Colors.white.withValues(alpha: 0.9), fontSize: 11), ), ], ); } } + +class _HangUpButton extends StatelessWidget { + const _HangUpButton({required this.onPressed}); + + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return Material( + color: Theme.of(context).colorScheme.error, + shape: const CircleBorder(), + child: InkWell( + onTap: onPressed, + customBorder: const CircleBorder(), + child: const Padding( + padding: EdgeInsets.all(18), + child: Icon(Icons.call_end, color: Colors.white, size: 32), + ), + ), + ); + } +} + +class _SwitchCameraButton extends StatelessWidget { + const _SwitchCameraButton({required this.onPressed}); + + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.white.withValues(alpha: 0.2), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(12), + child: const Padding( + padding: EdgeInsets.all(12), + child: Icon(Icons.cameraswitch_outlined, color: Colors.white, size: 24), + ), + ), + ); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/videocall/providers/videocall_signaling_datasource_provider.dart b/modules/legacy/modules/device_management/lib/src/features/videocall/providers/videocall_signaling_datasource_provider.dart index 5af91e3d..85b4f980 100644 --- a/modules/legacy/modules/device_management/lib/src/features/videocall/providers/videocall_signaling_datasource_provider.dart +++ b/modules/legacy/modules/device_management/lib/src/features/videocall/providers/videocall_signaling_datasource_provider.dart @@ -1,12 +1,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:get_it/get_it.dart'; -import 'package:sf_infrastructure/sf_infrastructure.dart'; +import 'package:legacy_device_state/legacy_device_state.dart'; import '../data/datasources/videocall_signaling_datasource.dart'; import '../data/datasources/videocall_signaling_datasource_impl.dart'; final videocallSignalingDatasourceProvider = Provider((ref) { - final repository = GetIt.I(); - return VideocallSignalingDatasourceImpl(repository); + return VideocallSignalingDatasourceImpl( + ref.read(commandsRepositoryProvider), + ); }); diff --git a/modules/legacy/modules/device_management/pubspec.yaml b/modules/legacy/modules/device_management/pubspec.yaml index 9be0867e..8c94adfd 100644 --- a/modules/legacy/modules/device_management/pubspec.yaml +++ b/modules/legacy/modules/device_management/pubspec.yaml @@ -72,6 +72,7 @@ dependencies: image_picker: ^1.2.1 share_plus: ^10.1.4 path_provider: ^2.1.5 + permission_handler: ^12.0.1 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. diff --git a/modules/legacy/packages/legacy_device_state/lib/src/data/models/send_command_request_model.dart b/modules/legacy/packages/legacy_device_state/lib/src/data/models/send_command_request_model.dart index 9bb5302a..5719f949 100644 --- a/modules/legacy/packages/legacy_device_state/lib/src/data/models/send_command_request_model.dart +++ b/modules/legacy/packages/legacy_device_state/lib/src/data/models/send_command_request_model.dart @@ -32,6 +32,14 @@ enum DeviceCommand { setWifi, @JsonValue('WIFI_DELETE') wifiDelete, + @JsonValue('VIDEO_CALL_REQUEST') + videoCallRequest, + @JsonValue('VIDEO_CALL_CANCEL') + videoCallCancel, + @JsonValue('VIDEO_CALL_REFUSE') + videoCallRefuse, + @JsonValue('VIDEO_CALL_ROOM_COUNT_REQUEST') + videoCallRoomCountRequest, } @freezed diff --git a/modules/legacy/packages/legacy_device_state/lib/src/data/models/send_command_request_model.g.dart b/modules/legacy/packages/legacy_device_state/lib/src/data/models/send_command_request_model.g.dart index ff690e76..7de25aed 100644 --- a/modules/legacy/packages/legacy_device_state/lib/src/data/models/send_command_request_model.g.dart +++ b/modules/legacy/packages/legacy_device_state/lib/src/data/models/send_command_request_model.g.dart @@ -37,4 +37,8 @@ const _$DeviceCommandEnumMap = { DeviceCommand.wifiSearch: 'WIFI_SEARCH', DeviceCommand.setWifi: 'SET_WIFI', DeviceCommand.wifiDelete: 'WIFI_DELETE', + DeviceCommand.videoCallRequest: 'VIDEO_CALL_REQUEST', + DeviceCommand.videoCallCancel: 'VIDEO_CALL_CANCEL', + DeviceCommand.videoCallRefuse: 'VIDEO_CALL_REFUSE', + DeviceCommand.videoCallRoomCountRequest: 'VIDEO_CALL_ROOM_COUNT_REQUEST', }; diff --git a/packages/sf_infrastructure/lib/src/network/dio_client.dart b/packages/sf_infrastructure/lib/src/network/dio_client.dart index 7c6e860a..c3fe2578 100644 --- a/packages/sf_infrastructure/lib/src/network/dio_client.dart +++ b/packages/sf_infrastructure/lib/src/network/dio_client.dart @@ -3,6 +3,8 @@ import 'package:dio/dio.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:path_provider/path_provider.dart'; +import 'retry_interceptor.dart'; + Future buildDioClient({ required String baseUrl, required String origin, @@ -25,6 +27,7 @@ Future buildDioClient({ final jar = cookieJar ?? await buildPersistCookieJar(); dio.interceptors.add(CookieManager(jar)); + dio.interceptors.add(RetryInterceptor(dio)); if (log) { dio.interceptors.add( diff --git a/packages/sf_infrastructure/lib/src/network/retry_interceptor.dart b/packages/sf_infrastructure/lib/src/network/retry_interceptor.dart new file mode 100644 index 00000000..e8af7fd0 --- /dev/null +++ b/packages/sf_infrastructure/lib/src/network/retry_interceptor.dart @@ -0,0 +1,52 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; + +class RetryInterceptor extends Interceptor { + RetryInterceptor(this._dio, {this.maxRetries = 2, this.retryDelay = const Duration(seconds: 1)}); + + final Dio _dio; + final int maxRetries; + final Duration retryDelay; + + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + if (!_isTransientError(err)) { + return handler.next(err); + } + + final attempt = _getAttempt(err.requestOptions); + if (attempt >= maxRetries) { + return handler.next(err); + } + + _retry(err, handler, attempt + 1); + } + + bool _isTransientError(DioException err) { + final error = err.error; + if (error is HandshakeException) return true; + if (error is SocketException) return true; + if (err.type == DioExceptionType.connectionError) return true; + if (err.type == DioExceptionType.connectionTimeout) return true; + return false; + } + + int _getAttempt(RequestOptions options) { + return options.extra['_retryAttempt'] as int? ?? 0; + } + + Future _retry(DioException err, ErrorInterceptorHandler handler, int attempt) async { + await Future.delayed(retryDelay * attempt); + + final options = err.requestOptions; + options.extra['_retryAttempt'] = attempt; + + try { + final response = await _dio.fetch(options); + handler.resolve(response); + } on DioException catch (retryError) { + handler.next(retryError); + } + } +} diff --git a/packages/sf_localizations/assets/l10n/de.json b/packages/sf_localizations/assets/l10n/de.json index 8b15033e..94735b2f 100644 --- a/packages/sf_localizations/assets/l10n/de.json +++ b/packages/sf_localizations/assets/l10n/de.json @@ -711,6 +711,14 @@ "videocallErrorCamera": "Kameraberechtigung erforderlich", "videocallErrorMic": "Mikrofonberechtigung erforderlich", "videocallErrorMissedCall": "Verpasster Anruf", + "videocallCallEnded": "Anruf beendet", + "videocallGroupCall": "Gruppen-Videoanruf", + "videocallDisclaimer": "*Für einen optimalen Videoanruf stellen Sie sicher, dass Ihre Uhr über mobile Daten mit Internetverbindung, guter Abdeckung oder WLAN-Verbindung verfügt.", + "videocallConnecting": "Warten auf Verbindungsaufbau...", + "videocallMute": "stummschalten", + "videocallSpeakerphone": "Freisprechen", + "videocallCameraOff": "Kamera ausschalten", + "videocallCameraOn": "Kamera einschalten", "positionUpdated": "Letzte verfügbare Position aktualisiert", "locationMapStyleLight": "Hell", "locationMapStyleDark": "Dunkel", diff --git a/packages/sf_localizations/assets/l10n/en.json b/packages/sf_localizations/assets/l10n/en.json index 91bd48b9..443d014f 100755 --- a/packages/sf_localizations/assets/l10n/en.json +++ b/packages/sf_localizations/assets/l10n/en.json @@ -891,6 +891,14 @@ "videocallErrorCamera": "Camera permission required", "videocallErrorMic": "Microphone permission required", "videocallErrorMissedCall": "Missed call", + "videocallCallEnded": "Call ended", + "videocallGroupCall": "Group video call", + "videocallDisclaimer": "*For an optimal video call, make sure your watch has mobile data with Internet connection, good coverage or WiFi connection.", + "videocallConnecting": "Waiting for the connection to be established...", + "videocallMute": "mute", + "videocallSpeakerphone": "speakerphone", + "videocallCameraOff": "turn off camera", + "videocallCameraOn": "turn on camera", "positionUpdated": "Updated to latest available position", "locationMapStyleLight": "Light", "locationMapStyleDark": "Dark", diff --git a/packages/sf_localizations/assets/l10n/es.json b/packages/sf_localizations/assets/l10n/es.json index eb276004..ce1afd26 100644 --- a/packages/sf_localizations/assets/l10n/es.json +++ b/packages/sf_localizations/assets/l10n/es.json @@ -892,6 +892,14 @@ "videocallErrorCamera": "Se requiere permiso de cámara", "videocallErrorMic": "Se requiere permiso de micrófono", "videocallErrorMissedCall": "Llamada perdida", + "videocallCallEnded": "Llamada finalizada", + "videocallGroupCall": "Videollamada grupal", + "videocallDisclaimer": "*Para una llamada de video óptima, asegúrate de que tu reloj tenga datos móviles con conexión a Internet, buena cobertura o conexión wifi.", + "videocallConnecting": "Esperando a que se establezca la conexión...", + "videocallMute": "silenciar", + "videocallSpeakerphone": "manos libres", + "videocallCameraOff": "apagar la cámara", + "videocallCameraOn": "encender la cámara", "positionUpdated": "Última posición disponible actualizada", "locationMapStyleLight": "Claro", "locationMapStyleDark": "Oscuro", diff --git a/packages/sf_localizations/assets/l10n/fr.json b/packages/sf_localizations/assets/l10n/fr.json index d0f20f01..185c6e61 100644 --- a/packages/sf_localizations/assets/l10n/fr.json +++ b/packages/sf_localizations/assets/l10n/fr.json @@ -711,6 +711,14 @@ "videocallErrorCamera": "Autorisation de la caméra requise", "videocallErrorMic": "Autorisation du microphone requise", "videocallErrorMissedCall": "Appel manqué", + "videocallCallEnded": "Appel terminé", + "videocallGroupCall": "Appel vidéo de groupe", + "videocallDisclaimer": "*Pour un appel vidéo optimal, assurez-vous que votre montre dispose de données mobiles avec connexion Internet, bonne couverture ou connexion WiFi.", + "videocallConnecting": "En attente de la connexion...", + "videocallMute": "couper le son", + "videocallSpeakerphone": "haut-parleur", + "videocallCameraOff": "éteindre la caméra", + "videocallCameraOn": "allumer la caméra", "positionUpdated": "Dernière position disponible mise à jour", "locationMapStyleLight": "Clair", "locationMapStyleDark": "Sombre", diff --git a/packages/sf_localizations/assets/l10n/it.json b/packages/sf_localizations/assets/l10n/it.json index dc76aa18..b121a8c4 100644 --- a/packages/sf_localizations/assets/l10n/it.json +++ b/packages/sf_localizations/assets/l10n/it.json @@ -711,6 +711,14 @@ "videocallErrorCamera": "Permesso fotocamera richiesto", "videocallErrorMic": "Permesso microfono richiesto", "videocallErrorMissedCall": "Chiamata persa", + "videocallCallEnded": "Chiamata terminata", + "videocallGroupCall": "Videochiamata di gruppo", + "videocallDisclaimer": "*Per una videochiamata ottimale, assicurati che il tuo orologio abbia dati mobili con connessione Internet, buona copertura o connessione WiFi.", + "videocallConnecting": "In attesa della connessione...", + "videocallMute": "silenzia", + "videocallSpeakerphone": "vivavoce", + "videocallCameraOff": "spegni fotocamera", + "videocallCameraOn": "accendi fotocamera", "positionUpdated": "Ultima posizione disponibile aggiornata", "locationMapStyleLight": "Chiaro", "locationMapStyleDark": "Scuro", diff --git a/packages/sf_localizations/assets/l10n/pt.json b/packages/sf_localizations/assets/l10n/pt.json index 7ffcacca..b5024a9a 100644 --- a/packages/sf_localizations/assets/l10n/pt.json +++ b/packages/sf_localizations/assets/l10n/pt.json @@ -711,6 +711,14 @@ "videocallErrorCamera": "Permissão de câmara necessária", "videocallErrorMic": "Permissão de microfone necessária", "videocallErrorMissedCall": "Chamada perdida", + "videocallCallEnded": "Chamada terminada", + "videocallGroupCall": "Videochamada em grupo", + "videocallDisclaimer": "*Para uma videochamada ideal, certifique-se de que o seu relógio tenha dados móveis com conexão à Internet, boa cobertura ou conexão WiFi.", + "videocallConnecting": "Aguardando conexão...", + "videocallMute": "silenciar", + "videocallSpeakerphone": "viva-voz", + "videocallCameraOff": "desligar câmara", + "videocallCameraOn": "ligar câmara", "positionUpdated": "Última posição disponível atualizada", "locationMapStyleLight": "Claro", "locationMapStyleDark": "Escuro", diff --git a/packages/sf_localizations/lib/src/generated/i18n.dart b/packages/sf_localizations/lib/src/generated/i18n.dart index 76effcff..188e5df8 100755 --- a/packages/sf_localizations/lib/src/generated/i18n.dart +++ b/packages/sf_localizations/lib/src/generated/i18n.dart @@ -1007,6 +1007,14 @@ class I18n { static const String vibrationOnly = 'vibrationOnly'; static const String videoCall = 'videoCall'; static const String videocallAccept = 'videocallAccept'; + static const String videocallCallEnded = 'videocallCallEnded'; + static const String videocallGroupCall = 'videocallGroupCall'; + static const String videocallDisclaimer = 'videocallDisclaimer'; + static const String videocallConnecting = 'videocallConnecting'; + static const String videocallMute = 'videocallMute'; + static const String videocallSpeakerphone = 'videocallSpeakerphone'; + static const String videocallCameraOff = 'videocallCameraOff'; + static const String videocallCameraOn = 'videocallCameraOn'; static const String videocallCalling = 'videocallCalling'; static const String videocallCameraBack = 'videocallCameraBack'; static const String videocallCameraFront = 'videocallCameraFront'; diff --git a/packages/videocall_sdk/lib/src/manager/videocall_sdk_manager.dart b/packages/videocall_sdk/lib/src/manager/videocall_sdk_manager.dart index 4836c87b..f7d3a87c 100644 --- a/packages/videocall_sdk/lib/src/manager/videocall_sdk_manager.dart +++ b/packages/videocall_sdk/lib/src/manager/videocall_sdk_manager.dart @@ -49,10 +49,9 @@ class VideocallSdkManager { return true; } - Future destroy() async { + Future shutdown() async { if (!_initialized) return; - // Reverse order await pushService.destroy(); await channelService.destroy(); await callService.destroy(); @@ -60,16 +59,13 @@ class VideocallSdkManager { await deviceService.destroy(); await client.destroy(); - _initialized = false; - } - - void dispose() { callService.dispose(); channelService.dispose(); pushService.dispose(); netService.dispose(); deviceService.dispose(); client.dispose(); + _initialized = false; } } diff --git a/packages/videocall_sdk/lib/src/models/videocall_item.dart b/packages/videocall_sdk/lib/src/models/videocall_item.dart index 52f186c5..8ee64bec 100644 --- a/packages/videocall_sdk/lib/src/models/videocall_item.dart +++ b/packages/videocall_sdk/lib/src/models/videocall_item.dart @@ -1,42 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + import 'call_direction.dart'; import 'call_state.dart'; -class VideocallItem { - const VideocallItem({ - required this.userId, - required this.isVideo, - required this.direction, - required this.state, - this.uploadVideoStreamSelf = false, - this.uploadVideoStreamOther = false, - }); +part 'videocall_item.freezed.dart'; - final String userId; - final bool isVideo; - final CallDirection direction; - final VideocallState state; - final bool uploadVideoStreamSelf; - final bool uploadVideoStreamOther; - - bool get isTalking => state == VideocallState.talking; - - VideocallItem copyWith({ - String? userId, - bool? isVideo, - CallDirection? direction, - VideocallState? state, - bool? uploadVideoStreamSelf, - bool? uploadVideoStreamOther, - }) { - return VideocallItem( - userId: userId ?? this.userId, - isVideo: isVideo ?? this.isVideo, - direction: direction ?? this.direction, - state: state ?? this.state, - uploadVideoStreamSelf: - uploadVideoStreamSelf ?? this.uploadVideoStreamSelf, - uploadVideoStreamOther: - uploadVideoStreamOther ?? this.uploadVideoStreamOther, - ); - } +@freezed +abstract class VideocallItem with _$VideocallItem { + const factory VideocallItem({ + required String userId, + required bool isVideo, + required CallDirection direction, + required VideocallState state, + @Default(false) bool uploadVideoStreamSelf, + @Default(false) bool uploadVideoStreamOther, + }) = _VideocallItem; +} + +extension VideocallItemX on VideocallItem { + bool get isTalking => state == VideocallState.talking; } diff --git a/packages/videocall_sdk/lib/src/models/videocall_item.freezed.dart b/packages/videocall_sdk/lib/src/models/videocall_item.freezed.dart new file mode 100644 index 00000000..b033b8ec --- /dev/null +++ b/packages/videocall_sdk/lib/src/models/videocall_item.freezed.dart @@ -0,0 +1,286 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'videocall_item.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$VideocallItem { + + String get userId; bool get isVideo; CallDirection get direction; VideocallState get state; bool get uploadVideoStreamSelf; bool get uploadVideoStreamOther; +/// Create a copy of VideocallItem +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$VideocallItemCopyWith get copyWith => _$VideocallItemCopyWithImpl(this as VideocallItem, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is VideocallItem&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.isVideo, isVideo) || other.isVideo == isVideo)&&(identical(other.direction, direction) || other.direction == direction)&&(identical(other.state, state) || other.state == state)&&(identical(other.uploadVideoStreamSelf, uploadVideoStreamSelf) || other.uploadVideoStreamSelf == uploadVideoStreamSelf)&&(identical(other.uploadVideoStreamOther, uploadVideoStreamOther) || other.uploadVideoStreamOther == uploadVideoStreamOther)); +} + + +@override +int get hashCode => Object.hash(runtimeType,userId,isVideo,direction,state,uploadVideoStreamSelf,uploadVideoStreamOther); + +@override +String toString() { + return 'VideocallItem(userId: $userId, isVideo: $isVideo, direction: $direction, state: $state, uploadVideoStreamSelf: $uploadVideoStreamSelf, uploadVideoStreamOther: $uploadVideoStreamOther)'; +} + + +} + +/// @nodoc +abstract mixin class $VideocallItemCopyWith<$Res> { + factory $VideocallItemCopyWith(VideocallItem value, $Res Function(VideocallItem) _then) = _$VideocallItemCopyWithImpl; +@useResult +$Res call({ + String userId, bool isVideo, CallDirection direction, VideocallState state, bool uploadVideoStreamSelf, bool uploadVideoStreamOther +}); + + + + +} +/// @nodoc +class _$VideocallItemCopyWithImpl<$Res> + implements $VideocallItemCopyWith<$Res> { + _$VideocallItemCopyWithImpl(this._self, this._then); + + final VideocallItem _self; + final $Res Function(VideocallItem) _then; + +/// Create a copy of VideocallItem +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? userId = null,Object? isVideo = null,Object? direction = null,Object? state = null,Object? uploadVideoStreamSelf = null,Object? uploadVideoStreamOther = null,}) { + return _then(_self.copyWith( +userId: null == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable +as String,isVideo: null == isVideo ? _self.isVideo : isVideo // ignore: cast_nullable_to_non_nullable +as bool,direction: null == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable +as CallDirection,state: null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable +as VideocallState,uploadVideoStreamSelf: null == uploadVideoStreamSelf ? _self.uploadVideoStreamSelf : uploadVideoStreamSelf // ignore: cast_nullable_to_non_nullable +as bool,uploadVideoStreamOther: null == uploadVideoStreamOther ? _self.uploadVideoStreamOther : uploadVideoStreamOther // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// Adds pattern-matching-related methods to [VideocallItem]. +extension VideocallItemPatterns on VideocallItem { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _VideocallItem value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _VideocallItem() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _VideocallItem value) $default,){ +final _that = this; +switch (_that) { +case _VideocallItem(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _VideocallItem value)? $default,){ +final _that = this; +switch (_that) { +case _VideocallItem() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String userId, bool isVideo, CallDirection direction, VideocallState state, bool uploadVideoStreamSelf, bool uploadVideoStreamOther)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _VideocallItem() when $default != null: +return $default(_that.userId,_that.isVideo,_that.direction,_that.state,_that.uploadVideoStreamSelf,_that.uploadVideoStreamOther);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String userId, bool isVideo, CallDirection direction, VideocallState state, bool uploadVideoStreamSelf, bool uploadVideoStreamOther) $default,) {final _that = this; +switch (_that) { +case _VideocallItem(): +return $default(_that.userId,_that.isVideo,_that.direction,_that.state,_that.uploadVideoStreamSelf,_that.uploadVideoStreamOther);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String userId, bool isVideo, CallDirection direction, VideocallState state, bool uploadVideoStreamSelf, bool uploadVideoStreamOther)? $default,) {final _that = this; +switch (_that) { +case _VideocallItem() when $default != null: +return $default(_that.userId,_that.isVideo,_that.direction,_that.state,_that.uploadVideoStreamSelf,_that.uploadVideoStreamOther);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _VideocallItem implements VideocallItem { + const _VideocallItem({required this.userId, required this.isVideo, required this.direction, required this.state, this.uploadVideoStreamSelf = false, this.uploadVideoStreamOther = false}); + + +@override final String userId; +@override final bool isVideo; +@override final CallDirection direction; +@override final VideocallState state; +@override@JsonKey() final bool uploadVideoStreamSelf; +@override@JsonKey() final bool uploadVideoStreamOther; + +/// Create a copy of VideocallItem +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$VideocallItemCopyWith<_VideocallItem> get copyWith => __$VideocallItemCopyWithImpl<_VideocallItem>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _VideocallItem&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.isVideo, isVideo) || other.isVideo == isVideo)&&(identical(other.direction, direction) || other.direction == direction)&&(identical(other.state, state) || other.state == state)&&(identical(other.uploadVideoStreamSelf, uploadVideoStreamSelf) || other.uploadVideoStreamSelf == uploadVideoStreamSelf)&&(identical(other.uploadVideoStreamOther, uploadVideoStreamOther) || other.uploadVideoStreamOther == uploadVideoStreamOther)); +} + + +@override +int get hashCode => Object.hash(runtimeType,userId,isVideo,direction,state,uploadVideoStreamSelf,uploadVideoStreamOther); + +@override +String toString() { + return 'VideocallItem(userId: $userId, isVideo: $isVideo, direction: $direction, state: $state, uploadVideoStreamSelf: $uploadVideoStreamSelf, uploadVideoStreamOther: $uploadVideoStreamOther)'; +} + + +} + +/// @nodoc +abstract mixin class _$VideocallItemCopyWith<$Res> implements $VideocallItemCopyWith<$Res> { + factory _$VideocallItemCopyWith(_VideocallItem value, $Res Function(_VideocallItem) _then) = __$VideocallItemCopyWithImpl; +@override @useResult +$Res call({ + String userId, bool isVideo, CallDirection direction, VideocallState state, bool uploadVideoStreamSelf, bool uploadVideoStreamOther +}); + + + + +} +/// @nodoc +class __$VideocallItemCopyWithImpl<$Res> + implements _$VideocallItemCopyWith<$Res> { + __$VideocallItemCopyWithImpl(this._self, this._then); + + final _VideocallItem _self; + final $Res Function(_VideocallItem) _then; + +/// Create a copy of VideocallItem +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? userId = null,Object? isVideo = null,Object? direction = null,Object? state = null,Object? uploadVideoStreamSelf = null,Object? uploadVideoStreamOther = null,}) { + return _then(_VideocallItem( +userId: null == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable +as String,isVideo: null == isVideo ? _self.isVideo : isVideo // ignore: cast_nullable_to_non_nullable +as bool,direction: null == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable +as CallDirection,state: null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable +as VideocallState,uploadVideoStreamSelf: null == uploadVideoStreamSelf ? _self.uploadVideoStreamSelf : uploadVideoStreamSelf // ignore: cast_nullable_to_non_nullable +as bool,uploadVideoStreamOther: null == uploadVideoStreamOther ? _self.uploadVideoStreamOther : uploadVideoStreamOther // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +// dart format on diff --git a/packages/videocall_sdk/lib/src/providers/videocall_providers.dart b/packages/videocall_sdk/lib/src/providers/videocall_providers.dart index ba1ef12b..bd385dda 100644 --- a/packages/videocall_sdk/lib/src/providers/videocall_providers.dart +++ b/packages/videocall_sdk/lib/src/providers/videocall_providers.dart @@ -11,8 +11,6 @@ import '../services/videocall_device_service.dart'; import '../services/videocall_net_service.dart'; import '../services/videocall_push_service.dart'; -// -- Service providers (thin wrappers over GetIt) -- - final videocallManagerProvider = Provider((ref) { return GetIt.I(); }); @@ -42,8 +40,6 @@ final videocallNetServiceProvider = Provider((ref) { return GetIt.I(); }); -// -- Stream providers (for reactive UI consumption) -- - final videocallClientStateProvider = StreamProvider((ref) { return ref.watch(videocallClientProvider).stateStream; diff --git a/packages/videocall_sdk/lib/src/services/videocall_call_service.dart b/packages/videocall_sdk/lib/src/services/videocall_call_service.dart index d4030cc9..7f5a620d 100644 --- a/packages/videocall_sdk/lib/src/services/videocall_call_service.dart +++ b/packages/videocall_sdk/lib/src/services/videocall_call_service.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/material.dart'; import 'package:jc_sdk/jc_sdk.dart'; import '../models/call_direction.dart'; @@ -12,16 +13,14 @@ class VideocallCallService with JCCallCallback { VideocallCallService({ required VideocallClient client, required VideocallDeviceService deviceService, - }) : _clientRef = client, - _deviceRef = deviceService; + }) : _clientRef = client, + _deviceRef = deviceService; final VideocallClient _clientRef; final VideocallDeviceService _deviceRef; JCCall? _call; JCCall? get call => _call; - // -- Streams -- - final _callItemAddController = StreamController.broadcast(); Stream get callItemAddStream => _callItemAddController.stream; @@ -37,25 +36,9 @@ class VideocallCallService with JCCallCallback { final _missedCallController = StreamController.broadcast(); Stream get missedCallStream => _missedCallController.stream; - final _messageReceivedController = - StreamController<({String type, String content, JCCallItem item})> - .broadcast(); - Stream<({String type, String content, JCCallItem item})> - get messageReceivedStream => _messageReceivedController.stream; - - final _dtmfReceivedController = - StreamController<({JCCallItem item, int value})>.broadcast(); - Stream<({JCCallItem item, int value})> get dtmfReceivedStream => - _dtmfReceivedController.stream; - - final _earlyMediaController = StreamController.broadcast(); - Stream get earlyMediaStream => _earlyMediaController.stream; - VideocallItem? _currentItem; VideocallItem? get currentItem => _currentItem; - // -- Lifecycle -- - Future initialize() async { final client = _clientRef.client; final mediaDevice = _deviceRef.mediaDevice; @@ -66,8 +49,6 @@ class VideocallCallService with JCCallCallback { Future destroy() async => JCCall.destroy(); - // -- Call actions -- - Future startCall({ required String userId, required bool isVideo, @@ -89,14 +70,11 @@ class VideocallCallService with JCCallCallback { return _call!.term(item, reason ?? JCCall.REASON_NONE, description ?? ''); } - Future termCall( - JCCallItem item, int reason, String description) async { + Future termCall(JCCallItem item, int reason, String description) async { if (_call == null) return false; return _call!.term(item, reason, description); } - // -- Mute/Audio -- - Future mute(JCCallItem item) async { if (_call == null) return false; return _call!.mute(item); @@ -117,8 +95,6 @@ class VideocallCallService with JCCallCallback { return _call!.setMicScale(item, scale); } - // -- Hold -- - Future hold(JCCallItem item) async { if (_call == null) return false; return _call!.hold(item); @@ -129,15 +105,14 @@ class VideocallCallService with JCCallCallback { return _call!.becomeActive(item); } - // -- Video -- - Future enableUploadVideoStream(JCCallItem item) async { if (_call == null) return false; return _call!.enableUploadVideoStream(item); } - Future startLocalVideo( - {int renderType = JCMediaDevice.RENDER_FULL_AUTO}) async { + Future startLocalVideo({ + int renderType = JCMediaDevice.RENDER_FULL_AUTO, + }) async { final item = await _call?.getActiveCallItem(); if (item == null) return null; return item.startSelfVideo(renderType); @@ -149,8 +124,9 @@ class VideocallCallService with JCCallCallback { return item.stopSelfVideo(); } - Future startRemoteVideo( - {int renderType = JCMediaDevice.RENDER_FULL_CONTENT}) async { + Future startRemoteVideo({ + int renderType = JCMediaDevice.RENDER_FULL_CONTENT, + }) async { final item = await _call?.getActiveCallItem(); if (item == null) return null; return item.startOtherVideo(renderType); @@ -162,10 +138,11 @@ class VideocallCallService with JCCallCallback { return item.stopOtherVideo(); } - // -- Recording -- - Future audioRecord( - JCCallItem item, bool enable, String filePath) async { + JCCallItem item, + bool enable, + String filePath, + ) async { if (_call == null) return false; return _call!.audioRecord(item, enable, filePath); } @@ -182,13 +159,18 @@ class VideocallCallService with JCCallCallback { }) async { if (_call == null) return false; return _call!.videoRecord( - item, enable, remote, width, height, filePath, bothAudio, keyframe); + item, + enable, + remote, + width, + height, + filePath, + bothAudio, + keyframe, + ); } - // -- Messaging -- - - Future sendMessage( - JCCallItem item, String type, String content) async { + Future sendMessage(JCCallItem item, String type, String content) async { if (_call == null) return false; return _call!.sendMessage(item, type, content); } @@ -198,14 +180,9 @@ class VideocallCallService with JCCallCallback { return _call!.sendDtmf(item, value); } - // -- Items -- - Future?> getCallItems() async => _call?.getCallItems(); - Future getActiveCallItem() async => - _call?.getActiveCallItem(); - - // -- Config -- + Future getActiveCallItem() async => _call?.getActiveCallItem(); Future getStatistics() async { if (_call == null) return ''; @@ -232,20 +209,13 @@ class VideocallCallService with JCCallCallback { static Future generateMediaConfigByMode(int mode) => JCCall.generateByMode(mode); - // -- Dispose -- - void dispose() { _callItemAddController.close(); _callItemUpdateController.close(); _callItemRemoveController.close(); _missedCallController.close(); - _messageReceivedController.close(); - _dtmfReceivedController.close(); - _earlyMediaController.close(); } - // -- Internal -- - VideocallState _mapCallState(int state) { if (state == JCCall.STATE_PENDING) return VideocallState.pending; if (state == JCCall.STATE_CONNECTING) return VideocallState.connecting; @@ -270,30 +240,36 @@ class VideocallCallService with JCCallCallback { ); } - // -- JCCallCallback -- - @override void onCallItemAdd(JCCallItem item) { + debugPrint( + '[VideocallSDK] onCallItemAdd: userId=${item.getUserId()}, video=${item.getVideo()}, direction=${item.getDirection()}', + ); _currentItem = _buildItem(item, VideocallState.pending); _callItemAddController.add(_currentItem!); } @override void onCallItemUpdate(JCCallItem item, ChangeParam changeParam) { - _currentItem = _buildItem(item, _mapCallState(item.getState())); + final mappedState = _mapCallState(item.getState()); + debugPrint( + '[VideocallSDK] onCallItemUpdate: rawState=${item.getState()}, mappedState=$mappedState, uploadSelf=${item.uploadVideoStreamSelf}, uploadOther=${item.uploadVideoStreamOther}', + ); + _currentItem = _buildItem(item, mappedState); _callItemUpdateController.add(_currentItem!); } @override void onCallItemRemove(JCCallItem item, int reason, String description) { + debugPrint( + '[VideocallSDK] onCallItemRemove: reason=$reason, description=$description', + ); _currentItem = null; _callItemRemoveController.add((reason: reason, description: description)); } @override - void onMessageReceive(String type, String content, JCCallItem item) { - _messageReceivedController.add((type: type, content: content, item: item)); - } + void onMessageReceive(String type, String content, JCCallItem item) {} @override void onMissedCallItem(JCCallItem item) { @@ -301,14 +277,10 @@ class VideocallCallService with JCCallCallback { } @override - void onDtmfReceived(JCCallItem item, int value) { - _dtmfReceivedController.add((item: item, value: value)); - } + void onDtmfReceived(JCCallItem item, int value) {} @override - void onEarlyMediaReceived(JCCallItem item) { - _earlyMediaController.add(item); - } + void onEarlyMediaReceived(JCCallItem item) {} @override void onSipRingInfoReceived(JCCallItem item, String callSipType) {} diff --git a/packages/videocall_sdk/lib/src/services/videocall_channel_service.dart b/packages/videocall_sdk/lib/src/services/videocall_channel_service.dart index ae1a7235..f560ec4c 100644 --- a/packages/videocall_sdk/lib/src/services/videocall_channel_service.dart +++ b/packages/videocall_sdk/lib/src/services/videocall_channel_service.dart @@ -17,7 +17,6 @@ class VideocallChannelService with JCMediaChannelCallback { JCMediaChannel? _channel; JCMediaChannel? get channel => _channel; - // -- Streams -- final _stateChangeController = StreamController<({int state, int oldState})>.broadcast(); @@ -97,7 +96,6 @@ class VideocallChannelService with JCMediaChannelCallback { Stream<({int operationId, bool result, int reason})> get inviteSipUserResultStream => _inviteSipUserResultController.stream; - // -- Lifecycle -- Future initialize() async { final client = _clientRef.client; @@ -109,7 +107,6 @@ class VideocallChannelService with JCMediaChannelCallback { Future destroy() async => JCMediaChannel.destroy(); - // -- Channel actions -- Future join(String channelId, {JoinParam? joinParam}) async { if (_channel == null) return false; @@ -131,7 +128,6 @@ class VideocallChannelService with JCMediaChannelCallback { return _channel!.query(channelId); } - // -- Audio/Video streams -- Future enableUploadAudioStream(bool enable) async { if (_channel == null) return false; @@ -159,7 +155,6 @@ class VideocallChannelService with JCMediaChannelCallback { return _channel!.requestScreenVideo(screenUri, pictureSize); } - // -- Screen share / CDN / Recording -- Future enableScreenShare( bool enable, ScreenShareParam? screenShareParam) async { @@ -177,7 +172,6 @@ class VideocallChannelService with JCMediaChannelCallback { return _channel!.enableRecord(enable, recordParam); } - // -- Participants -- Future?> getParticipants() async => _channel?.getParticipants(); @@ -195,7 +189,6 @@ class VideocallChannelService with JCMediaChannelCallback { return _channel!.inviteSipUser(userId, sipParam); } - // -- Custom roles/state -- Future setCustomRole(int customRole, {JCMediaChannelParticipant? participant}) async { @@ -219,7 +212,6 @@ class VideocallChannelService with JCMediaChannelCallback { return _channel!.getCustomState(); } - // -- Channel properties -- Future setCustomProperty(String property) async { if (_channel == null) return -1; @@ -231,7 +223,6 @@ class VideocallChannelService with JCMediaChannelCallback { return _channel!.getCustomProperty(); } - // -- Channel info -- Future getChannelUri() async { if (_channel == null) return ''; @@ -313,7 +304,6 @@ class VideocallChannelService with JCMediaChannelCallback { return _channel!.getScreenUserId(); } - // -- Self participant -- Future getSelfParticipant() async { return _channel?.getSelfParticipant(); @@ -325,7 +315,6 @@ class VideocallChannelService with JCMediaChannelCallback { return _channel!.subscribeParticipantAudio(participant, subscribe); } - // -- Screen share video -- Future startScreenShareVideo( int renderType, int pictureSize) async { @@ -337,7 +326,6 @@ class VideocallChannelService with JCMediaChannelCallback { return _channel!.stopScreenShareVideo(); } - // -- Video ratio / resolution -- Future enableSelfVideoRatio(bool enable, double ratio) async { if (_channel == null) return false; @@ -349,7 +337,6 @@ class VideocallChannelService with JCMediaChannelCallback { return _channel!.getMaxResolution(); } - // -- Messaging -- Future sendMessage( String type, String content, String toUserId) async { @@ -367,14 +354,12 @@ class VideocallChannelService with JCMediaChannelCallback { return _channel!.sendCommandToDelivery(command); } - // -- Statistics -- Future getStatistics() async { if (_channel == null) return ''; return _channel!.getStatistics(); } - // -- Volume change notify -- Future enableVolumeChangeNotify(bool value) async { if (_channel == null) return false; @@ -386,7 +371,6 @@ class VideocallChannelService with JCMediaChannelCallback { return _channel!.getVolumeChangeNotify(); } - // -- Dispose -- void dispose() { _stateChangeController.close(); @@ -403,7 +387,6 @@ class VideocallChannelService with JCMediaChannelCallback { _inviteSipUserResultController.close(); } - // -- JCMediaChannelCallback -- @override void onMediaChannelStateChange(int state, int oldState) => diff --git a/packages/videocall_sdk/lib/src/services/videocall_client.dart b/packages/videocall_sdk/lib/src/services/videocall_client.dart index 3d87f03f..3ecaf4cd 100644 --- a/packages/videocall_sdk/lib/src/services/videocall_client.dart +++ b/packages/videocall_sdk/lib/src/services/videocall_client.dart @@ -13,7 +13,6 @@ class VideocallClient with JCClientCallback { JCClient? _client; JCClient? get client => _client; - // -- Streams -- final _stateController = StreamController.broadcast(); Stream get stateStream => _stateController.stream; @@ -46,7 +45,6 @@ class VideocallClient with JCClientCallback { VideocallClientState _state = VideocallClientState.notInitialized; VideocallClientState get state => _state; - // -- Lifecycle -- Future initialize() async { try { @@ -75,7 +73,6 @@ class VideocallClient with JCClientCallback { return result; } - // -- Auth -- Future login({ required String userId, @@ -100,7 +97,6 @@ class VideocallClient with JCClientCallback { return _client!.logout(); } - // -- User info -- Future getUserId() async => _client?.getUserId(); @@ -118,7 +114,6 @@ class VideocallClient with JCClientCallback { return _client!.getServerUid(); } - // -- Server config -- Future setServerAddress(String serverAddress) async { if (_client == null) return false; @@ -130,14 +125,12 @@ class VideocallClient with JCClientCallback { return _client!.getServerAddress(); } - // -- Foreground/background -- Future setForeground(bool foreground) async { if (_client == null) return false; return _client!.setForeground(foreground); } - // -- Online messaging -- Future sendOnlineMessage({ required String userId, @@ -147,7 +140,6 @@ class VideocallClient with JCClientCallback { return _client!.sendOnlineMessage(userId, content); } - // -- Params -- Future getCreateParam() async => _client?.getCreateParam(); @@ -158,7 +150,6 @@ class VideocallClient with JCClientCallback { return _client!.getState(); } - // -- Dispose -- void dispose() { _stateController.close(); @@ -169,7 +160,6 @@ class VideocallClient with JCClientCallback { _serverMessageController.close(); } - // -- Internal -- void _updateState(VideocallClientState newState) { _state = newState; @@ -207,7 +197,6 @@ class VideocallClient with JCClientCallback { return LoginFailureReason.unknown; } - // -- JCClientCallback -- @override void onClientStateChange(int state, int oldState) { diff --git a/packages/videocall_sdk/lib/src/services/videocall_device_service.dart b/packages/videocall_sdk/lib/src/services/videocall_device_service.dart index b23c3997..ab006dfa 100644 --- a/packages/videocall_sdk/lib/src/services/videocall_device_service.dart +++ b/packages/videocall_sdk/lib/src/services/videocall_device_service.dart @@ -12,7 +12,6 @@ class VideocallDeviceService with JCMediaDeviceCallback { JCMediaDevice? _mediaDevice; JCMediaDevice? get mediaDevice => _mediaDevice; - // -- Streams -- final _cameraUpdateController = StreamController.broadcast(); Stream get cameraUpdateStream => _cameraUpdateController.stream; @@ -41,7 +40,6 @@ class VideocallDeviceService with JCMediaDeviceCallback { final _audioResumeController = StreamController.broadcast(); Stream get audioResumeStream => _audioResumeController.stream; - // -- Lifecycle -- Future initialize() async { final client = _clientRef.client; @@ -52,7 +50,6 @@ class VideocallDeviceService with JCMediaDeviceCallback { Future destroy() async => JCMediaDevice.destroy(); - // -- Camera -- Future isCameraOpen() async { if (_mediaDevice == null) return false; @@ -106,7 +103,6 @@ class VideocallDeviceService with JCMediaDeviceCallback { return _mediaDevice!.getCameraType(cameraIndex); } - // -- Exposure -- Future getMinExposureCompensation() async { if (_mediaDevice == null) return 0; @@ -133,7 +129,6 @@ class VideocallDeviceService with JCMediaDeviceCallback { return _mediaDevice!.setExposureCompensation(level); } - // -- Flash -- Future isCameraFlashSupported() async { if (_mediaDevice == null) return false; @@ -145,7 +140,6 @@ class VideocallDeviceService with JCMediaDeviceCallback { return _mediaDevice!.enableFlash(enable); } - // -- Focus/Zoom -- Future handleFocusMetering( JCMediaDeviceVideoCanvas canvas, double xPercent, double yPercent) async { @@ -168,7 +162,6 @@ class VideocallDeviceService with JCMediaDeviceCallback { return _mediaDevice!.getCameraCurrentZoom(); } - // -- Speaker -- Future isSpeakerOn() async { if (_mediaDevice == null) return false; @@ -195,7 +188,6 @@ class VideocallDeviceService with JCMediaDeviceCallback { return _mediaDevice!.getAudioRouteType(); } - // -- Audio -- Future isAudioStart() async { if (_mediaDevice == null) return false; @@ -245,7 +237,6 @@ class VideocallDeviceService with JCMediaDeviceCallback { Future getAudioParam() async => _mediaDevice?.getAudioParam(); - // -- Volume -- Future getOutputVolume() async { if (_mediaDevice == null) return 0; @@ -267,7 +258,6 @@ class VideocallDeviceService with JCMediaDeviceCallback { return _mediaDevice!.removeVolumeCallback(callback); } - // -- Video rendering -- Future startCameraVideo( {int renderType = JCMediaDevice.RENDER_FULL_AUTO}) async { @@ -284,7 +274,6 @@ class VideocallDeviceService with JCMediaDeviceCallback { return _mediaDevice!.stopVideo(canvas); } - // -- Video file (custom capture) -- Future isVideoFileOpen() async { if (_mediaDevice == null) return false; @@ -313,7 +302,6 @@ class VideocallDeviceService with JCMediaDeviceCallback { return _mediaDevice!.stopVideoFile(); } - // -- Video angle -- Future setVideoAngle(int angle) async { if (_mediaDevice == null) return false; @@ -325,7 +313,6 @@ class VideocallDeviceService with JCMediaDeviceCallback { return _mediaDevice!.getVideoAngle(); } - // -- Frame callbacks -- Future setAudioFrameCallback(JCAudioFrameCallback? callback) async { if (_mediaDevice == null) return false; @@ -337,7 +324,6 @@ class VideocallDeviceService with JCMediaDeviceCallback { return _mediaDevice!.setVideoFrameCallback(callback); } - // -- Custom audio -- Future inputCustomAudioData(int sampleRateHz, int channels, Uint8List byteBuffer, int playDelayMS, int recDelayMS, int clockDrift) async { @@ -350,7 +336,6 @@ class VideocallDeviceService with JCMediaDeviceCallback { return _mediaDevice?.getAudioOutputData(sampleRateHz, channels); } - // -- Dispose -- void dispose() { _cameraUpdateController.close(); @@ -362,7 +347,6 @@ class VideocallDeviceService with JCMediaDeviceCallback { _audioResumeController.close(); } - // -- JCMediaDeviceCallback -- @override void onCameraUpdate() => _cameraUpdateController.add(null); diff --git a/packages/videocall_sdk/lib/src/services/videocall_net_service.dart b/packages/videocall_sdk/lib/src/services/videocall_net_service.dart index dfb80335..dca2f32e 100644 --- a/packages/videocall_sdk/lib/src/services/videocall_net_service.dart +++ b/packages/videocall_sdk/lib/src/services/videocall_net_service.dart @@ -3,14 +3,12 @@ import 'dart:async'; import 'package:jc_sdk/jc_sdk.dart'; class VideocallNetService with JCNetCallback { - // -- Streams -- final _netChangeController = StreamController<({int newNetType, int oldNetType})>.broadcast(); Stream<({int newNetType, int oldNetType})> get netChangeStream => _netChangeController.stream; - // -- Lifecycle -- void initialize() { JCNet.getInstance().addCallback(this); @@ -20,20 +18,17 @@ class VideocallNetService with JCNetCallback { JCNet.getInstance().removeCallback(this); } - // -- Network info -- Future getNetType() async => JCNet.getInstance().getNetType(); Future hasNet() async => JCNet.getInstance().hasNet(); - // -- Dispose -- void dispose() { uninitialize(); _netChangeController.close(); } - // -- JCNetCallback -- @override void onNetChange(int newNetType, int oldNetType) { diff --git a/packages/videocall_sdk/lib/src/services/videocall_push_service.dart b/packages/videocall_sdk/lib/src/services/videocall_push_service.dart index d15a85db..6054e0d9 100644 --- a/packages/videocall_sdk/lib/src/services/videocall_push_service.dart +++ b/packages/videocall_sdk/lib/src/services/videocall_push_service.dart @@ -11,7 +11,6 @@ class VideocallPushService { JCPush? _push; JCPush? get push => _push; - // -- Lifecycle -- Future initialize() async { final client = _clientRef.client; @@ -25,7 +24,6 @@ class VideocallPushService { _push = null; } - // -- Push -- Future addPushInfo(JCPushTemplate info) async { if (_push == null) return false; @@ -37,7 +35,6 @@ class VideocallPushService { return _push!.addPushTemplate(data); } - // -- Dispose -- void dispose() { _push = null; diff --git a/packages/videocall_sdk/pubspec.yaml b/packages/videocall_sdk/pubspec.yaml index a4d30e18..bc9520d3 100644 --- a/packages/videocall_sdk/pubspec.yaml +++ b/packages/videocall_sdk/pubspec.yaml @@ -14,8 +14,11 @@ dependencies: jc_sdk: ^2.16.5 flutter_riverpod: ^3.0.3 get_it: ^9.0.5 + freezed_annotation: ^3.0.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0 + freezed: ^3.0.6 + build_runner: ^2.4.15