feat(videocall): complete signaling integration, group call support, and UI polish

- Wire VIDEO_CALL_REQUEST/CANCEL/REFUSE/ROOM_COUNT commands via CommandsRepository
- Add VideocallChatType enum (single/multi) with chatType stored in state
- Implement auto-login to Juphoon SDK using sanitized email + user UUID
- Add runtime camera/microphone permissions before call start
- Add RetryInterceptor for transient TLS/socket errors in Dio
- Migrate VideocallItem to Freezed with isTalking extension
- Implement startGroupCall/leaveGroupCall using ChannelService with participant grid
- Add PopScope to intercept back navigation during active calls
- Redesign idle screen with device option cards and group call button
- Redesign active call UI with video overlay, PiP local view, and new controls layout
- Clean up SDK wrapper: remove unused streams, merge destroy+dispose into shutdown
- Add i18n keys for videocall UI across 6 locales
This commit is contained in:
2026-04-26 21:52:00 +02:00
parent 5aa0c0acc7
commit 555a668481
40 changed files with 1344 additions and 530 deletions

View File

@@ -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_<IMEI>")`)
4. Reloj contesta → SDK notifica via `callItemUpdateStream` (estado `isTalking`)
5. App envía `VIDEO_CALL_ROOM_COUNT_REQUEST` con `type` (0/1), `count: 2`, `room_num`
### Reloj → APP (incoming)
1. Reloj envía notificación de llamada al backend
2. Backend notifica a la app (requiere app abierta con SDK inicializado, ver Fase 9)
3. SDK detecta llamada entrante via `callItemAddStream` con `CallDirection.incoming`
4. Usuario acepta → `answerCall()` → SDK conecta
5. Reloj reporta participantes al backend
### Colgar / Rechazar
- **Colgar (en llamada):** `hangUp()` en SDK + `VIDEO_CALL_CANCEL` al backend
- **Rechazar (incoming):** `hangUp()` en SDK + `VIDEO_CALL_REFUSE` al backend con `appAccount`, `roomNumber`
### Convenciones de nombres
| Campo | Formato | Ejemplo |
|---|---|---|
| Watch userID | `w_` + IMEI | `w_000078932675810` |
| Mobile userID | `p_` + email sanitizado | `p_user_example_com` |
| Room (single) | `deviceId` + `_` + appAccount | `0245423235_p_user_example_com` |
| Room (group) | `deviceId` + `_group` | `0245423235_group` |
| Session ID | `deviceId` + `_` + epoch en segundos | `0245423235_1714150800` |
Sanitización: `@` y `.` se reemplazan por `_` en userIDs y roomNumbers.
### Configuración del SDK por tipo de dispositivo
- RTOS watches: `MediaConfig.MODE_RTOS`
- Android watches: `MediaConfig.MODE_INTELLIGENT_HARDWARE`
- Se determina con `device.capabilities.system` (`isRtos` / `isAndroid`)
### Auto-login
- userId: `p_` + email sanitizado (ej: `p_julian_test_com`)
- password: `user.id` (UUID del usuario padre)
- En dev/testing `autoCreateAccount = true` permite login con cualquier password
---
## Limitaciones actuales
### Recepción de llamadas requiere app abierta
La app debe estar en primer plano con el SDK inicializado y el client logueado para recibir llamadas entrantes. Si el app está en background o cerrada, las llamadas no llegan. Esto se resuelve en Fase 9 (Push/Background).
### Sin timeout de llamada
El protocolo menciona un límite de 5 min por llamada, pero no está implementado en la app. El reloj podría manejar el corte por su lado.
---
## Pendientes por verificar
- **chatType**: El protocolo TCP usa `0` (single) y `1` (multi) como enteros. Nuestra app envía `"single"`/`"multi"` como strings en el JSON del comando. Verificar que el backend hace la conversión correctamente antes de enviar al reloj via TCP.

View File

@@ -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: () =>

View File

@@ -1,22 +1,30 @@
import '../../domain/entities/videocall_chat_type.dart';
abstract class VideocallSignalingDatasource {
Future<void> initiateCall({
required String deviceId,
required String deviceIdentificator,
required VideocallChatType chatType,
required String appAccount,
required String roomNumber,
required String sessionId,
});
Future<void> cancelCall({
required String deviceIdentificator,
required VideocallChatType chatType,
});
Future<void> refuseCall({
required String deviceIdentificator,
required VideocallChatType chatType,
required String appAccount,
required String roomNumber,
});
Future<void> cancelCall({required String deviceId});
Future<void> refuseCall({
required String deviceId,
required String roomNumber,
});
Future<int> getRoomParticipantCount({required String roomNumber});
Future<void> reportParticipantCount({
required String roomNumber,
Future<void> reportRoomCount({
required String deviceIdentificator,
required VideocallChatType chatType,
required int count,
required int type,
required String roomNumber,
});
}

View File

@@ -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<void> 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<void> cancelCall({required String deviceId}) async {
throw UnimplementedError(
'Backend signaling API not yet available. Waiting for endpoint spec.');
Future<void> cancelCall({
required String deviceIdentificator,
required VideocallChatType chatType,
}) async {
await _commandsRepository.send(
request: SendCommandRequestModel(
device: deviceIdentificator,
command: DeviceCommand.videoCallCancel,
data: {'chatType': chatType.value},
),
);
}
@override
Future<void> 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<int> getRoomParticipantCount({required String roomNumber}) async {
throw UnimplementedError(
'Backend signaling API not yet available. Waiting for endpoint spec.');
}
@override
Future<void> reportParticipantCount({
required String roomNumber,
Future<void> 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,
},
),
);
}
}

View File

@@ -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<void> 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<void> cancelCall({
required String deviceIdentificator,
required VideocallChatType chatType,
}) {
return _datasource.cancelCall(
deviceIdentificator: deviceIdentificator,
chatType: chatType,
);
}
@override
Future<void> 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<void> cancelCall({required String deviceId}) {
return _datasource.cancelCall(deviceId: deviceId);
}
@override
Future<void> refuseCall({
required String deviceId,
required String roomNumber,
}) {
return _datasource.refuseCall(deviceId: deviceId, roomNumber: roomNumber);
}
@override
Future<int> getRoomParticipantCount({required String roomNumber}) {
return _datasource.getRoomParticipantCount(roomNumber: roomNumber);
}
@override
Future<void> reportParticipantCount({
required String roomNumber,
Future<void> 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,
);
}
}

View File

@@ -0,0 +1,6 @@
enum VideocallChatType {
single,
multi;
String get value => name;
}

View File

@@ -20,4 +20,5 @@ enum VideocallScreenMode {
outgoing,
incoming,
inCall,
groupCall,
}

View File

@@ -1,22 +1,30 @@
import '../entities/videocall_chat_type.dart';
abstract class VideocallSignalingRepository {
Future<void> initiateCall({
required String deviceId,
required String deviceIdentificator,
required VideocallChatType chatType,
required String appAccount,
required String roomNumber,
required String sessionId,
});
Future<void> cancelCall({
required String deviceIdentificator,
required VideocallChatType chatType,
});
Future<void> refuseCall({
required String deviceIdentificator,
required VideocallChatType chatType,
required String appAccount,
required String roomNumber,
});
Future<void> cancelCall({required String deviceId});
Future<void> refuseCall({
required String deviceId,
required String roomNumber,
});
Future<int> getRoomParticipantCount({required String roomNumber});
Future<void> reportParticipantCount({
required String roomNumber,
Future<void> reportRoomCount({
required String deviceIdentificator,
required VideocallChatType chatType,
required int count,
required int type,
required String roomNumber,
});
}

View File

@@ -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) {

View File

@@ -42,7 +42,7 @@ final class GroupCallControllerProvider
}
String _$groupCallControllerHash() =>
r'0d3c5bd234ef3ed76b0b0f7666ddb73c8b98be55';
r'e0c417dc7b6669fb801b17d8b04ed6dc814b42f3';
abstract class _$GroupCallController extends $Notifier<GroupCallState> {
GroupCallState build();

View File

@@ -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<VideocallItem>? _callAddSub;
StreamSubscription<VideocallItem>? _callUpdateSub;
StreamSubscription<({int reason, String description})>? _callRemoveSub;
StreamSubscription<VideocallItem>? _missedCallSub;
StreamSubscription<VideocallClientState>? _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<void> _initSdk() async {
if (_manager.isInitialized) {
_subscribeToStreams();
await _configureDevice();
if (!ref.mounted) return;
state = state.copyWith(isSdkReady: true);
return;
}
debugPrint('[Videocall] _initSdk: isInitialized=${_manager.isInitialized}');
if (!_manager.isInitialized) {
final ok = await _manager.initialize();
debugPrint('[Videocall] _initSdk: initialize result=$ok');
if (!ref.mounted) return;
if (!ok) {
state = state.copyWith(errorEvent: VideocallErrorEvent.sdkInitialization);
state = state.copyWith(
errorEvent: VideocallErrorEvent.sdkInitialization,
);
return;
}
}
_subscribeToStreams();
await _configureDevice();
if (!ref.mounted) return;
state = state.copyWith(isSdkReady: true);
await _autoLogin();
}
Future<void> _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<void> _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<bool> 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<bool> _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<void> 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<void> 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<void> 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<void> 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<void> 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<void> _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();
}
}
}

