feat(video_calls): ios voip pushes handling

This commit is contained in:
misha
2026-06-04 14:38:27 +03:00
parent 634d83fd61
commit f51a68324b
10 changed files with 420 additions and 18 deletions

View File

@@ -0,0 +1,194 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/video_calls/managers/video_call_manager.dart';
import 'package:eatvio_app/features/video_calls/managers/video_call_manager_snapshot.dart';
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_cubit.dart';
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_state.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_notification_navigator.dart';
import 'package:flutter_callkit_incoming/entities/call_event.dart';
import 'package:flutter_callkit_incoming/entities/call_kit_params.dart';
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
class VideoCallCallKitCoordinator {
VideoCallCallKitCoordinator({
required FirebaseApi firebaseApi,
VideoCallManager? videoCallManager,
VideoCallSessionCubit? videoCallSessionCubit,
}) : _firebaseApi = firebaseApi,
_videoCallManager = videoCallManager,
_videoCallSessionCubit = videoCallSessionCubit;
final FirebaseApi _firebaseApi;
final VideoCallManager? _videoCallManager;
final VideoCallSessionCubit? _videoCallSessionCubit;
StreamSubscription<CallEvent?>? _eventSubscription;
final Map<String, CallKitParams> _callKitParamsById = {};
String? _activeCallKitId;
bool _initialized = false;
Future<void> initialize() async {
if (_initialized) return;
if (!Platform.isIOS) return;
_initialized = true;
_eventSubscription = FlutterCallkitIncoming.onEvent.listen(_onCallKitEvent);
await _firebaseApi.registerPushToken();
}
void dispose() {
unawaited(_eventSubscription?.cancel());
_eventSubscription = null;
_callKitParamsById.clear();
_initialized = false;
}
VideoCallManager get _manager =>
_videoCallManager ?? sl<VideoCallManager>();
VideoCallSessionCubit get _cubit =>
_videoCallSessionCubit ?? sl<VideoCallSessionCubit>();
Future<void> _onCallKitEvent(CallEvent? event) async {
if (event == null) return;
switch (event) {
case CallEventActionDidUpdateDevicePushTokenVoip():
await _firebaseApi.registerPushToken();
case CallEventActionCallIncoming(:final callKitParams):
_cacheCallKitParams(callKitParams);
case CallEventActionCallAccept(:final id):
await _handleAccept(id);
case CallEventActionCallCallback(:final id):
await _handleAccept(id);
case CallEventActionCallDecline(:final id):
await _handleEnd(id);
case CallEventActionCallEnded(:final id):
await _handleEnd(id);
case CallEventActionCallTimeout(:final id):
await _handleEnd(id);
default:
break;
}
}
void _cacheCallKitParams(CallKitParams params) {
_callKitParamsById[params.id] = params;
_activeCallKitId = params.id;
}
CallKitParams? _paramsForCallId(String id) => _callKitParamsById[id];
Future<void> _handleAccept(String callKitId) async {
_activeCallKitId = callKitId;
final params = _paramsForCallId(callKitId);
final extra = params?.extra ?? <String, dynamic>{};
final conversationUuid = _parseConversationUuid(
extra,
handle: params?.handle,
);
if (conversationUuid.isEmpty) {
log(
'VideoCallCallKitCoordinator: accept without conversationUuid, callKitId=$callKitId',
name: 'VideoCallCallKit',
);
return;
}
final counterpartName = _parseString(extra, const [
'counterpartName',
'caller_name',
'callerName',
]) ?? params?.nameCaller;
final localParticipantName = _parseString(extra, const [
'localParticipantName',
'local_participant_name',
]);
final avatarUrl = _parseString(extra, const [
'counterpartAvatarUrl',
'avatar',
'avatarUrl',
'caller_avatar',
]) ?? params?.avatar;
await _cubit.startCall(
conversationUuid: conversationUuid,
counterpartName: counterpartName,
localParticipantName: localParticipantName,
);
await VideoCallNotificationNavigator.openIncomingCall(
conversationUuid: conversationUuid,
counterpartName: counterpartName,
counterpartAvatarUrl: avatarUrl,
localParticipantName: localParticipantName,
);
_waitForConnectedThenEnterCall(callKitId);
}
void _waitForConnectedThenEnterCall(String callKitId) {
void onSnapshot(VideoCallManagerSnapshot snapshot) {
if (snapshot.connectPhase != VideoCallConnectPhase.connected) return;
_manager.removeListener(onSnapshot);
unawaited(_cubit.enterCallStage());
unawaited(FlutterCallkitIncoming.setCallConnected(callKitId));
}
if (_manager.snapshot.connectPhase == VideoCallConnectPhase.connected) {
onSnapshot(_manager.snapshot);
return;
}
_manager.addListener(onSnapshot);
}
Future<void> _handleEnd(String callKitId) async {
await FlutterCallkitIncoming.endCall(callKitId);
_callKitParamsById.remove(callKitId);
if (_activeCallKitId == callKitId) {
_activeCallKitId = null;
}
if (_manager.isActiveCall ||
_cubit.state.connectPhase != VideoCallConnectPhase.idle) {
await _cubit.endCall();
}
}
static String _parseConversationUuid(
Map<String, dynamic> extra, {
String? handle,
}) {
final fromExtra = _parseString(extra, const [
'conversationUuid',
'conversation_uuid',
]);
if (fromExtra != null && fromExtra.isNotEmpty) return fromExtra;
final trimmedHandle = handle?.trim();
if (trimmedHandle != null && trimmedHandle.isNotEmpty) {
return trimmedHandle;
}
return '';
}
static String? _parseString(
Map<String, dynamic> map,
List<String> keys,
) {
for (final key in keys) {
final value = map[key];
if (value is String && value.trim().isNotEmpty) {
return value.trim();
}
}
return null;
}
}

View File

@@ -12,6 +12,48 @@ class VideoCallNotificationNavigator {
final uuid = conversationUuid ?? manager.conversationUuid;
if (uuid.isEmpty) return;
await _pushCallScreen(
conversationUuid: uuid,
counterpartName: manager.counterpartName,
localParticipantName: manager.localParticipantName,
);
await manager.onCallScreenVisible(
conversationUuid: uuid,
counterpartName: manager.counterpartName,
localParticipantName: manager.localParticipantName,
);
}
static Future<void> openIncomingCall({
required String conversationUuid,
String? counterpartName,
String? counterpartAvatarUrl,
String? localParticipantName,
}) async {
if (conversationUuid.isEmpty) return;
await _pushCallScreen(
conversationUuid: conversationUuid,
counterpartName: counterpartName,
counterpartAvatarUrl: counterpartAvatarUrl,
localParticipantName: localParticipantName,
);
final manager = sl<VideoCallManager>();
await manager.onCallScreenVisible(
conversationUuid: conversationUuid,
counterpartName: counterpartName,
localParticipantName: localParticipantName,
);
}
static Future<void> _pushCallScreen({
required String conversationUuid,
String? counterpartName,
String? counterpartAvatarUrl,
String? localParticipantName,
}) async {
for (var attempt = 0; attempt < 40; attempt++) {
final ctx = AppRouter.navigatorKey.currentContext;
if (ctx != null && ctx.mounted) {
@@ -24,17 +66,13 @@ class VideoCallNotificationNavigator {
await router.pushNamed(
VideoCallScreen.name,
extra: <String, dynamic>{
'conversationUuid': uuid,
'counterpartName': manager.counterpartName,
'localParticipantName': manager.localParticipantName,
'conversationUuid': conversationUuid,
'counterpartName': counterpartName,
'counterpartAvatarUrl': counterpartAvatarUrl,
'localParticipantName': localParticipantName,
},
);
}
await manager.onCallScreenVisible(
conversationUuid: uuid,
counterpartName: manager.counterpartName,
localParticipantName: manager.localParticipantName,
);
return;
}
await Future<void>.delayed(const Duration(milliseconds: 50));