From 37778ba202d3e409851932fe9071602d7f7d40b5 Mon Sep 17 00:00:00 2001 From: isaacakakpo1 Date: Tue, 5 Nov 2024 13:53:27 +0100 Subject: [PATCH] Implement Reconnection Logic for Network Roaming --- .../lib/model/push_notification.dart.json | 18 ++++ .../lib/model/socket_method.dart.json | 18 ++++ .../receive/received_message_body.dart.json | 26 +++++ .../telnyx_webrtc/lib/peer/peer.dart.json | 22 ++++ ios/Podfile.lock | 13 +-- ios/Runner.xcodeproj/project.pbxproj | 3 + lib/main.dart | 4 +- packages/telnyx_webrtc/lib/call.dart | 4 +- .../lib/model/push_notification.dart | 3 +- .../lib/model/socket_method.dart | 2 +- .../verto/receive/received_message_body.dart | 11 +- packages/telnyx_webrtc/lib/peer/peer.dart | 10 +- packages/telnyx_webrtc/lib/telnyx_client.dart | 101 +++++++++++++++++- packages/telnyx_webrtc/pubspec.yaml | 1 + pubspec.yaml | 1 + 15 files changed, 215 insertions(+), 22 deletions(-) create mode 100644 .lh/packages/telnyx_webrtc/lib/model/push_notification.dart.json create mode 100644 .lh/packages/telnyx_webrtc/lib/model/socket_method.dart.json create mode 100644 .lh/packages/telnyx_webrtc/lib/model/verto/receive/received_message_body.dart.json create mode 100644 .lh/packages/telnyx_webrtc/lib/peer/peer.dart.json diff --git a/.lh/packages/telnyx_webrtc/lib/model/push_notification.dart.json b/.lh/packages/telnyx_webrtc/lib/model/push_notification.dart.json new file mode 100644 index 0000000..dec93e9 --- /dev/null +++ b/.lh/packages/telnyx_webrtc/lib/model/push_notification.dart.json @@ -0,0 +1,18 @@ +{ + "sourceFile": "packages/telnyx_webrtc/lib/model/push_notification.dart", + "activeCommit": 0, + "commits": [ + { + "activePatchIndex": 0, + "patches": [ + { + "date": 1730799598150, + "content": "Index: \n===================================================================\n--- \n+++ \n" + } + ], + "date": 1730799598150, + "name": "Commit-0", + "content": "class PushNotification {\n PushNotification({\n required this.metadata,\n required this.message,\n });\n\n PushMetaData metadata;\n String message;\n}\n\nclass PushMetaData {\n PushMetaData(\n {this.caller_name, this.caller_number, this.call_id, this.voice_sdk_id});\n \n String? caller_name;\n String? caller_number;\n String? call_id;\n String? voice_sdk_id;\n bool? isAnswer;\n bool? isDecline;\n\n PushMetaData.fromJson(Map json) {\n caller_name = json['caller_name'];\n caller_number = json['caller_number'];\n call_id = json['call_id'];\n voice_sdk_id = json['voice_sdk_id'];\n isAnswer = json['isAnswer'];\n isDecline = json['isDecline'];\n }\n\n Map toJson() {\n final Map data = {};\n data['caller_name'] = caller_name;\n data['caller_number'] = caller_number;\n data['call_id'] = call_id;\n data['voice_sdk_id'] = voice_sdk_id;\n data['isAnswer'] = isAnswer;\n data['isDecline'] = isDecline;\n return data;\n }\n}\n" + } + ] +} \ No newline at end of file diff --git a/.lh/packages/telnyx_webrtc/lib/model/socket_method.dart.json b/.lh/packages/telnyx_webrtc/lib/model/socket_method.dart.json new file mode 100644 index 0000000..c307799 --- /dev/null +++ b/.lh/packages/telnyx_webrtc/lib/model/socket_method.dart.json @@ -0,0 +1,18 @@ +{ + "sourceFile": "packages/telnyx_webrtc/lib/model/socket_method.dart", + "activeCommit": 0, + "commits": [ + { + "activePatchIndex": 0, + "patches": [ + { + "date": 1730809640028, + "content": "Index: \n===================================================================\n--- \n+++ \n" + } + ], + "date": 1730809640028, + "name": "Commit-0", + "content": "class SocketMethod {\n static const ANSWER = \"telnyx_rtc.answer\";\n static const INVITE = \"telnyx_rtc.invite\";\n static const BYE = \"telnyx_rtc.bye\";\n static const MODIFY = \"telnyx_rtc.modify\";\n static const MEDIA = \"telnyx_rtc.media\";\n static const INFO = \"telnyx_rtc.info\";\n static const RINGING = \"telnyx_rtc.ringing\";\n static const CLIENT_READY = \"telnyx_rtc.clientReady\";\n static const GATEWAY_STATE = \"telnyx_rtc.gatewayState\";\n static const PING = \"telnyx_rtc.ping\";\n static const LOGIN = \"login\";\n static const ATTACH_CALL = \"telnyx_rtc.attachCalls\";\n static const ATTACH = \"telnyx_rtc.attach\";\n}\n" + } + ] +} \ No newline at end of file diff --git a/.lh/packages/telnyx_webrtc/lib/model/verto/receive/received_message_body.dart.json b/.lh/packages/telnyx_webrtc/lib/model/verto/receive/received_message_body.dart.json new file mode 100644 index 0000000..fed6762 --- /dev/null +++ b/.lh/packages/telnyx_webrtc/lib/model/verto/receive/received_message_body.dart.json @@ -0,0 +1,26 @@ +{ + "sourceFile": "packages/telnyx_webrtc/lib/model/verto/receive/received_message_body.dart", + "activeCommit": 0, + "commits": [ + { + "activePatchIndex": 2, + "patches": [ + { + "date": 1730798011877, + "content": "Index: \n===================================================================\n--- \n+++ \n" + }, + { + "date": 1730798025300, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -37,10 +37,10 @@\n : null;\n if (json['params']['dialogParams'] != null) {\n dialogParams = DialogParams.fromJson(json['params']['dialogParams']);\n }\n- if (json['params']['voice_sdk_id'] != null) {\n- voiceSdkId = json['params']['voice_sdk_id'];\n+ if (json['voice_sdk_id'] != null) {\n+ voiceSdkId = json['voice_sdk_id'];\n Logger().i('Voice SDK ID: $voiceSdkId');\n }\n }\n \n" + }, + { + "date": 1730808273018, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -113,9 +113,9 @@\n ReattachedParams({this.reattachedSessions});\n \n ReattachedParams.fromJson(Map json) {\n if (json['reattached_sessions'] != null) {\n- reattachedSessions = [];\n+ reattachedSessions = [];\n json['reattached_sessions'].forEach((v) {\n reattachedSessions!.add(v);\n });\n }\n" + } + ], + "date": 1730798011877, + "name": "Commit-0", + "content": "import 'package:logger/logger.dart';\nimport '../send/invite_answer_message_body.dart';\nimport 'package:telnyx_webrtc/model/telnyx_socket_error.dart';\n\nclass ReceivedMessage {\n String? jsonrpc;\n int? id;\n String? method;\n ReattachedParams? reattachedParams;\n StateParams? stateParams;\n IncomingInviteParams? inviteParams;\n DialogParams? dialogParams;\n\n String? voiceSdkId;\n\n ReceivedMessage(\n {this.jsonrpc,\n this.id,\n this.method,\n this.reattachedParams,\n this.stateParams,\n this.inviteParams,\n this.dialogParams,\n this.voiceSdkId});\n\n ReceivedMessage.fromJson(Map json) {\n jsonrpc = json['jsonrpc'];\n id = json['id'];\n method = json['method'];\n reattachedParams = json['params'] != null\n ? ReattachedParams.fromJson(json['params'])\n : null;\n stateParams =\n json['params'] != null ? StateParams.fromJson(json['params']) : null;\n inviteParams = json['params'] != null\n ? IncomingInviteParams.fromJson(json['params'])\n : null;\n if (json['params']['dialogParams'] != null) {\n dialogParams = DialogParams.fromJson(json['params']['dialogParams']);\n }\n if (json['params']['voice_sdk_id'] != null) {\n voiceSdkId = json['params']['voice_sdk_id'];\n Logger().i('Voice SDK ID: $voiceSdkId');\n }\n }\n\n Map toJson() {\n final Map data = {};\n data['jsonrpc'] = jsonrpc;\n data['id'] = id;\n data['method'] = method;\n if (reattachedParams != null) {\n data['params'] = reattachedParams!.toJson();\n }\n if (stateParams != null) {\n data['params'] = stateParams!.toJson();\n }\n if (inviteParams != null) {\n data['params'] = inviteParams!.toJson();\n }\n if (dialogParams != null) {\n data['dialogParams'] = dialogParams!.toJson();\n }\n return data;\n }\n\n @override\n String toString() {\n return 'Received Message: {jsonrpc: $jsonrpc, id: $id method: $method, reattachedParams: $reattachedParams, stateParams: $stateParams}';\n }\n}\n\nclass ReceivedResult {\n String? jsonrpc;\n String? id;\n ResultParams? resultParams;\n String? sessId;\n TelnyxSocketError? error;\n\n ReceivedResult({this.jsonrpc, this.id, this.resultParams});\n\n ReceivedResult.fromJson(Map json) {\n jsonrpc = json['jsonrpc'];\n id = json['id'];\n resultParams =\n json['result'] != null ? ResultParams.fromJson(json['result']) : null;\n sessId = json['sessid'];\n error = json['error'] != null\n ? TelnyxSocketError.fromJson(json['error'])\n : null;\n }\n\n Map toJson() {\n final Map data = {};\n data['jsonrpc'] = jsonrpc;\n data['id'] = id;\n\n if (resultParams != null) {\n data['params'] = resultParams!.toJson();\n }\n return data;\n }\n\n @override\n String toString() {\n return 'Received Message: {jsonrpc: $jsonrpc, id: $id, stateParams: ${resultParams?.toJson()}}';\n }\n}\n\nclass ReattachedParams {\n List? reattachedSessions;\n\n ReattachedParams({this.reattachedSessions});\n\n ReattachedParams.fromJson(Map json) {\n if (json['reattached_sessions'] != null) {\n reattachedSessions = [];\n json['reattached_sessions'].forEach((v) {\n reattachedSessions!.add(v);\n });\n }\n }\n\n Map toJson() {\n final Map data = {};\n if (reattachedSessions != null) {\n data['reattached_sessions'] = reattachedSessions!.map((v) => v).toList();\n }\n return data;\n }\n\n @override\n String toString() {\n return 'Reattached Params : $reattachedSessions';\n }\n}\n\nclass ResultParams {\n StateParams? stateParams;\n\n ResultParams({this.stateParams});\n\n ResultParams.fromJson(Map json) {\n stateParams =\n json['params'] != null ? StateParams.fromJson(json['params']) : null;\n }\n\n Map toJson() {\n final Map data = {};\n if (stateParams != null) {\n data['params'] = stateParams!.toJson();\n }\n return data;\n }\n}\n\nclass StateParams {\n String? state;\n\n StateParams({this.state});\n\n StateParams.fromJson(Map json) {\n state = json['state'];\n }\n\n Map toJson() {\n final Map data = {};\n data['state'] = state;\n return data;\n }\n\n @override\n String toString() {\n return 'State Params : $state';\n }\n}\n\nclass IncomingInviteParams {\n String? callID;\n Variables? variables;\n String? sdp;\n String? callerIdName;\n String? callerIdNumber;\n String? calleeIdName;\n String? calleeIdNumber;\n String? telnyxSessionId;\n String? telnyxLegId;\n String? displayDirection;\n\n IncomingInviteParams(\n {this.callID,\n this.variables,\n this.sdp,\n this.callerIdName,\n this.callerIdNumber,\n this.calleeIdName,\n this.calleeIdNumber,\n this.telnyxSessionId,\n this.telnyxLegId,\n this.displayDirection});\n\n IncomingInviteParams.fromJson(Map json) {\n callID = json['callID'];\n variables = json['variables'] != null\n ? Variables.fromJson(json['variables'])\n : null;\n sdp = json['sdp'];\n callerIdName = json['caller_id_name'];\n callerIdNumber = json['caller_id_number'];\n calleeIdName = json['callee_id_name'];\n calleeIdNumber = json['callee_id_number'];\n telnyxSessionId = json['telnyx_session_id'];\n telnyxLegId = json['telnyx_leg_id'];\n displayDirection = json['display_direction'];\n }\n\n Map toJson() {\n final Map data = {};\n data['callID'] = callID;\n if (variables != null) {\n data['variables'] = variables!.toJson();\n }\n data['sdp'] = sdp;\n data['caller_id_name'] = callerIdName;\n data['caller_id_number'] = callerIdNumber;\n data['callee_id_name'] = calleeIdName;\n data['callee_id_number'] = calleeIdNumber;\n data['telnyx_session_id'] = telnyxSessionId;\n data['telnyx_leg_id'] = telnyxLegId;\n data['display_direction'] = displayDirection;\n return data;\n }\n}\n\nclass Variables {\n String? eventName;\n String? coreUUID;\n String? freeSWITCHHostname;\n String? freeSWITCHSwitchname;\n String? freeSWITCHIPv4;\n String? freeSWITCHIPv6;\n String? eventDateLocal;\n String? eventDateGMT;\n String? eventDateTimestamp;\n String? eventCallingFile;\n String? eventCallingFunction;\n String? eventCallingLineNumber;\n String? eventSequence;\n\n Variables(\n {this.eventName,\n this.coreUUID,\n this.freeSWITCHHostname,\n this.freeSWITCHSwitchname,\n this.freeSWITCHIPv4,\n this.freeSWITCHIPv6,\n this.eventDateLocal,\n this.eventDateGMT,\n this.eventDateTimestamp,\n this.eventCallingFile,\n this.eventCallingFunction,\n this.eventCallingLineNumber,\n this.eventSequence});\n\n Variables.fromJson(Map json) {\n eventName = json['Event-Name'];\n coreUUID = json['Core-UUID'];\n freeSWITCHHostname = json['FreeSWITCH-Hostname'];\n freeSWITCHSwitchname = json['FreeSWITCH-Switchname'];\n freeSWITCHIPv4 = json['FreeSWITCH-IPv4'];\n freeSWITCHIPv6 = json['FreeSWITCH-IPv6'];\n eventDateLocal = json['Event-Date-Local'];\n eventDateGMT = json['Event-Date-GMT'];\n eventDateTimestamp = json['Event-Date-Timestamp'];\n eventCallingFile = json['Event-Calling-File'];\n eventCallingFunction = json['Event-Calling-Function'];\n eventCallingLineNumber = json['Event-Calling-Line-Number'];\n eventSequence = json['Event-Sequence'];\n }\n\n Map toJson() {\n final Map data = {};\n data['Event-Name'] = eventName;\n data['Core-UUID'] = coreUUID;\n data['FreeSWITCH-Hostname'] = freeSWITCHHostname;\n data['FreeSWITCH-Switchname'] = freeSWITCHSwitchname;\n data['FreeSWITCH-IPv4'] = freeSWITCHIPv4;\n data['FreeSWITCH-IPv6'] = freeSWITCHIPv6;\n data['Event-Date-Local'] = eventDateLocal;\n data['Event-Date-GMT'] = eventDateGMT;\n data['Event-Date-Timestamp'] = eventDateTimestamp;\n data['Event-Calling-File'] = eventCallingFile;\n data['Event-Calling-Function'] = eventCallingFunction;\n data['Event-Calling-Line-Number'] = eventCallingLineNumber;\n data['Event-Sequence'] = eventSequence;\n return data;\n }\n}\n" + } + ] +} \ No newline at end of file diff --git a/.lh/packages/telnyx_webrtc/lib/peer/peer.dart.json b/.lh/packages/telnyx_webrtc/lib/peer/peer.dart.json new file mode 100644 index 0000000..747ce61 --- /dev/null +++ b/.lh/packages/telnyx_webrtc/lib/peer/peer.dart.json @@ -0,0 +1,22 @@ +{ + "sourceFile": "packages/telnyx_webrtc/lib/peer/peer.dart", + "activeCommit": 0, + "commits": [ + { + "activePatchIndex": 1, + "patches": [ + { + "date": 1730809569544, + "content": "Index: \n===================================================================\n--- \n+++ \n" + }, + { + "date": 1730809600452, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -273,9 +273,9 @@\n userAgent: \"Flutter-1.0\");\n var answerMessage = InviteAnswerMessage(\n id: const Uuid().v4(),\n jsonrpc: JsonRPCConstant.jsonrpc,\n- method: SocketMethod.ANSWER,\n+ method: isAttach ? SocketMethod.ATTACH : SocketMethod.ANSWER,\n params: inviteParams);\n \n String jsonAnswerMessage = jsonEncode(answerMessage);\n _send(jsonAnswerMessage);\n" + } + ], + "date": 1730809569544, + "name": "Commit-0", + "content": "import 'dart:convert';\nimport 'dart:async';\nimport 'dart:math';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_webrtc/flutter_webrtc.dart';\nimport 'package:telnyx_webrtc/config.dart';\nimport 'package:telnyx_webrtc/model/socket_method.dart';\nimport 'package:telnyx_webrtc/model/verto/send/invite_answer_message_body.dart';\nimport 'package:telnyx_webrtc/tx_socket.dart'\n if (dart.library.js) 'package:telnyx_webrtc/tx_socket_web.dart';\nimport 'package:uuid/uuid.dart';\nimport 'package:logger/logger.dart';\nimport 'package:telnyx_webrtc/model/verto/receive/received_message_body.dart';\nimport '../model/call_state.dart';\nimport '../model/jsonrpc.dart';\n\nenum SignalingState {\n ConnectionOpen,\n ConnectionClosed,\n ConnectionError,\n}\n\nclass Session {\n Session({required this.sid, required this.pid});\n\n String pid;\n String sid;\n RTCPeerConnection? peerConnection;\n RTCDataChannel? dc;\n List remoteCandidates = [];\n}\n\nclass Peer {\n Peer(this._socket);\n\n final _logger = Logger();\n\n final String _selfId = randomNumeric(6);\n\n final TxSocket _socket;\n\n final Map _sessions = {};\n MediaStream? _localStream;\n final List _remoteStreams = [];\n\n Function(SignalingState state)? onSignalingStateChange;\n Function(Session session, CallState state)? onCallStateChange;\n Function(MediaStream stream)? onLocalStream;\n Function(Session session, MediaStream stream)? onAddRemoteStream;\n Function(Session session, MediaStream stream)? onRemoveRemoteStream;\n Function(dynamic event)? onPeersUpdate;\n Function(Session session, RTCDataChannel dc, RTCDataChannelMessage data)?\n onDataChannelMessage;\n Function(Session session, RTCDataChannel dc)? onDataChannel;\n\n String get sdpSemantics =>\n WebRTC.platformIsWindows ? 'plan-b' : 'unified-plan';\n\n final Map _iceServers = {\n 'iceServers': [\n {\n 'url': DefaultConfig.defaultStun,\n 'username': DefaultConfig.username,\n 'credential': DefaultConfig.password\n },\n {\n 'url': DefaultConfig.defaultTurn,\n 'username': DefaultConfig.username,\n 'credential': DefaultConfig.password\n },\n ]\n };\n\n final Map _dcConstraints = {\n 'mandatory': {\n 'OfferToReceiveAudio': true,\n 'OfferToReceiveVideo': false,\n },\n 'optional': [\n {'DtlsSrtpKeyAgreement': true},\n ],\n };\n\n close() async {\n await _cleanSessions();\n }\n\n void muteUnmuteMic() {\n if (_localStream != null) {\n bool enabled = _localStream!.getAudioTracks()[0].enabled;\n _localStream!.getAudioTracks()[0].enabled = !enabled;\n } else {\n _logger.d(\"Peer :: No local stream :: Unable to Mute / Unmute\");\n }\n }\n\n void enableSpeakerPhone(bool enable) {\n if (_localStream != null) {\n _localStream!.getAudioTracks()[0].enableSpeakerphone(enable);\n } else {\n _logger.d(\"Peer :: No local stream :: Unable to toggle speaker mode\");\n }\n }\n\n void invite(\n String callerName,\n String callerNumber,\n String destinationNumber,\n String clientState,\n String callId,\n String telnyxSessionId,\n Map customHeaders) async {\n var sessionId = _selfId;\n\n Session session = await _createSession(null,\n peerId: \"0\", sessionId: sessionId, media: \"audio\");\n\n _sessions[sessionId] = session;\n\n _createOffer(session, \"audio\", callerName, callerNumber, destinationNumber,\n clientState, callId, telnyxSessionId, customHeaders);\n onCallStateChange?.call(session, CallState.newCall);\n }\n\n Future _createOffer(\n Session session,\n String media,\n String callerName,\n String callerNumber,\n String destinationNumber,\n String clientState,\n String callId,\n String sessionId,\n Map customHeaders) async {\n try {\n RTCSessionDescription s =\n await session.peerConnection!.createOffer(_dcConstraints);\n await session.peerConnection!.setLocalDescription(s);\n\n if (session.remoteCandidates.isNotEmpty) {\n for (var candidate in session.remoteCandidates) {\n if (candidate.candidate != null) {\n _logger.i(\"adding $candidate\");\n await session.peerConnection?.addCandidate(candidate);\n }\n }\n session.remoteCandidates.clear();\n }\n\n await Future.delayed(const Duration(milliseconds: 500));\n\n String? sdpUsed = \"\";\n session.peerConnection\n ?.getLocalDescription()\n .then((value) => sdpUsed = value?.sdp.toString());\n\n Timer(const Duration(milliseconds: 500), () {\n var dialogParams = DialogParams(\n attach: false,\n audio: true,\n callID: callId,\n callerIdName: callerName,\n callerIdNumber: callerNumber,\n clientState: clientState,\n destinationNumber: destinationNumber,\n remoteCallerIdName: \"\",\n screenShare: false,\n useStereo: false,\n userVariables: [],\n video: false,\n customHeaders: customHeaders);\n var inviteParams = InviteParams(\n dialogParams: dialogParams,\n sdp: sdpUsed,\n sessid: sessionId,\n userAgent: \"Flutter-1.0\");\n var inviteMessage = InviteAnswerMessage(\n id: const Uuid().v4(),\n jsonrpc: JsonRPCConstant.jsonrpc,\n method: SocketMethod.INVITE,\n params: inviteParams);\n\n String jsonInviteMessage = jsonEncode(inviteMessage);\n\n _send(jsonInviteMessage);\n });\n } catch (e) {\n _logger.e(\"Peer :: $e\");\n }\n }\n\n void remoteSessionReceived(String sdp) async {\n await _sessions[_selfId]\n ?.peerConnection\n ?.setRemoteDescription(RTCSessionDescription(sdp, \"answer\"));\n }\n\n void accept(\n String callerName,\n String callerNumber,\n String destinationNumber,\n String clientState,\n String callId,\n IncomingInviteParams invite,\n Map customHeaders,bool isAttach) async {\n var sessionId = _selfId;\n Session session = await _createSession(null,\n peerId: \"0\", sessionId: sessionId, media: \"audio\");\n _sessions[sessionId] = session;\n\n await session.peerConnection\n ?.setRemoteDescription(RTCSessionDescription(invite.sdp, \"offer\"));\n\n _createAnswer(session, \"audio\", callerName, callerNumber, destinationNumber,\n clientState, callId, customHeaders,isAttach);\n\n onCallStateChange?.call(session, CallState.active);\n }\n\n Future _createAnswer(\n Session session,\n String media,\n String callerName,\n String callerNumber,\n String destinationNumber,\n String clientState,\n String callId,\n Map customHeaders,bool isAttach) async {\n try {\n session.peerConnection?.onIceCandidate = (candidate) async {\n if (session.peerConnection != null) {\n _logger.i(\"Peer :: Add Ice Candidate!\");\n if (candidate.candidate != null) {\n await session.peerConnection?.addCandidate(candidate);\n }\n } else {\n session.remoteCandidates.add(candidate);\n }\n };\n\n RTCSessionDescription s =\n await session.peerConnection!.createAnswer(_dcConstraints);\n await session.peerConnection!.setLocalDescription(s);\n\n await Future.delayed(const Duration(milliseconds: 500));\n\n String? sdpUsed = \"\";\n session.peerConnection\n ?.getLocalDescription()\n .then((value) => sdpUsed = value?.sdp.toString());\n\n Timer(const Duration(milliseconds: 500), () {\n var dialogParams = DialogParams(\n attach: false,\n audio: true,\n callID: callId,\n callerIdName: callerNumber,\n callerIdNumber: callerNumber,\n clientState: clientState,\n destinationNumber: destinationNumber,\n remoteCallerIdName: \"\",\n screenShare: false,\n useStereo: false,\n userVariables: [],\n video: false,\n customHeaders: customHeaders);\n var inviteParams = InviteParams(\n dialogParams: dialogParams,\n sdp: sdpUsed,\n sessid: session.sid,\n userAgent: \"Flutter-1.0\");\n var answerMessage = InviteAnswerMessage(\n id: const Uuid().v4(),\n jsonrpc: JsonRPCConstant.jsonrpc,\n method: SocketMethod.ANSWER,\n params: inviteParams);\n\n String jsonAnswerMessage = jsonEncode(answerMessage);\n _send(jsonAnswerMessage);\n });\n } catch (e) {\n _logger.e(\"Peer :: $e\");\n }\n }\n\n void closeSession() {\n var sess = _sessions[_selfId];\n if (sess != null) {\n _logger.d(\"Session end success\");\n _closeSession(sess);\n } else {\n _logger.d(\"Session end failed\");\n }\n }\n\n Future createStream(String media) async {\n final Map mediaConstraints = {\n 'audio': true,\n 'video': false\n };\n\n MediaStream stream =\n await navigator.mediaDevices.getUserMedia(mediaConstraints);\n onLocalStream?.call(stream);\n return stream;\n }\n\n Future _createSession(Session? session,\n {required String peerId,\n required String sessionId,\n required String media}) async {\n var newSession = session ?? Session(sid: sessionId, pid: peerId);\n if (media != 'data') _localStream = await createStream(media);\n\n RTCPeerConnection peerConnection = await createPeerConnection({\n ..._iceServers,\n ...{'sdpSemantics': sdpSemantics}\n }, _dcConstraints);\n if (media != 'data') {\n switch (sdpSemantics) {\n case 'plan-b':\n peerConnection.onAddStream = (MediaStream stream) {\n onAddRemoteStream?.call(newSession, stream);\n _remoteStreams.add(stream);\n };\n await peerConnection.addStream(_localStream!);\n break;\n case 'unified-plan':\n // Unified-Plan\n peerConnection.onTrack = (event) {\n if (event.track.kind == 'video') {\n onAddRemoteStream?.call(newSession, event.streams[0]);\n } else if (event.track.kind == 'audio') {\n onAddRemoteStream?.call(newSession, event.streams[0]);\n }\n };\n _localStream!.getTracks().forEach((track) {\n peerConnection.addTrack(track, _localStream!);\n });\n break;\n }\n }\n peerConnection.onIceCandidate = (candidate) async {\n if (!candidate.candidate.toString().contains(\"127.0.0.1\")) {\n _logger.i(\"Peer :: Adding ICE candidate :: ${candidate.toString()}\");\n peerConnection.addCandidate(candidate);\n } else {\n _logger.i(\"Peer :: Local candidate skipped!\");\n }\n if (candidate.candidate == null) {\n _logger.i(\"Peer :: onIceCandidate: complete!\");\n return;\n }\n };\n\n peerConnection.onIceConnectionState = (state) {\n _logger.i(\"Peer :: ICE Connection State change :: $state\");\n switch (state) {\n case RTCIceConnectionState.RTCIceConnectionStateFailed:\n peerConnection.restartIce();\n return;\n default:\n return;\n }\n };\n\n peerConnection.onRemoveStream = (stream) {\n onRemoveRemoteStream?.call(newSession, stream);\n _remoteStreams.removeWhere((it) {\n return (it.id == stream.id);\n });\n };\n\n peerConnection.onDataChannel = (channel) {\n _addDataChannel(newSession, channel);\n };\n\n newSession.peerConnection = peerConnection;\n return newSession;\n }\n\n void _addDataChannel(Session session, RTCDataChannel channel) {\n channel.onDataChannelState = (e) {};\n channel.onMessage = (RTCDataChannelMessage data) {\n onDataChannelMessage?.call(session, channel, data);\n };\n session.dc = channel;\n onDataChannel?.call(session, channel);\n }\n\n /*Future _createDataChannel(Session session,\n {label = 'fileTransfer'}) async {\n RTCDataChannelInit dataChannelDict = RTCDataChannelInit()\n ..maxRetransmits = 30;\n RTCDataChannel channel =\n await session.peerConnection!.createDataChannel(label, dataChannelDict);\n _addDataChannel(session, channel);\n }*/\n\n _send(event) {\n _socket.send(event);\n }\n\n Future _cleanSessions() async {\n if (_localStream != null) {\n _localStream!.getTracks().forEach((element) async {\n await element.stop();\n });\n await _localStream!.dispose();\n _localStream = null;\n }\n _sessions.forEach((key, sess) async {\n await sess.peerConnection?.close();\n await sess.dc?.close();\n });\n _sessions.clear();\n }\n\n /*void _closeSessionByPeerId(String peerId) {\n Session? session;\n _sessions.removeWhere((String key, Session sess) {\n var ids = key.split('-');\n session = sess;\n return peerId == ids[0] || peerId == ids[1];\n });\n if (session != null) {\n _closeSession(session!);\n onCallStateChange?.call(session!, CallState.CallStateBye);\n }\n }*/\n\n Future _closeSession(Session session) async {\n _localStream?.getTracks().forEach((element) async {\n await element.stop();\n });\n await _localStream?.dispose();\n _localStream = null;\n\n await session.peerConnection?.close();\n await session.dc?.close();\n }\n}\n\nint randomBetween(int from, int to) {\n if (from > to) throw Exception('$from cannot be > $to');\n var rand = Random();\n return ((to - from) * rand.nextDouble()).toInt() + from;\n}\n\nString randomString(int length, {int from = 33, int to = 126}) {\n return String.fromCharCodes(\n List.generate(length, (index) => randomBetween(from, to)));\n}\n\nString randomNumeric(int length) => randomString(length, from: 48, to: 57);\n" + } + ] +} \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 02b94b2..a124604 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -5,6 +5,9 @@ PODS: - Flutter - audioplayers_darwin (0.0.1): - Flutter + - connectivity_plus (0.0.1): + - Flutter + - FlutterMacOS - CryptoSwift (1.8.2) - Firebase/CoreOnly (10.29.0): - FirebaseCore (= 10.29.0) @@ -42,8 +45,6 @@ PODS: - flutter_callkit_incoming (0.0.1): - CryptoSwift - Flutter - - flutter_local_notifications (0.0.1): - - Flutter - flutter_webrtc (0.11.3): - Flutter - WebRTC-SDK (= 125.6422.04) @@ -100,11 +101,11 @@ DEPENDENCIES: - assets_audio_player (from `.symlinks/plugins/assets_audio_player/ios`) - assets_audio_player_web (from `.symlinks/plugins/assets_audio_player_web/ios`) - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - flutter_callkit_incoming (from `.symlinks/plugins/flutter_callkit_incoming/ios`) - - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) @@ -133,6 +134,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/assets_audio_player_web/ios" audioplayers_darwin: :path: ".symlinks/plugins/audioplayers_darwin/ios" + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/darwin" firebase_core: :path: ".symlinks/plugins/firebase_core/ios" firebase_messaging: @@ -141,8 +144,6 @@ EXTERNAL SOURCES: :path: Flutter flutter_callkit_incoming: :path: ".symlinks/plugins/flutter_callkit_incoming/ios" - flutter_local_notifications: - :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_webrtc: :path: ".symlinks/plugins/flutter_webrtc/ios" fluttertoast: @@ -158,6 +159,7 @@ SPEC CHECKSUMS: assets_audio_player: edee322b9cb625571b830b35872ead1a295fd917 assets_audio_player_web: 19826380c44375761aa0b9053665c1e3fbc3b86b audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40 + connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563 CryptoSwift: c63a805d8bb5e5538e88af4e44bb537776af11ea Firebase: cec914dab6fd7b1bd8ab56ea07ce4e03dd251c2d firebase_core: 57aeb91680e5d5e6df6b888064be7c785f146efb @@ -168,7 +170,6 @@ SPEC CHECKSUMS: FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_callkit_incoming: 417dd1b46541cdd5d855ad795ccbe97d1c18155e - flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 07847ea..05d232d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -382,6 +382,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; @@ -516,6 +517,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; @@ -544,6 +546,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; diff --git a/lib/main.dart b/lib/main.dart index 38ea3c2..3af3769 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,8 +23,8 @@ import 'package:telnyx_webrtc/model/socket_method.dart'; final logger = Logger(); final mainViewModel = MainViewModel(); -const MOCK_USER = ""; -const MOCK_PASSWORD = ""; +const MOCK_USER = "isaac69601"; +const MOCK_PASSWORD = "DYVTuQ6V"; // Android Only - Push Notifications @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { diff --git a/packages/telnyx_webrtc/lib/call.dart b/packages/telnyx_webrtc/lib/call.dart index 1f92fa0..c4e7896 100644 --- a/packages/telnyx_webrtc/lib/call.dart +++ b/packages/telnyx_webrtc/lib/call.dart @@ -86,9 +86,9 @@ class Call { /// your local specified [callerName], [callerNumber] and [clientState] Call acceptCall(IncomingInviteParams invite, String callerName, String callerNumber, String clientState, - {Map customHeaders = const {}}) { + {bool isAttach = false, Map customHeaders = const {}}) { return _txClient.acceptCall(invite, callerName, callerNumber, clientState, - customHeaders: customHeaders); + customHeaders: customHeaders, isAttach: isAttach); } /// Attempts to end the call identified via the [callID] diff --git a/packages/telnyx_webrtc/lib/model/push_notification.dart b/packages/telnyx_webrtc/lib/model/push_notification.dart index 2b6bea3..bd1ad2f 100644 --- a/packages/telnyx_webrtc/lib/model/push_notification.dart +++ b/packages/telnyx_webrtc/lib/model/push_notification.dart @@ -9,7 +9,8 @@ class PushNotification { } class PushMetaData { - PushMetaData({this.caller_name, this.caller_number, this.call_id}); + PushMetaData( + {this.caller_name, this.caller_number, this.call_id, this.voice_sdk_id}); String? caller_name; String? caller_number; diff --git a/packages/telnyx_webrtc/lib/model/socket_method.dart b/packages/telnyx_webrtc/lib/model/socket_method.dart index 0539f64..9ed03fe 100644 --- a/packages/telnyx_webrtc/lib/model/socket_method.dart +++ b/packages/telnyx_webrtc/lib/model/socket_method.dart @@ -11,5 +11,5 @@ class SocketMethod { static const PING = "telnyx_rtc.ping"; static const LOGIN = "login"; static const ATTACH_CALL = "telnyx_rtc.attachCalls"; - + static const ATTACH = "telnyx_rtc.attach"; } diff --git a/packages/telnyx_webrtc/lib/model/verto/receive/received_message_body.dart b/packages/telnyx_webrtc/lib/model/verto/receive/received_message_body.dart index baf0ce4..f4f87a1 100644 --- a/packages/telnyx_webrtc/lib/model/verto/receive/received_message_body.dart +++ b/packages/telnyx_webrtc/lib/model/verto/receive/received_message_body.dart @@ -11,6 +11,8 @@ class ReceivedMessage { IncomingInviteParams? inviteParams; DialogParams? dialogParams; + String? voiceSdkId; + ReceivedMessage( {this.jsonrpc, this.id, @@ -18,7 +20,8 @@ class ReceivedMessage { this.reattachedParams, this.stateParams, this.inviteParams, - this.dialogParams}); + this.dialogParams, + this.voiceSdkId}); ReceivedMessage.fromJson(Map json) { jsonrpc = json['jsonrpc']; @@ -35,6 +38,10 @@ class ReceivedMessage { if (json['params']['dialogParams'] != null) { dialogParams = DialogParams.fromJson(json['params']['dialogParams']); } + if (json['voice_sdk_id'] != null) { + voiceSdkId = json['voice_sdk_id']; + Logger().i('Voice SDK ID: $voiceSdkId'); + } } Map toJson() { @@ -107,7 +114,7 @@ class ReattachedParams { ReattachedParams.fromJson(Map json) { if (json['reattached_sessions'] != null) { - reattachedSessions = []; + reattachedSessions = []; json['reattached_sessions'].forEach((v) { reattachedSessions!.add(v); }); diff --git a/packages/telnyx_webrtc/lib/peer/peer.dart b/packages/telnyx_webrtc/lib/peer/peer.dart index bc262d6..a6df528 100644 --- a/packages/telnyx_webrtc/lib/peer/peer.dart +++ b/packages/telnyx_webrtc/lib/peer/peer.dart @@ -202,7 +202,8 @@ class Peer { String clientState, String callId, IncomingInviteParams invite, - Map customHeaders) async { + Map customHeaders, + bool isAttach) async { var sessionId = _selfId; Session session = await _createSession(null, peerId: "0", sessionId: sessionId, media: "audio"); @@ -212,7 +213,7 @@ class Peer { ?.setRemoteDescription(RTCSessionDescription(invite.sdp, "offer")); _createAnswer(session, "audio", callerName, callerNumber, destinationNumber, - clientState, callId, customHeaders); + clientState, callId, customHeaders, isAttach); onCallStateChange?.call(session, CallState.active); } @@ -225,7 +226,8 @@ class Peer { String destinationNumber, String clientState, String callId, - Map customHeaders) async { + Map customHeaders, + bool isAttach) async { try { session.peerConnection?.onIceCandidate = (candidate) async { if (session.peerConnection != null) { @@ -272,7 +274,7 @@ class Peer { var answerMessage = InviteAnswerMessage( id: const Uuid().v4(), jsonrpc: JsonRPCConstant.jsonrpc, - method: SocketMethod.ANSWER, + method: isAttach ? SocketMethod.ATTACH : SocketMethod.ANSWER, params: inviteParams); String jsonAnswerMessage = jsonEncode(answerMessage); diff --git a/packages/telnyx_webrtc/lib/telnyx_client.dart b/packages/telnyx_webrtc/lib/telnyx_client.dart index 3e162bc..8fcef21 100644 --- a/packages/telnyx_webrtc/lib/telnyx_client.dart +++ b/packages/telnyx_webrtc/lib/telnyx_client.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:logger/logger.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:telnyx_webrtc/model/verto/send/attach_call_message.dart'; @@ -85,6 +86,47 @@ class TelnyxClient { String ringtonePath = ""; String ringBackpath = ""; PushMetaData? _pushMetaData; + bool _isAttaching = false; + + void checkReconnection() { + // Remember to cancel the subscription when it's no longer needed + StreamSubscription> subscription = Connectivity() + .onConnectivityChanged + .listen((List connectivityResult) { + if (connectivityResult.contains(ConnectivityResult.mobile)) { + _logger.i('Mobile network available.'); + if (activeCalls().isNotEmpty && !_isAttaching) { + _reconnectToSocket(); + } // Mobile network available. + } else if (connectivityResult.contains(ConnectivityResult.wifi)) { + _logger.i('Wi-fi is available.'); + if (activeCalls().isNotEmpty && !_isAttaching) { + _reconnectToSocket(); + } + // Wi-fi is available. + // Note for Android: + // When both mobile and Wi-Fi are turned on system will return Wi-Fi only as active network type + } else if (connectivityResult.contains(ConnectivityResult.ethernet)) { + _logger.i('Ethernet connection available.'); + // Ethernet connection available. + } else if (connectivityResult.contains(ConnectivityResult.vpn)) { + // Vpn connection active. + // Note for iOS and macOS: + _logger.i('Vpn connection active.'); + } else if (connectivityResult.contains(ConnectivityResult.bluetooth)) { + _logger.i('Bluetooth connection available.'); + // Bluetooth connection available. + } else if (connectivityResult.contains(ConnectivityResult.other)) { + _logger.i( + 'Connected to a network which is not in the above mentioned networks.'); + // Connected to a network which is not in the above mentioned networks. + } else if (connectivityResult.contains(ConnectivityResult.none)) { + _logger.i('No available network types'); + // No available network types + } + // Received changes in available connectivity types! + }); + } TelnyxClient() { // Default implementation of onSocketMessageReceived @@ -109,6 +151,8 @@ class TelnyxClient { _logger.i( 'TelnyxClient :: onSocketMessageReceived Override this on client side: $message'); }; + + checkReconnection(); } TxSocket txSocket = TxSocket("wss://rtc.telnyx.com:443"); @@ -123,6 +167,7 @@ class TelnyxClient { static const int RETRY_REGISTER_TIME = 3; static const int RETRY_CONNECT_TIME = 3; static const int GATEWAY_RESPONSE_DELAY = 3000; + static const int RECONNECT_TIMER = 1000; Timer? _gatewayResponseTimer; bool _autoReconnectLogin = true; @@ -207,6 +252,9 @@ class TelnyxClient { void _connectWithCallBack( PushMetaData? pushMetaData, OnOpenCallback openCallback) { _logger.i('connect() ${pushMetaData?.toJson()}'); + if (pushMetaData != null) { + _pushMetaData = pushMetaData; + } try { if (pushMetaData?.voice_sdk_id != null) { txSocket.hostAddress = @@ -369,8 +417,12 @@ class TelnyxClient { } void _reconnectToSocket() { + _isAttaching = true; + Timer(const Duration(milliseconds: GATEWAY_RESPONSE_DELAY), () { + _isAttaching = false; + }); + txSocket.close(); - txSocket.connect(); // Delay to allow connection Timer(const Duration(seconds: 1), () { if (storedCredentialConfig != null) { @@ -444,6 +496,7 @@ class TelnyxClient { login: user, passwd: password, loginParams: [], + sessionId: sessid, userVariables: notificationParams, attachCall: "true", ); @@ -457,7 +510,7 @@ class TelnyxClient { if (isConnected()) { txSocket.send(jsonLoginMessage); } else { - _connectWithCallBack(null, () { + _connectWithCallBack(_pushMetaData, () { txSocket.send(jsonLoginMessage); }); } @@ -544,12 +597,13 @@ class TelnyxClient { /// your local specified [callerName], [callerNumber] and [clientState] Call acceptCall(IncomingInviteParams invite, String callerName, String callerNumber, String clientState, - {Map customHeaders = const {}}) { + {bool isAttach = false, Map customHeaders = const {}}) { Call answerCall = getCallOrNull(invite.callID!) ?? _createCall(); answerCall.callId = invite.callID; answerCall.sessionCallerName = callerName; answerCall.sessionCallerNumber = callerNumber; + answerCall.callState = CallState.active; answerCall.sessionDestinationNumber = invite.callerIdName ?? "Unknown Caller"; answerCall.sessionClientState = clientState; @@ -558,7 +612,7 @@ class TelnyxClient { answerCall.peerConnection = Peer(txSocket); answerCall.peerConnection?.accept(callerName, callerNumber, destinationNum!, - clientState, answerCall.callId!, invite, customHeaders); + clientState, answerCall.callId!, invite, customHeaders, isAttach); answerCall.callHandler.changeState(CallState.active, answerCall); answerCall.stopAudio(); if (answerCall.callId != null) { @@ -769,6 +823,18 @@ class TelnyxClient { } else if (data.toString().trim().contains("method")) { //Received Telnyx Method Message var messageJson = jsonDecode(data.toString()); + + ReceivedMessage clientReadyMessage = + ReceivedMessage.fromJson(jsonDecode(data.toString())); + if (clientReadyMessage.voiceSdkId != null) { + _logger.i('VoiceSdkID :: ${clientReadyMessage.voiceSdkId}'); + _pushMetaData = PushMetaData( + caller_number: null, + caller_name: null, + voice_sdk_id: clientReadyMessage.voiceSdkId); + } else { + _logger.e('VoiceSdkID not found'); + } _logger.i( 'Received WebSocket message - Contains Method :: $messageJson'); switch (messageJson['method']) { @@ -829,6 +895,7 @@ class TelnyxClient { Call offerCall = _createCall(); offerCall.callId = invite.inviteParams?.callID; updateCall(offerCall); + onSocketMessageReceived.call(message); offerCall.callHandler @@ -852,6 +919,32 @@ class TelnyxClient { offerCall.callHandler.changeState(CallState.done, offerCall); _pendingDeclineFromPush = false; } + break; + } + case SocketMethod.ATTACH: + { + _logger.i('ATTACH RECEIVED :: $messageJson'); + _logger.i('INCOMING INVITATION :: $messageJson'); + ReceivedMessage invite = + ReceivedMessage.fromJson(jsonDecode(data.toString())); + var message = TelnyxMessage( + socketMethod: SocketMethod.INVITE, message: invite); + //play ringtone for web + Call offerCall = _createCall(); + offerCall.callId = invite.inviteParams?.callID; + updateCall(offerCall); + + onSocketMessageReceived.call(message); + + offerCall.acceptCall( + invite.inviteParams!, + invite.inviteParams!.calleeIdName ?? "", + invite.inviteParams!.callerIdNumber ?? "", + "State", + isAttach: true); + _pendingAnswerFromPush = false; + // offerCall.callHandler.changeState(CallState.active, offerCall); + break; } case SocketMethod.MEDIA: diff --git a/packages/telnyx_webrtc/pubspec.yaml b/packages/telnyx_webrtc/pubspec.yaml index 8bde5b9..7925a0d 100644 --- a/packages/telnyx_webrtc/pubspec.yaml +++ b/packages/telnyx_webrtc/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: audioplayers: ^5.2.1 assets_audio_player: ^3.1.1 shared_preferences: ^2.2.3 + connectivity_plus: ^6.1.0 dev_dependencies: flutter_test: diff --git a/pubspec.yaml b/pubspec.yaml index f49d0ab..d01f50a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: flutter_callkit_incoming: ^2.0.4+1 shared_preferences: ^2.2.3 permission_handler: ^11.3.1 + connectivity_plus: ^6.1.0 dev_dependencies: