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:
@@ -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.
|
||||
|
||||
@@ -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: () =>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
enum VideocallChatType {
|
||||
single,
|
||||
multi;
|
||||
|
||||
String get value => name;
|
||||
}
|
||||
@@ -20,4 +20,5 @@ enum VideocallScreenMode {
|
||||
outgoing,
|
||||
incoming,
|
||||
inCall,
|
||||
groupCall,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -42,7 +42,7 @@ final class GroupCallControllerProvider
|
||||
}
|
||||
|
||||
String _$groupCallControllerHash() =>
|
||||
r'0d3c5bd234ef3ed76b0b0f7666ddb73c8b98be55';
|
||||
r'e0c417dc7b6669fb801b17d8b04ed6dc814b42f3';
|
||||
|
||||
abstract class _$GroupCallController extends $Notifier<GroupCallState> {
|
||||
GroupCallState build();
|
||||
|
||||
@@ -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();
|
||||
debugPrint('[Videocall] _initSdk: isInitialized=${_manager.isInitialized}');
|
||||
if (!_manager.isInitialized) {
|
||||
final ok = await _manager.initialize();
|
||||
debugPrint('[Videocall] _initSdk: initialize result=$ok');
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isSdkReady: true);
|
||||
return;
|
||||
}
|
||||
|
||||
final ok = await _manager.initialize();
|
||||
if (!ref.mounted) return;
|
||||
|
||||
if (!ok) {
|
||||
state = state.copyWith(errorEvent: VideocallErrorEvent.sdkInitialization);
|
||||
return;
|
||||
if (!ok) {
|
||||
state = state.copyWith(
|
||||
errorEvent: VideocallErrorEvent.sdkInitialization,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_subscribeToStreams();
|
||||
await _configureDevice();
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isSdkReady: true);
|
||||
await _autoLogin();
|
||||
}
|
||||
|
||||
Future<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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ final class VideocallControllerProvider
|
||||
}
|
||||
|
||||
String _$videocallControllerHash() =>
|
||||
r'910674a27ecef98e5df917d193f97fca1c60ac02';
|
||||
r'46fef79b569fea891291d092f187d30f191719ae';
|
||||
|
||||
abstract class _$VideocallController extends $Notifier<VideocallState> {
|
||||
VideocallState build();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,156 +56,264 @@ 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),
|
||||
VideocallScreenMode.incoming => _IncomingView(state: state, vm: vm),
|
||||
VideocallScreenMode.inCall => _InCallView(state: state, vm: vm),
|
||||
},
|
||||
final device = ref.watch(selectedDeviceProvider).value;
|
||||
final deviceName = device?.carrierName ?? '';
|
||||
|
||||
final isInActiveCall = state.screenMode != VideocallScreenMode.idle;
|
||||
|
||||
final body = switch (state.screenMode) {
|
||||
VideocallScreenMode.idle => !state.isSdkReady
|
||||
? const LegacyLoadingIndicator()
|
||||
: _IdleBody(deviceName: deviceName, vm: vm),
|
||||
VideocallScreenMode.outgoing || VideocallScreenMode.inCall =>
|
||||
_ActiveCallView(state: state, vm: vm, deviceName: deviceName),
|
||||
VideocallScreenMode.incoming => _IncomingView(state: state, vm: vm),
|
||||
VideocallScreenMode.groupCall => _GroupCallView(vm: vm),
|
||||
};
|
||||
|
||||
return PopScope(
|
||||
canPop: !isInActiveCall,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) {
|
||||
if (state.screenMode == VideocallScreenMode.groupCall) {
|
||||
vm.leaveGroupCall();
|
||||
} else {
|
||||
vm.hangUp();
|
||||
}
|
||||
}
|
||||
},
|
||||
child: LegacyPageLayout(
|
||||
title: context.translate(I18n.videocallTitle),
|
||||
body: body,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IdleView extends StatelessWidget {
|
||||
const _IdleView({
|
||||
required this.state,
|
||||
required this.controller,
|
||||
required this.vm,
|
||||
});
|
||||
class _IdleBody extends StatelessWidget {
|
||||
const _IdleBody({required this.deviceName, required this.vm});
|
||||
|
||||
final VideocallState state;
|
||||
final TextEditingController controller;
|
||||
final String deviceName;
|
||||
final VideocallController vm;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
if (!state.isSdkReady) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(color: colorScheme.onSurface),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
context.translate(I18n.videocallInitializingSdk),
|
||||
style: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.7)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
final horizontalPadding = SizeUtils.getByScreen<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: 8),
|
||||
Text(
|
||||
deviceName,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 14, big: 13),
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_VideocallOptionCard(
|
||||
image: Image.asset(
|
||||
'assets/shared/images/iso_sf.png',
|
||||
width: SizeUtils.getByScreen<double>(small: 48, big: 44),
|
||||
height: SizeUtils.getByScreen<double>(small: 48, big: 44),
|
||||
),
|
||||
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,
|
||||
),
|
||||
text: context.translate(I18n.videocallGroupCall),
|
||||
onTap: () => vm.startGroupCall(),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
context.translate(I18n.videocallTitle),
|
||||
context.translate(I18n.videocallDisclaimer),
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: SizeUtils.getByScreen(small: 12, big: 11),
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
TextField(
|
||||
controller: controller,
|
||||
style: TextStyle(color: colorScheme.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: context.translate(I18n.videocallRecipientUserId),
|
||||
hintStyle: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.38)),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: colorScheme.onSurface.withValues(alpha: 0.24)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: context.sfColors.legacyPrimary,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
final userId = controller.text.trim();
|
||||
if (userId.isEmpty) return;
|
||||
vm.startCall(userId);
|
||||
},
|
||||
icon: const Icon(Icons.videocam),
|
||||
label: Text(context.translate(I18n.videocallStart)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: context.sfColors.legacyPrimary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (kDebugMode && state.localUserId.isEmpty)
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await vm.login(userId: 'p_test1', password: 'test123');
|
||||
},
|
||||
child: Text(
|
||||
'Login como p_test1 (testing)',
|
||||
style: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.38)),
|
||||
),
|
||||
),
|
||||
if (state.localUserId.isNotEmpty)
|
||||
Text(
|
||||
context.translate(I18n.videocallLoggedInAs, args: {'userId': state.localUserId}),
|
||||
style: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.38), fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OutgoingView extends StatelessWidget {
|
||||
const _OutgoingView({required this.state, required this.vm});
|
||||
class _VideocallOptionCard extends StatelessWidget {
|
||||
const _VideocallOptionCard({
|
||||
this.image,
|
||||
this.icon,
|
||||
required this.text,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final VideocallState state;
|
||||
final VideocallController vm;
|
||||
final Widget? image;
|
||||
final Widget? icon;
|
||||
final String text;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: CallStatusIndicator(
|
||||
screenMode: state.screenMode,
|
||||
remoteUserId: state.remoteUserId,
|
||||
onCancel: vm.hangUp,
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Material(
|
||||
color: colorScheme.surfaceContainerLowest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
elevation: 0.5,
|
||||
shadowColor: colorScheme.shadow.withValues(alpha: 0.3),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen<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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IncomingView extends StatelessWidget {
|
||||
const _IncomingView({required this.state, required this.vm});
|
||||
|
||||
@@ -228,83 +331,59 @@ class _IncomingView extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _InCallView extends StatelessWidget {
|
||||
const _InCallView({required this.state, required this.vm});
|
||||
class _GroupCallView extends ConsumerWidget {
|
||||
const _GroupCallView({required this.vm});
|
||||
|
||||
final VideocallState state;
|
||||
final VideocallController vm;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final groupState = ref.watch(groupCallControllerProvider);
|
||||
final groupVm = ref.read(groupCallControllerProvider.notifier);
|
||||
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (state.remoteCanvas != null)
|
||||
VideoViewWidget(canvas: state.remoteCanvas!)
|
||||
else
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.person, color: colorScheme.onSurface.withValues(alpha: 0.24), size: 96),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
context.translate(I18n.videocallWaitingRemoteVideo),
|
||||
style: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.38)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (state.localCanvas != null)
|
||||
const ColoredBox(color: Colors.black),
|
||||
ParticipantGridWidget(participants: groupState.participants),
|
||||
if (groupState.localCanvas != null)
|
||||
Positioned(
|
||||
top: 16,
|
||||
top: 8,
|
||||
right: 16,
|
||||
child: Container(
|
||||
width: 120,
|
||||
height: 160,
|
||||
width: 100,
|
||||
height: 140,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: colorScheme.onSurface.withValues(alpha: 0.24), width: 1),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: VideoViewWidget(
|
||||
canvas: state.localCanvas!,
|
||||
width: 120,
|
||||
height: 160,
|
||||
canvas: groupState.localCanvas!,
|
||||
width: 100,
|
||||
height: 140,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
colorScheme.surface.withValues(alpha: 0.8),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: CallControlsWidget(
|
||||
isMicEnabled: state.isMicEnabled,
|
||||
isSpeakerEnabled: state.isSpeakerEnabled,
|
||||
isFrontCamera: state.isFrontCamera,
|
||||
onToggleMic: vm.toggleMic,
|
||||
onToggleSpeaker: vm.toggleSpeaker,
|
||||
onSwitchCamera: vm.switchCamera,
|
||||
onHangUp: vm.hangUp,
|
||||
),
|
||||
child: CallControlsWidget(
|
||||
isMicEnabled: groupState.isMicEnabled,
|
||||
isSpeakerEnabled: true,
|
||||
isFrontCamera: true,
|
||||
onToggleMic: groupVm.toggleMic,
|
||||
onToggleSpeaker: () {},
|
||||
onSwitchCamera: groupVm.switchCamera,
|
||||
onHangUp: vm.leaveGroupCall,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,39 +23,52 @@ class CallControlsWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_ControlButton(
|
||||
icon: isMicEnabled ? Icons.mic : Icons.mic_off,
|
||||
label: context.translate(isMicEnabled ? I18n.videocallMicOn : I18n.videocallMicOff),
|
||||
isActive: isMicEnabled,
|
||||
onPressed: onToggleMic,
|
||||
),
|
||||
_ControlButton(
|
||||
icon: isSpeakerEnabled
|
||||
? Icons.volume_up
|
||||
: Icons.volume_off,
|
||||
label: context.translate(isSpeakerEnabled ? I18n.videocallSpeaker : I18n.videocallEarpiece),
|
||||
isActive: isSpeakerEnabled,
|
||||
onPressed: onToggleSpeaker,
|
||||
),
|
||||
_ControlButton(
|
||||
icon: Icons.cameraswitch,
|
||||
label: context.translate(isFrontCamera ? I18n.videocallCameraFront : I18n.videocallCameraBack),
|
||||
isActive: true,
|
||||
onPressed: onSwitchCamera,
|
||||
),
|
||||
_ControlButton(
|
||||
icon: Icons.call_end,
|
||||
label: context.translate(I18n.videocallHangUp),
|
||||
isActive: false,
|
||||
isDestructive: true,
|
||||
onPressed: onHangUp,
|
||||
),
|
||||
],
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_ControlButton(
|
||||
icon: isMicEnabled ? Icons.mic_none : Icons.mic_off,
|
||||
label: context.translate(I18n.videocallMute),
|
||||
isActive: !isMicEnabled,
|
||||
onPressed: onToggleMic,
|
||||
),
|
||||
_ControlButton(
|
||||
icon: isSpeakerEnabled ? Icons.volume_up : Icons.hearing,
|
||||
label: context.translate(I18n.videocallSpeakerphone),
|
||||
isActive: isSpeakerEnabled,
|
||||
onPressed: onToggleSpeaker,
|
||||
),
|
||||
_ControlButton(
|
||||
icon: Icons.videocam,
|
||||
label: context.translate(I18n.videocallCameraOff),
|
||||
isActive: true,
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(),
|
||||
_HangUpButton(onPressed: onHangUp),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: _SwitchCameraButton(onPressed: onSwitchCamera),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -66,31 +79,19 @@ class _ControlButton extends StatelessWidget {
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.isActive,
|
||||
this.isDestructive = false,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final bool isActive;
|
||||
final bool isDestructive;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final backgroundColor = isDestructive
|
||||
? colorScheme.error
|
||||
: isActive
|
||||
? colorScheme.onSurface.withValues(alpha: 0.2)
|
||||
: colorScheme.onSurface.withValues(alpha: 0.4);
|
||||
|
||||
final iconColor = isDestructive
|
||||
? Colors.white
|
||||
: isActive
|
||||
? colorScheme.onSurface
|
||||
: colorScheme.onSurface.withValues(alpha: 0.7);
|
||||
final backgroundColor = isActive
|
||||
? Colors.white.withValues(alpha: 0.3)
|
||||
: Colors.white.withValues(alpha: 0.15);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -103,16 +104,60 @@ class _ControlButton extends StatelessWidget {
|
||||
customBorder: const CircleBorder(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Icon(icon, color: iconColor, size: 28),
|
||||
child: Icon(icon, color: Colors.white, size: 28),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.7), fontSize: 11),
|
||||
style: TextStyle(color: Colors.white.withValues(alpha: 0.9), fontSize: 11),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HangUpButton extends StatelessWidget {
|
||||
const _HangUpButton({required this.onPressed});
|
||||
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
shape: const CircleBorder(),
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
customBorder: const CircleBorder(),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(18),
|
||||
child: Icon(Icons.call_end, color: Colors.white, size: 32),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SwitchCameraButton extends StatelessWidget {
|
||||
const _SwitchCameraButton({required this.onPressed});
|
||||
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: Icon(Icons.cameraswitch_outlined, color: Colors.white, size: 24),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,22 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'call_direction.dart';
|
||||
import 'call_state.dart';
|
||||
|
||||
class VideocallItem {
|
||||
const VideocallItem({
|
||||
required this.userId,
|
||||
required this.isVideo,
|
||||
required this.direction,
|
||||
required this.state,
|
||||
this.uploadVideoStreamSelf = false,
|
||||
this.uploadVideoStreamOther = false,
|
||||
});
|
||||
part 'videocall_item.freezed.dart';
|
||||
|
||||
final String userId;
|
||||
final bool isVideo;
|
||||
final CallDirection direction;
|
||||
final VideocallState state;
|
||||
final bool uploadVideoStreamSelf;
|
||||
final bool uploadVideoStreamOther;
|
||||
|
||||
bool get isTalking => state == VideocallState.talking;
|
||||
|
||||
VideocallItem copyWith({
|
||||
String? userId,
|
||||
bool? isVideo,
|
||||
CallDirection? direction,
|
||||
VideocallState? state,
|
||||
bool? uploadVideoStreamSelf,
|
||||
bool? uploadVideoStreamOther,
|
||||
}) {
|
||||
return VideocallItem(
|
||||
userId: userId ?? this.userId,
|
||||
isVideo: isVideo ?? this.isVideo,
|
||||
direction: direction ?? this.direction,
|
||||
state: state ?? this.state,
|
||||
uploadVideoStreamSelf:
|
||||
uploadVideoStreamSelf ?? this.uploadVideoStreamSelf,
|
||||
uploadVideoStreamOther:
|
||||
uploadVideoStreamOther ?? this.uploadVideoStreamOther,
|
||||
);
|
||||
}
|
||||
@freezed
|
||||
abstract class VideocallItem with _$VideocallItem {
|
||||
const factory VideocallItem({
|
||||
required String userId,
|
||||
required bool isVideo,
|
||||
required CallDirection direction,
|
||||
required VideocallState state,
|
||||
@Default(false) bool uploadVideoStreamSelf,
|
||||
@Default(false) bool uploadVideoStreamOther,
|
||||
}) = _VideocallItem;
|
||||
}
|
||||
|
||||
extension VideocallItemX on VideocallItem {
|
||||
bool get isTalking => state == VideocallState.talking;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jc_sdk/jc_sdk.dart';
|
||||
|
||||
import '../models/call_direction.dart';
|
||||
@@ -12,16 +13,14 @@ class VideocallCallService with JCCallCallback {
|
||||
VideocallCallService({
|
||||
required VideocallClient client,
|
||||
required VideocallDeviceService deviceService,
|
||||
}) : _clientRef = client,
|
||||
_deviceRef = deviceService;
|
||||
}) : _clientRef = client,
|
||||
_deviceRef = deviceService;
|
||||
|
||||
final VideocallClient _clientRef;
|
||||
final VideocallDeviceService _deviceRef;
|
||||
JCCall? _call;
|
||||
JCCall? get call => _call;
|
||||
|
||||
// -- Streams --
|
||||
|
||||
final _callItemAddController = StreamController<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) {}
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user