View File

@@ -42,7 +42,7 @@ final class VideocallControllerProvider
}
String _$videocallControllerHash() =>
r'910674a27ecef98e5df917d193f97fca1c60ac02';
r'46fef79b569fea891291d092f187d30f191719ae';
abstract class _$VideocallController extends $Notifier<VideocallState> {
VideocallState build();

View File

@@ -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,

View File

@@ -14,7 +14,7 @@ T _$identity<T>(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<VideocallState> 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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

View File

@@ -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<VideocallScreen> {
final _userIdController = TextEditingController();
@override
void dispose() {
_userIdController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final state = ref.watch(videocallControllerProvider);
@@ -61,131 +56,97 @@ class _VideocallScreenState extends ConsumerState<VideocallScreen> {
(_, 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),
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.inCall => _InCallView(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<double>(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: 24),
const SizedBox(height: 8),
Text(
context.translate(I18n.videocallTitle),
deviceName,
style: TextStyle(
color: colorScheme.onSurface,
fontSize: 24,
fontWeight: FontWeight.bold,
fontSize: SizeUtils.getByScreen(small: 14, big: 13),
color: colorScheme.onSurface.withValues(alpha: 0.6),
),
),
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),
const SizedBox(height: 12),
_VideocallOptionCard(
image: Image.asset(
'assets/shared/images/iso_sf.png',
width: SizeUtils.getByScreen<double>(small: 48, big: 44),
height: SizeUtils.getByScreen<double>(small: 48, big: 44),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
text: deviceName,
onTap: () => vm.startCall(''),
),
const SizedBox(height: 12),
_VideocallOptionCard(
icon: Icon(
Icons.groups_outlined,
size: SizeUtils.getByScreen<double>(small: 36, big: 32),
color: context.sfColors.legacyPrimary,
),
borderRadius: BorderRadius.circular(12),
),
),
text: context.translate(I18n.videocallGroupCall),
onTap: () => vm.startGroupCall(),
),
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),
context.translate(I18n.videocallDisclaimer),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 12, big: 11),
color: colorScheme.onSurface.withValues(alpha: 0.5),
),
),
],
),
@@ -193,20 +154,162 @@ class _IdleView extends StatelessWidget {
}
}
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<double>(small: 16, big: 15),
vertical: SizeUtils.getByScreen<double>(small: 14, big: 13),
),
child: Row(
children: [
SizedBox(
width: SizeUtils.getByScreen<double>(small: 48, big: 44),
height: SizeUtils.getByScreen<double>(small: 48, big: 44),
child: Center(child: image ?? icon),
),
SizedBox(width: SizeUtils.getByScreen<double>(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,
),
),
],
);
}
}
@@ -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,
),
isMicEnabled: groupState.isMicEnabled,
isSpeakerEnabled: true,
isFrontCamera: true,
onToggleMic: groupVm.toggleMic,
onToggleSpeaker: () {},
onSwitchCamera: groupVm.switchCamera,
onHangUp: vm.leaveGroupCall,
),
),
],
);
}
}

View File

@@ -23,40 +23,53 @@ class CallControlsWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
return SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_ControlButton(
icon: isMicEnabled ? Icons.mic : Icons.mic_off,
label: context.translate(isMicEnabled ? I18n.videocallMicOn : I18n.videocallMicOff),
isActive: isMicEnabled,
icon: isMicEnabled ? Icons.mic_none : Icons.mic_off,
label: context.translate(I18n.videocallMute),
isActive: !isMicEnabled,
onPressed: onToggleMic,
),
_ControlButton(
icon: isSpeakerEnabled
? Icons.volume_up
: Icons.volume_off,
label: context.translate(isSpeakerEnabled ? I18n.videocallSpeaker : I18n.videocallEarpiece),
icon: isSpeakerEnabled ? Icons.volume_up : Icons.hearing,
label: context.translate(I18n.videocallSpeakerphone),
isActive: isSpeakerEnabled,
onPressed: onToggleSpeaker,
),
_ControlButton(
icon: Icons.cameraswitch,
label: context.translate(isFrontCamera ? I18n.videocallCameraFront : I18n.videocallCameraBack),
icon: Icons.videocam,
label: context.translate(I18n.videocallCameraOff),
isActive: true,
onPressed: onSwitchCamera,
),
_ControlButton(
icon: Icons.call_end,
label: context.translate(I18n.videocallHangUp),
isActive: false,
isDestructive: true,
onPressed: onHangUp,
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),
),
),
);
}
}

