forked from eatvio/eatvio_app
feat(video_calls): ios voip pushes handling
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user