import 'dart:core'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart' as webrtc_impl; import 'package:matrix/matrix.dart'; import 'package:webrtc_interface/webrtc_interface.dart' hide Navigator; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/voip/dialer/dialer.dart'; import 'package:fluffychat/pages/voip/utils/callkeep_manager.dart'; import 'package:fluffychat/pages/voip/utils/user_media_manager.dart'; import 'package:fluffychat/utils/famedlysdk_store.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/fluffy_chat_app.dart'; enum VoipType { kVoice, kVideo, kGroup } class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate { final Client client; VoipPlugin(this.client) { voip = VoIP(client, this); try { Connectivity() .onConnectivityChanged .listen(_handleNetworkChanged) .onError((e) => _currentConnectivity = ConnectivityResult.none); Connectivity() .checkConnectivity() .then((result) => _currentConnectivity = result) .catchError((e) => _currentConnectivity = ConnectivityResult.none); } catch (e) { // currently broken on web Logs().e('Listening to connectivity failed. $e'); } if (!kIsWeb) { final wb = WidgetsBinding.instance; wb.addObserver(this); didChangeAppLifecycleState(wb.lifecycleState); } } bool background = false; bool speakerOn = false; late VoIP voip; ConnectivityResult? _currentConnectivity; OverlayEntry? overlayEntry; // void onPhoneButtonTap(BuildContext context, Room room) async { // final callType = await showDialog( // context: context, // builder: (BuildContext context) { // return SimpleDialog( // titlePadding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 24.0), // title: Row( // mainAxisAlignment: MainAxisAlignment.spaceEvenly, // crossAxisAlignment: CrossAxisAlignment.center, // children: [ // ProfileScaffoldButton( // key: const Key('CallProfileScaffoldVoiceCall'), // iconData: FamedlyIconsV2.phone, // onTap: () { // Navigator.pop(context, VoipType.kVoice); // }, // ), // ProfileScaffoldButton( // key: const Key('CallProfileScaffoldVideoCall'), // iconData: Icons.videocam, // onTap: () { // Navigator.pop(context, VoipType.kVideo); // }, // ), // ProfileScaffoldButton( // key: const Key('CallProfileScaffoldGroupCall'), // iconData: Icons.people, // onTap: () { // Navigator.pop(context, VoipType.kGroup); // }, // ), // ], // ), // ); // }); // if (callType == null) { // return; // } // final success = await showFutureLoadingDialog( // context: context, // future: () => // Famedly.of(context).voipPlugin.voip.requestTurnServerCredentials()); // if (success.result != null) { // final voipPlugin = Famedly.of(context).voipPlugin; // if ({VoipType.kVideo, VoipType.kVoice}.contains(callType)) { // await voipPlugin.voip // .inviteToCall(room.id, // callType == VoipType.kVoice ? CallType.kVoice : CallType.kVideo) // .catchError((e) { // ScaffoldMessenger.of(context).showSnackBar( // SnackBar( // content: Text( // LocalizedExceptionExtension.toLocalizedString(context, e)), // ), // ); // }); // } else { // final groupCall = await voipPlugin.voip.fetchOrCreateGroupCall(room.id); // groupCall?.enter(); // Logs().e('Group call should be enter now'); // } // } else { // await showOkAlertDialog( // context: context, // title: L10n.of(context)!.thisFeatureHasNotBeenImplementedYet, // okLabel: L10n.of(context)!.next, // useRootNavigator: false, // ); // } // } void _handleNetworkChanged(ConnectivityResult result) async { /// Got a new connectivity status! if (_currentConnectivity != result) { voip.calls.forEach((_, sess) { sess.restartIce(); }); } _currentConnectivity = result; } @override void didChangeAppLifecycleState(AppLifecycleState? state) { Logs().v('AppLifecycleState = $state'); background = !(state != AppLifecycleState.detached && state != AppLifecycleState.paused); } void addCallingOverlay() { final context = kIsWeb ? ChatList.contextForVoip! : FluffyChatApp.routerKey.currentContext ?? ChatList.contextForVoip!; if (overlayEntry != null) { Logs().w('[VOIP] addCallingOverlay: The call session already exists?'); overlayEntry!.remove(); } final overlay = Overlay.of(context)!; overlayEntry = OverlayEntry( builder: (_) => Calling( voipPlugin: this, onClear: () { overlayEntry?.remove(); overlayEntry = null; }), ); overlay.insert(overlayEntry!); } CallSession? get currentCall => voip.currentCID == null ? null : voip.calls[voip.currentCID]; GroupCall? get currentGroupCall => voip.currentGroupCID == null ? null : voip.groupCalls[voip.currentGroupCID]; bool inMeeting = false; void start() { voip.startGroupCalls(); } bool getInMeetingState() { return currentCall != null || currentGroupCall != null; } String? get currentMeetingRoomId { return currentCall?.room.id ?? currentGroupCall?.room.id; } CallSession? get call { if (voip.currentCID != null) { final call = voip.calls[voip.currentCID]; if (call != null && call.groupCallId != null) { return call; } } return null; } GroupCall? get groupCall { if (voip.currentCID != null) { return voip.groupCalls[voip.currentGroupCID]; } return null; } @override MediaDevices get mediaDevices => webrtc_impl.navigator.mediaDevices; @override // remove this from sdk once callkeep is stable bool get isBackgroud => false; @override bool get isWeb => kIsWeb; @override Future createPeerConnection( Map configuration, [Map constraints = const {}]) => webrtc_impl.createPeerConnection(configuration, constraints); @override VideoRenderer createRenderer() { return webrtc_impl.RTCVideoRenderer(); } @override void playRingtone() async { if (!background && !await hasCallingAccount) { try { await UserMediaManager().startRingingTone(); } catch (_) {} } } @override void stopRingtone() async { if (!background && !await hasCallingAccount) { try { await UserMediaManager().stopRingingTone(); } catch (_) {} } } Future get hasCallingAccount async => PlatformInfos.isAndroid ? await CallKeepManager().hasPhoneAccountEnabled : false; @override void handleNewCall(CallSession call) async { Logs().d('[VoipPlugin] detected new call'); if (!getInMeetingState()) { return; } if (call.direction == CallDirection.kIncoming && await hasCallingAccount && call.type == CallType.kVoice && Platform.isAndroid) { ///Popup native telecom manager call UI for incoming call. final callKeeper = CallKeeper(CallKeepManager(), call); CallKeepManager().addCall(call.callId, callKeeper); await CallKeepManager().showCallkitIncoming(call); } else { if (!await hasCallingAccount || !Platform.isAndroid) { if (Platform.isAndroid) { try { ScaffoldMessenger.of(FluffyChatApp.routerKey.currentContext!) .showSnackBar(const SnackBar( content: Text( 'No calling accounts found (used for native calls UI)', ))); } catch (e) { Logs().e('[VoipPlugin] Failed to show snackbar', e); } } try { final wasForeground = await FlutterForegroundTask.isAppOnForeground; await Store().setItem( 'wasForeground', wasForeground == true ? 'true' : 'false'); FlutterForegroundTask.setOnLockScreenVisibility(true); FlutterForegroundTask.wakeUpScreen(); FlutterForegroundTask.launchApp(); } catch (e) { Logs().e('VOIP foreground failed $e'); } } Logs().d('[VoipPlugin] pushing flutter call overlay'); // use fallback flutter call pages for outgoing and video calls. addCallingOverlay(); } } @override void handleCallEnded(CallSession session) async { if (getInMeetingState()) { return; } if (overlayEntry != null) { overlayEntry!.remove(); overlayEntry = null; if (Platform.isAndroid) { FlutterForegroundTask.setOnLockScreenVisibility(false); FlutterForegroundTask.stopService(); final wasForeground = await Store().getItem('wasForeground'); wasForeground == 'false' ? FlutterForegroundTask.minimizeApp() : null; } } } @override void handleNewGroupCall(GroupCall groupCall) { Logs().d('[VOIP] new call found'); if (!getInMeetingState()) { return; } /// Popup CallingPage for incoming call. addCallingOverlay(); if (PlatformInfos.isAndroid) { FlutterForegroundTask.setOnLockScreenVisibility(true); } Logs().d('[VOIP] overlay stuff should be there up there'); } @override void handleGroupCallEnded(GroupCall groupCall) { if (overlayEntry != null) { overlayEntry!.remove(); overlayEntry = null; } if (Platform.isAndroid) { FlutterForegroundTask.setOnLockScreenVisibility(false); FlutterForegroundTask.stopService(); } } @override Future cloneStream( webrtc_impl.MediaStream stream) async { final cloneStream = await webrtc_impl.createLocalMediaStream(stream.id); stream.getTracks().forEach((track) { cloneStream.addTrack(track, addToNative: true); }); return cloneStream; } }