fluffychat/lib/pages/voip/utils/voip_plugin.dart
2022-09-12 08:12:02 +05:30

342 lines
11 KiB
Dart

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<VoipType>(
// 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: <Widget>[
// 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<RTCPeerConnection> createPeerConnection(
Map<String, dynamic> configuration,
[Map<String, dynamic> 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<bool> 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<webrtc_impl.MediaStream> 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;
}
}