View File

@@ -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<VideocallSignalingDatasource>((ref) {
final repository = GetIt.I<SaveFamilyRepository>();
return VideocallSignalingDatasourceImpl(repository);
return VideocallSignalingDatasourceImpl(
ref.read(commandsRepositoryProvider),
);
});

View File

@@ -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.

View File

@@ -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

View File

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

View File

@@ -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<Dio> buildDioClient({
required String baseUrl,
required String origin,
@@ -25,6 +27,7 @@ Future<Dio> buildDioClient({
final jar = cookieJar ?? await buildPersistCookieJar();
dio.interceptors.add(CookieManager(jar));
dio.interceptors.add(RetryInterceptor(dio));
if (log) {
dio.interceptors.add(

View File

@@ -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<void> _retry(DioException err, ErrorInterceptorHandler handler, int attempt) async {
await Future<void>.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);
}
}
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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';

View File

@@ -49,10 +49,9 @@ class VideocallSdkManager {
return true;
}
Future<void> destroy() async {
Future<void> 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;
}
}

View File

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

View File

@@ -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>(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<VideocallItem> get copyWith => _$VideocallItemCopyWithImpl<VideocallItem>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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

View File

@@ -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<VideocallSdkManager>((ref) {
return GetIt.I<VideocallSdkManager>();
});
@@ -42,8 +40,6 @@ final videocallNetServiceProvider = Provider<VideocallNetService>((ref) {
return GetIt.I<VideocallNetService>();
});
// -- Stream providers (for reactive UI consumption) --
final videocallClientStateProvider =
StreamProvider<VideocallClientState>((ref) {
return ref.watch(videocallClientProvider).stateStream;

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:jc_sdk/jc_sdk.dart';
import '../models/call_direction.dart';
@@ -20,8 +21,6 @@ class VideocallCallService with JCCallCallback {
JCCall? _call;
JCCall? get call => _call;
// -- Streams --
final _callItemAddController = StreamController<VideocallItem>.broadcast();
Stream<VideocallItem> get callItemAddStream => _callItemAddController.stream;
@@ -37,25 +36,9 @@ class VideocallCallService with JCCallCallback {
final _missedCallController = StreamController<VideocallItem>.broadcast();
Stream<VideocallItem> 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<JCCallItem>.broadcast();
Stream<JCCallItem> get earlyMediaStream => _earlyMediaController.stream;
VideocallItem? _currentItem;
VideocallItem? get currentItem => _currentItem;
// -- Lifecycle --
Future<bool> initialize() async {
final client = _clientRef.client;
final mediaDevice = _deviceRef.mediaDevice;
@@ -66,8 +49,6 @@ class VideocallCallService with JCCallCallback {
Future<bool> destroy() async => JCCall.destroy();
// -- Call actions --
Future<bool> 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<bool> termCall(
JCCallItem item, int reason, String description) async {
Future<bool> termCall(JCCallItem item, int reason, String description) async {
if (_call == null) return false;
return _call!.term(item, reason, description);
}
// -- Mute/Audio --
Future<bool> 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<bool> 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<bool> enableUploadVideoStream(JCCallItem item) async {
if (_call == null) return false;
return _call!.enableUploadVideoStream(item);
}
Future<JCMediaDeviceVideoCanvas?> startLocalVideo(
{int renderType = JCMediaDevice.RENDER_FULL_AUTO}) async {
Future<JCMediaDeviceVideoCanvas?> 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<JCMediaDeviceVideoCanvas?> startRemoteVideo(
{int renderType = JCMediaDevice.RENDER_FULL_CONTENT}) async {
Future<JCMediaDeviceVideoCanvas?> 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<bool> 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<bool> sendMessage(
JCCallItem item, String type, String content) async {
Future<bool> 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<List<JCCallItem>?> getCallItems() async => _call?.getCallItems();
Future<JCCallItem?> getActiveCallItem() async =>
_call?.getActiveCallItem();
// -- Config --
Future<JCCallItem?> getActiveCallItem() async => _call?.getActiveCallItem();
Future<String> getStatistics() async {
if (_call == null) return '';
@@ -232,20 +209,13 @@ class VideocallCallService with JCCallCallback {
static Future<MediaConfig> 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) {}

View File

@@ -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<bool> initialize() async {
final client = _clientRef.client;
@@ -109,7 +107,6 @@ class VideocallChannelService with JCMediaChannelCallback {
Future<bool> destroy() async => JCMediaChannel.destroy();
// -- Channel actions --
Future<bool> 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<bool> 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<bool> enableScreenShare(
bool enable, ScreenShareParam? screenShareParam) async {
@@ -177,7 +172,6 @@ class VideocallChannelService with JCMediaChannelCallback {
return _channel!.enableRecord(enable, recordParam);
}
// -- Participants --
Future<List<JCMediaChannelParticipant>?> getParticipants() async =>
_channel?.getParticipants();
@@ -195,7 +189,6 @@ class VideocallChannelService with JCMediaChannelCallback {
return _channel!.inviteSipUser(userId, sipParam);
}
// -- Custom roles/state --
Future<bool> setCustomRole(int customRole,
{JCMediaChannelParticipant? participant}) async {
@@ -219,7 +212,6 @@ class VideocallChannelService with JCMediaChannelCallback {
return _channel!.getCustomState();
}
// -- Channel properties --
Future<int> setCustomProperty(String property) async {
if (_channel == null) return -1;
@@ -231,7 +223,6 @@ class VideocallChannelService with JCMediaChannelCallback {
return _channel!.getCustomProperty();
}
// -- Channel info --
Future<String> getChannelUri() async {
if (_channel == null) return '';
@@ -313,7 +304,6 @@ class VideocallChannelService with JCMediaChannelCallback {
return _channel!.getScreenUserId();
}
// -- Self participant --
Future<JCMediaChannelParticipant?> getSelfParticipant() async {
return _channel?.getSelfParticipant();
@@ -325,7 +315,6 @@ class VideocallChannelService with JCMediaChannelCallback {
return _channel!.subscribeParticipantAudio(participant, subscribe);
}
// -- Screen share video --
Future<JCMediaDeviceVideoCanvas?> startScreenShareVideo(
int renderType, int pictureSize) async {
@@ -337,7 +326,6 @@ class VideocallChannelService with JCMediaChannelCallback {
return _channel!.stopScreenShareVideo();
}
// -- Video ratio / resolution --
Future<bool> enableSelfVideoRatio(bool enable, double ratio) async {
if (_channel == null) return false;
@@ -349,7 +337,6 @@ class VideocallChannelService with JCMediaChannelCallback {
return _channel!.getMaxResolution();
}
// -- Messaging --
Future<bool> sendMessage(
String type, String content, String toUserId) async {
@@ -367,14 +354,12 @@ class VideocallChannelService with JCMediaChannelCallback {
return _channel!.sendCommandToDelivery(command);
}
// -- Statistics --
Future<String> getStatistics() async {
if (_channel == null) return '';
return _channel!.getStatistics();
}
// -- Volume change notify --
Future<bool> 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) =>

View File

@@ -13,7 +13,6 @@ class VideocallClient with JCClientCallback {
JCClient? _client;
JCClient? get client => _client;
// -- Streams --
final _stateController = StreamController<VideocallClientState>.broadcast();
Stream<VideocallClientState> get stateStream => _stateController.stream;
@@ -46,7 +45,6 @@ class VideocallClient with JCClientCallback {
VideocallClientState _state = VideocallClientState.notInitialized;
VideocallClientState get state => _state;
// -- Lifecycle --
Future<bool> initialize() async {
try {
@@ -75,7 +73,6 @@ class VideocallClient with JCClientCallback {
return result;
}
// -- Auth --
Future<bool> login({
required String userId,
@@ -100,7 +97,6 @@ class VideocallClient with JCClientCallback {
return _client!.logout();
}
// -- User info --
Future<String?> getUserId() async => _client?.getUserId();
@@ -118,7 +114,6 @@ class VideocallClient with JCClientCallback {
return _client!.getServerUid();
}
// -- Server config --
Future<bool> setServerAddress(String serverAddress) async {
if (_client == null) return false;
@@ -130,14 +125,12 @@ class VideocallClient with JCClientCallback {
return _client!.getServerAddress();
}
// -- Foreground/background --
Future<bool> setForeground(bool foreground) async {
if (_client == null) return false;
return _client!.setForeground(foreground);
}
// -- Online messaging --
Future<int> sendOnlineMessage({
required String userId,
@@ -147,7 +140,6 @@ class VideocallClient with JCClientCallback {
return _client!.sendOnlineMessage(userId, content);
}
// -- Params --
Future<CreateParam?> 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) {

View File

@@ -12,7 +12,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
JCMediaDevice? _mediaDevice;
JCMediaDevice? get mediaDevice => _mediaDevice;
// -- Streams --
final _cameraUpdateController = StreamController<void>.broadcast();
Stream<void> get cameraUpdateStream => _cameraUpdateController.stream;
@@ -41,7 +40,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
final _audioResumeController = StreamController<void>.broadcast();
Stream<void> get audioResumeStream => _audioResumeController.stream;
// -- Lifecycle --
Future<bool> initialize() async {
final client = _clientRef.client;
@@ -52,7 +50,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
Future<bool> destroy() async => JCMediaDevice.destroy();
// -- Camera --
Future<bool> isCameraOpen() async {
if (_mediaDevice == null) return false;
@@ -106,7 +103,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
return _mediaDevice!.getCameraType(cameraIndex);
}
// -- Exposure --
Future<int> getMinExposureCompensation() async {
if (_mediaDevice == null) return 0;
@@ -133,7 +129,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
return _mediaDevice!.setExposureCompensation(level);
}
// -- Flash --
Future<bool> isCameraFlashSupported() async {
if (_mediaDevice == null) return false;
@@ -145,7 +140,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
return _mediaDevice!.enableFlash(enable);
}
// -- Focus/Zoom --
Future<bool> handleFocusMetering(
JCMediaDeviceVideoCanvas canvas, double xPercent, double yPercent) async {
@@ -168,7 +162,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
return _mediaDevice!.getCameraCurrentZoom();
}
// -- Speaker --
Future<bool> isSpeakerOn() async {
if (_mediaDevice == null) return false;
@@ -195,7 +188,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
return _mediaDevice!.getAudioRouteType();
}
// -- Audio --
Future<bool> isAudioStart() async {
if (_mediaDevice == null) return false;
@@ -245,7 +237,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
Future<JCMediaDeviceAudioParam?> getAudioParam() async =>
_mediaDevice?.getAudioParam();
// -- Volume --
Future<int> getOutputVolume() async {
if (_mediaDevice == null) return 0;
@@ -267,7 +258,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
return _mediaDevice!.removeVolumeCallback(callback);
}
// -- Video rendering --
Future<JCMediaDeviceVideoCanvas?> 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<bool> isVideoFileOpen() async {
if (_mediaDevice == null) return false;
@@ -313,7 +302,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
return _mediaDevice!.stopVideoFile();
}
// -- Video angle --
Future<bool> setVideoAngle(int angle) async {
if (_mediaDevice == null) return false;
@@ -325,7 +313,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
return _mediaDevice!.getVideoAngle();
}
// -- Frame callbacks --
Future<bool> setAudioFrameCallback(JCAudioFrameCallback? callback) async {
if (_mediaDevice == null) return false;
@@ -337,7 +324,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
return _mediaDevice!.setVideoFrameCallback(callback);
}
// -- Custom audio --
Future<bool> 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);

View File

@@ -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<int> getNetType() async => JCNet.getInstance().getNetType();
Future<bool> hasNet() async => JCNet.getInstance().hasNet();
// -- Dispose --
void dispose() {
uninitialize();
_netChangeController.close();
}
// -- JCNetCallback --
@override
void onNetChange(int newNetType, int oldNetType) {

View File

@@ -11,7 +11,6 @@ class VideocallPushService {
JCPush? _push;
JCPush? get push => _push;
// -- Lifecycle --
Future<bool> initialize() async {
final client = _clientRef.client;
@@ -25,7 +24,6 @@ class VideocallPushService {
_push = null;
}
// -- Push --
Future<bool> addPushInfo(JCPushTemplate info) async {
if (_push == null) return false;
@@ -37,7 +35,6 @@ class VideocallPushService {
return _push!.addPushTemplate(data);
}
// -- Dispose --
void dispose() {
_push = null;

View File

@@ -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