forked from eatvio/eatvio_app
feat(video_calls): ios voip pushes handling
This commit is contained in:
@@ -57,10 +57,9 @@ jobs:
|
||||
|
||||
- name: Flutter build
|
||||
run: |
|
||||
flutter upgrade
|
||||
flutter --version
|
||||
flutter clean
|
||||
flutter pub get
|
||||
flutter pub upgrade
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
# Configure Flutter for release mode
|
||||
cd ios
|
||||
|
||||
3
android/app/proguard-rules.pro
vendored
3
android/app/proguard-rules.pro
vendored
@@ -4,6 +4,9 @@
|
||||
-keep class org.bouncycastle.** { *; }
|
||||
-dontwarn org.bouncycastle.**
|
||||
|
||||
-keep class com.hiennv.flutter_callkit_incoming.** { *; }
|
||||
-dontwarn com.hiennv.flutter_callkit_incoming.**
|
||||
|
||||
-keep class com.pravera.flutter_foreground_task.** { *; }
|
||||
-dontwarn com.pravera.flutter_foreground_task.**
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import AVFAudio
|
||||
import CallKit
|
||||
import Flutter
|
||||
import PushKit
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
import YandexLoginSDK
|
||||
import flutter_callkit_incoming
|
||||
import flutter_foreground_task
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
@objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CallkitIncomingAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
@@ -13,22 +18,146 @@ import flutter_foreground_task
|
||||
SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback { registry in
|
||||
GeneratedPluginRegistrant.register(with: registry)
|
||||
}
|
||||
|
||||
let mainQueue = DispatchQueue.main
|
||||
let voipRegistry = PKPushRegistry(queue: mainQueue)
|
||||
voipRegistry.delegate = self
|
||||
voipRegistry.desiredPushTypes = [PKPushType.voIP]
|
||||
|
||||
if #available(iOS 10.0, *) {
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
}
|
||||
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
func application(
|
||||
_ application: UIApplication, continue userActivity: NSUserActivity,
|
||||
restorationHandler: @escaping ([Any]?) -> Void
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
continue userActivity: NSUserActivity,
|
||||
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
|
||||
) -> Bool {
|
||||
YXLSdk.shared.processUserActivity(userActivity)
|
||||
return true
|
||||
if YXLSdk.shared.processUserActivity(userActivity) {
|
||||
return true
|
||||
}
|
||||
return super.application(
|
||||
application,
|
||||
continue: userActivity,
|
||||
restorationHandler: restorationHandler
|
||||
)
|
||||
}
|
||||
|
||||
override public func application(
|
||||
_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
||||
_ app: UIApplication,
|
||||
open url: URL,
|
||||
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
||||
) -> Bool {
|
||||
return YXLSdk.shared.handleOpen(
|
||||
url, sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String)
|
||||
url,
|
||||
sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func pushRegistry(
|
||||
_ registry: PKPushRegistry,
|
||||
didUpdate credentials: PKPushCredentials,
|
||||
for type: PKPushType
|
||||
) {
|
||||
let deviceToken = credentials.token.map { String(format: "%02x", $0) }.joined()
|
||||
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken)
|
||||
}
|
||||
|
||||
func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
|
||||
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP("")
|
||||
}
|
||||
|
||||
func pushRegistry(
|
||||
_ registry: PKPushRegistry,
|
||||
didReceiveIncomingPushWith payload: PKPushPayload,
|
||||
for type: PKPushType,
|
||||
completion: @escaping () -> Void
|
||||
) {
|
||||
guard type == .voIP else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
let dict = payload.dictionaryPayload
|
||||
let id = (dict["id"] as? String)
|
||||
?? (dict["call_id"] as? String)
|
||||
?? (dict["message_id"] as? String)
|
||||
?? UUID().uuidString
|
||||
let nameCaller = (dict["nameCaller"] as? String)
|
||||
?? (dict["caller_name"] as? String)
|
||||
?? (dict["callerName"] as? String)
|
||||
?? ""
|
||||
let conversationUuid = (dict["conversation_uuid"] as? String)
|
||||
?? (dict["conversationUuid"] as? String)
|
||||
?? (dict["handle"] as? String)
|
||||
?? ""
|
||||
let isVideo = (dict["isVideo"] as? Bool)
|
||||
?? (dict["is_video"] as? Bool)
|
||||
?? true
|
||||
|
||||
var extra: [String: Any] = [:]
|
||||
if !conversationUuid.isEmpty {
|
||||
extra["conversationUuid"] = conversationUuid
|
||||
}
|
||||
if let avatar = dict["caller_avatar"] as? String ?? dict["avatar"] as? String
|
||||
?? dict["avatarUrl"] as? String
|
||||
{
|
||||
extra["counterpartAvatarUrl"] = avatar
|
||||
}
|
||||
if let messageId = dict["message_id"] as? String {
|
||||
extra["messageId"] = messageId
|
||||
}
|
||||
if let roomName = dict["room_name"] as? String {
|
||||
extra["roomName"] = roomName
|
||||
}
|
||||
if let localName = dict["localParticipantName"] as? String
|
||||
?? dict["local_participant_name"] as? String
|
||||
{
|
||||
extra["localParticipantName"] = localName
|
||||
}
|
||||
if let counterpartName = dict["counterpartName"] as? String {
|
||||
extra["counterpartName"] = counterpartName
|
||||
} else if !nameCaller.isEmpty {
|
||||
extra["counterpartName"] = nameCaller
|
||||
}
|
||||
|
||||
let data = flutter_callkit_incoming.Data(
|
||||
id: id,
|
||||
nameCaller: nameCaller,
|
||||
handle: conversationUuid.isEmpty ? nameCaller : conversationUuid,
|
||||
type: isVideo ? 1 : 0
|
||||
)
|
||||
data.extra = extra
|
||||
|
||||
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(
|
||||
data,
|
||||
fromPushKit: true
|
||||
) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func onAccept(_ call: Call, _ action: CXAnswerCallAction) {
|
||||
action.fulfill()
|
||||
}
|
||||
|
||||
func onDecline(_ call: Call, _ action: CXEndCallAction) {
|
||||
action.fulfill()
|
||||
}
|
||||
|
||||
func onEnd(_ call: Call, _ action: CXEndCallAction) {
|
||||
action.fulfill()
|
||||
}
|
||||
|
||||
func onTimeOut(_ call: Call) {}
|
||||
|
||||
func didActivateAudioSession(_ audioSession: AVAudioSession) {}
|
||||
|
||||
func didDeactivateAudioSession(_ audioSession: AVAudioSession) {}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>voip</string>
|
||||
<string>fetch</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
|
||||
@@ -35,6 +35,7 @@ import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
|
||||
import 'package:eatvio_app/features/registration/services/registration_service.dart';
|
||||
import 'package:eatvio_app/features/registration_v2/services/registration_service_v2.dart';
|
||||
import 'package:eatvio_app/features/settings/services/settings_service.dart';
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_callkit_coordinator.dart';
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_service.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
@@ -75,6 +76,9 @@ class ServiceRegistrations {
|
||||
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
|
||||
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
|
||||
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi(errorReporter: sl()));
|
||||
sl.registerLazySingleton<VideoCallCallKitCoordinator>(
|
||||
() => VideoCallCallKitCoordinator(firebaseApi: sl()),
|
||||
);
|
||||
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
|
||||
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
|
||||
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/services.dart' show MethodChannel, PlatformException;
|
||||
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
|
||||
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
|
||||
|
||||
class FirebaseApi {
|
||||
final ErrorReporter _errorReporter;
|
||||
@@ -87,6 +88,9 @@ class FirebaseApi {
|
||||
throw TimeoutException('registerPushToken timeout');
|
||||
},
|
||||
);
|
||||
if (Platform.isIOS) {
|
||||
await _registerIosVoipPushToken();
|
||||
}
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
await _errorReporter.report(e, st, origin: 'FirebaseApi');
|
||||
@@ -95,6 +99,31 @@ class FirebaseApi {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _registerIosVoipPushToken() async {
|
||||
try {
|
||||
final token = (await FlutterCallkitIncoming.getDevicePushTokenVoIP())?.trim();
|
||||
if (token == null || token.isEmpty) return;
|
||||
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
await sl<Dio>().post(
|
||||
'/api/profile/notification/token/voip/register',
|
||||
data: {
|
||||
'platform': 'ios',
|
||||
'hw_id': (await deviceInfo.iosInfo).identifierForVendor,
|
||||
'token': token,
|
||||
},
|
||||
).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
throw TimeoutException('registerPushToken voip timeout');
|
||||
},
|
||||
);
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
await _errorReporter.report(e, st, origin: 'FirebaseApi');
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _isHuaweiDevice() async {
|
||||
if (!Platform.isAndroid) return false;
|
||||
try {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -4,8 +4,11 @@ import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/notifications/push_navigation_handler.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_callkit_coordinator.dart';
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_foreground_bootstrap.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/widgets/video_call_global_pip_layer.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -32,6 +35,7 @@ class _MyAppState extends State<MyApp> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
PushNavigationHandler.initialize();
|
||||
unawaited(sl<VideoCallCallKitCoordinator>().initialize());
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -87,6 +87,7 @@ dependencies:
|
||||
livekit_client: ^2.6.1
|
||||
audio_session: ^0.1.25
|
||||
flutter_foreground_task: ^9.2.1
|
||||
flutter_callkit_incoming: ^3.1.1
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.10.4
|
||||
|
||||
Reference in New Issue
Block a user