diff --git a/.lh/packages/telnyx_webrtc/lib/peer/peer.dart.json b/.lh/packages/telnyx_webrtc/lib/peer/peer.dart.json index 747ce61..63303f9 100644 --- a/.lh/packages/telnyx_webrtc/lib/peer/peer.dart.json +++ b/.lh/packages/telnyx_webrtc/lib/peer/peer.dart.json @@ -3,7 +3,7 @@ "activeCommit": 0, "commits": [ { - "activePatchIndex": 1, + "activePatchIndex": 7, "patches": [ { "date": 1730809569544, @@ -12,6 +12,30 @@ { "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": 1732094808505, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -284,8 +284,20 @@\n _logger.e(\"Peer :: $e\");\n }\n }\n \n+ Future getStats(RTCPeerConnection peerConnection) async {\n+ peerConnection.getStats(null).then((value) {\n+ value.forEach((report) {\n+ _logger.i(\"Stats: {\");\n+ report.values.forEach((key, value) {\n+ _logger.i(\"$key: $value\");\n+ });\n+ _logger.i(\"}\");\n+ });\n+ });\n+ }\n+\n void closeSession() {\n var sess = _sessions[_selfId];\n if (sess != null) {\n _logger.d(\"Session end success\");\n" + }, + { + "date": 1732103558178, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -201,9 +201,10 @@\n String destinationNumber,\n String clientState,\n String callId,\n IncomingInviteParams invite,\n- Map customHeaders,bool isAttach) async {\n+ Map customHeaders,\n+ 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@@ -211,9 +212,9 @@\n await session.peerConnection\n ?.setRemoteDescription(RTCSessionDescription(invite.sdp, \"offer\"));\n \n _createAnswer(session, \"audio\", callerName, callerNumber, destinationNumber,\n- clientState, callId, customHeaders,isAttach);\n+ clientState, callId, customHeaders, isAttach);\n \n onCallStateChange?.call(session, CallState.active);\n }\n \n@@ -224,9 +225,10 @@\n String callerNumber,\n String destinationNumber,\n String clientState,\n String callId,\n- Map customHeaders,bool isAttach) async {\n+ Map customHeaders,\n+ bool isAttach) async {\n try {\n session.peerConnection?.onIceCandidate = (candidate) async {\n if (session.peerConnection != null) {\n _logger.i(\"Peer :: Add Ice Candidate!\");\n" + }, + { + "date": 1732103618535, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -372,8 +372,11 @@\n switch (state) {\n case RTCIceConnectionState.RTCIceConnectionStateFailed:\n peerConnection.restartIce();\n return;\n+ case RTCIceConnectionState.RTCIceConnectionStateConnected:\n+ getStats(peerConnection);\n+ return;\n default:\n return;\n }\n };\n" + }, + { + "date": 1732194107233, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -5,8 +5,9 @@\n import 'package:flutter_webrtc/flutter_webrtc.dart';\n import 'package:telnyx_webrtc/config.dart';\n import 'package:telnyx_webrtc/model/socket_method.dart';\n import 'package:telnyx_webrtc/model/verto/send/invite_answer_message_body.dart';\n+import 'package:telnyx_webrtc/stats/statsmanager.dart';\n import 'package:telnyx_webrtc/tx_socket.dart'\n if (dart.library.js) 'package:telnyx_webrtc/tx_socket_web.dart';\n import 'package:uuid/uuid.dart';\n import 'package:logger/logger.dart';\n@@ -38,8 +39,10 @@\n final String _selfId = randomNumeric(6);\n \n final TxSocket _socket;\n \n+ StatsManager? _statsManager;\n+\n final Map _sessions = {};\n MediaStream? _localStream;\n final List _remoteStreams = [];\n \n@@ -112,9 +115,9 @@\n Map customHeaders) async {\n var sessionId = _selfId;\n \n Session session = await _createSession(null,\n- peerId: \"0\", sessionId: sessionId, media: \"audio\");\n+ peerId: \"0\", sessionId: sessionId,callId: callId, media: \"audio\");\n \n _sessions[sessionId] = session;\n \n _createOffer(session, \"audio\", callerName, callerNumber, destinationNumber,\n@@ -205,9 +208,9 @@\n Map customHeaders,\n bool isAttach) async {\n var sessionId = _selfId;\n Session session = await _createSession(null,\n- peerId: \"0\", sessionId: sessionId, media: \"audio\");\n+ peerId: \"0\", sessionId: sessionId,callId: callId, media: \"audio\");\n _sessions[sessionId] = session;\n \n await session.peerConnection\n ?.setRemoteDescription(RTCSessionDescription(invite.sdp, \"offer\"));\n@@ -284,19 +287,8 @@\n _logger.e(\"Peer :: $e\");\n }\n }\n \n- Future getStats(RTCPeerConnection peerConnection) async {\n- peerConnection.getStats(null).then((value) {\n- value.forEach((report) {\n- _logger.i(\"Stats: {\");\n- report.values.forEach((key, value) {\n- _logger.i(\"$key: $value\");\n- });\n- _logger.i(\"}\");\n- });\n- });\n- }\n \n void closeSession() {\n var sess = _sessions[_selfId];\n if (sess != null) {\n@@ -321,8 +313,9 @@\n \n Future _createSession(Session? session,\n {required String peerId,\n required String sessionId,\n+ required String callId,\n required String media}) async {\n var newSession = session ?? Session(sid: sessionId, pid: peerId);\n if (media != 'data') _localStream = await createStream(media);\n \n@@ -373,9 +366,10 @@\n case RTCIceConnectionState.RTCIceConnectionStateFailed:\n peerConnection.restartIce();\n return;\n case RTCIceConnectionState.RTCIceConnectionStateConnected:\n- getStats(peerConnection);\n+ _statsManager = StatsManager(_socket, peerConnection,callId);\n+ _statsManager?.startTimer();\n return;\n default:\n return;\n }\n" + }, + { + "date": 1732194650698, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -38,8 +38,9 @@\n \n final String _selfId = randomNumeric(6);\n \n final TxSocket _socket;\n+ bool isDebugStats = false;\n \n StatsManager? _statsManager;\n \n final Map _sessions = {};\n@@ -365,9 +366,8 @@\n case RTCIceConnectionState.RTCIceConnectionStateFailed:\n peerConnection.restartIce();\n return;\n case RTCIceConnectionState.RTCIceConnectionStateConnected:\n- _statsManager = StatsManager(_socket, peerConnection, callId);\n _statsManager?.startTimer();\n return;\n default:\n return;\n@@ -397,8 +397,14 @@\n session.dc = channel;\n onDataChannel?.call(session, channel);\n }\n \n+ void startStats(String debugReportId) {\n+ isDebugStats = true;\n+ _statsManager = StatsManager(_socket, peerConnection, callId);\n+ _statsManager?.startStats(debugReportId);\n+ }\n+\n /*Future _createDataChannel(Session session,\n {label = 'fileTransfer'}) async {\n RTCDataChannelInit dataChannelDict = RTCDataChannelInit()\n ..maxRetransmits = 30;\n" + }, + { + "date": 1732195341173, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -41,8 +41,9 @@\n final TxSocket _socket;\n bool isDebugStats = false;\n \n StatsManager? _statsManager;\n+ RTCPeerConnection? peerConnection;\n \n final Map _sessions = {};\n MediaStream? _localStream;\n final List _remoteStreams = [];\n@@ -318,40 +319,40 @@\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+ 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+ peerConnection?.onAddStream = (MediaStream stream) {\n onAddRemoteStream?.call(newSession, stream);\n _remoteStreams.add(stream);\n };\n- await peerConnection.addStream(_localStream!);\n+ await peerConnection?.addStream(_localStream!);\n break;\n case 'unified-plan':\n // Unified-Plan\n- peerConnection.onTrack = (event) {\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+ peerConnection?.addTrack(track, _localStream!);\n });\n break;\n }\n }\n- peerConnection.onIceCandidate = (candidate) async {\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+ peerConnection?.addCandidate(candidate);\n } else {\n _logger.i(\"Peer :: Local candidate skipped!\");\n }\n if (candidate.candidate == null) {\n@@ -359,30 +360,30 @@\n return;\n }\n };\n \n- peerConnection.onIceConnectionState = (state) {\n+ peerConnection?.onIceConnectionState = (state) {\n _logger.i(\"Peer :: ICE Connection State change :: $state\");\n switch (state) {\n case RTCIceConnectionState.RTCIceConnectionStateFailed:\n- peerConnection.restartIce();\n+ peerConnection?.restartIce();\n return;\n- case RTCIceConnectionState.RTCIceConnectionStateConnected:\n- _statsManager?.startTimer();\n+ case RTCIceConnectionState.RTCIceConnectionStateDisconnected:\n+ _statsManager?.stopTimer();\n return;\n default:\n return;\n }\n };\n \n- peerConnection.onRemoveStream = (stream) {\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+ peerConnection?.onDataChannel = (channel) {\n _addDataChannel(newSession, channel);\n };\n \n newSession.peerConnection = peerConnection;\n@@ -397,12 +398,12 @@\n session.dc = channel;\n onDataChannel?.call(session, channel);\n }\n \n- void startStats(String debugReportId) {\n+ void startStats(String callId) {\n isDebugStats = true;\n- _statsManager = StatsManager(_socket, peerConnection, callId);\n- _statsManager?.startStats(debugReportId);\n+ _statsManager = StatsManager(_socket, peerConnection!, callId);\n+ _statsManager?.startTimer();\n }\n \n /*Future _createDataChannel(Session session,\n {label = 'fileTransfer'}) async {\n" } ], "date": 1730809569544, diff --git a/.lh/packages/telnyx_webrtc/lib/stats/stats_params.dart.json b/.lh/packages/telnyx_webrtc/lib/stats/stats_params.dart.json new file mode 100644 index 0000000..09b0c68 --- /dev/null +++ b/.lh/packages/telnyx_webrtc/lib/stats/stats_params.dart.json @@ -0,0 +1,18 @@ +{ + "sourceFile": "packages/telnyx_webrtc/lib/stats/stats_params.dart", + "activeCommit": 0, + "commits": [ + { + "activePatchIndex": 0, + "patches": [ + { + "date": 1732172924923, + "content": "Index: \n===================================================================\n--- \n+++ \n" + } + ], + "date": 1732172924923, + "name": "Commit-0", + "content": "import 'package:uuid/uuid.dart';\n\nclass StatParams {\n final String type;\n final String debugReportId;\n final Map reportData;\n final int debugReportVersion;\n final String id;\n final String jsonrpc;\n\n StatParams({\n this.type = \"debug_report_data\",\n required this.debugReportId,\n required this.reportData,\n this.debugReportVersion = 1,\n String? id,\n this.jsonrpc = \"2.0\",\n }) : id = id ?? const Uuid().v4();\n\n Map toJson() {\n return {\n \"type\": type,\n \"debug_report_id\": debugReportId,\n \"debug_report_data\": reportData,\n \"debug_report_version\": debugReportVersion,\n \"id\": id,\n \"jsonrpc\": jsonrpc,\n };\n }\n}\n\nclass InitiateOrStopStatParams {\n final String type;\n final String debugReportId;\n final int debugReportVersion;\n final String id;\n final String jsonrpc;\n\n InitiateOrStopStatParams({\n required this.type,\n required this.debugReportId,\n this.debugReportVersion = 1,\n String? id,\n this.jsonrpc = \"2.0\",\n }) : id = id ?? Uuid().v4();\n\n Map toJson() {\n return {\n \"type\": type,\n \"debug_report_id\": debugReportId,\n \"debug_report_version\": debugReportVersion,\n \"id\": id,\n \"jsonrpc\": jsonrpc,\n };\n }\n}\n" + } + ] +} \ No newline at end of file diff --git a/.lh/packages/telnyx_webrtc/lib/stats/statsmanager.dart.json b/.lh/packages/telnyx_webrtc/lib/stats/statsmanager.dart.json new file mode 100644 index 0000000..02e4376 --- /dev/null +++ b/.lh/packages/telnyx_webrtc/lib/stats/statsmanager.dart.json @@ -0,0 +1,42 @@ +{ + "sourceFile": "packages/telnyx_webrtc/lib/stats/statsmanager.dart", + "activeCommit": 0, + "commits": [ + { + "activePatchIndex": 6, + "patches": [ + { + "date": 1732105758108, + "content": "Index: \n===================================================================\n--- \n+++ \n" + }, + { + "date": 1732113058069, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1,10 +1,16 @@\n import 'dart:async';\n import 'dart:convert';\n+import 'package:flutter_webrtc/flutter_webrtc.dart';\n+import 'package:telnyx_webrtc/stats/stats_params.dart';\n+import 'package:telnyx_webrtc/tx_socket.dart';\n import 'package:uuid/uuid.dart';\n \n class StatsManager {\n+ StatsManager(this.socket,this.peerConnection,this.callId) \n+\n final Timer? _timer = null;\n+ bool debugReportStarted = false;\n final Uuid uuid = Uuid();\n final int STATS_INITIAL = 1000; // Replace with appropriate initial delay\n final int STATS_INTERVAL = 1000; // Replace with appropriate interval\n final int CANDIDATE_LIMIT = 10; // Adjust as needed\n@@ -19,25 +25,25 @@\n String? debugStatsId;\n bool isDebugStats = false;\n \n // Placeholder for `client` and `peerConnection`, replace with actual implementation.\n- final dynamic client = null;\n- final dynamic peerConnection = null;\n+ final TxSocket socket;\n+ final RTCPeerConnection peerConnection;\n final String callId = 'sampleCallId'; // Replace with actual callId.\n \n void stopTimer() {\n- client?.stopStats(debugStatsId);\n+ socket?.stopStats(debugStatsId);\n debugStatsId = null;\n mainObject = {};\n _timer?.cancel();\n }\n \n void startTimer() {\n isDebugStats = true;\n \n- if (client != null && !(client.debugReportStarted ?? false)) {\n+ if (socket != null && !(socket.debugReportStarted ?? false)) {\n debugStatsId = uuid.v4();\n- client?.startStats(debugStatsId);\n+ socket?.startStats(debugStatsId);\n }\n \n Timer.periodic(Duration(milliseconds: STATS_INTERVAL), (timer) {\n mainObject = {\n@@ -86,11 +92,39 @@\n \n print(\"Stats Inbound: ${jsonEncode(mainObject)}\");\n \n if (debugStatsId != null) {\n- client?.sendStats(mainObject, debugStatsId);\n+ socket?.sendStats(mainObject, debugStatsId);\n }\n }\n });\n });\n }\n+\n+\n+ void startStats(String sessionId) {\n+ debugReportStarted = true;\n+ var loginMessage = InitiateOrStopStatParams(\n+ type: \"debug_report_start\",\n+ debugReportId: sessionId,\n+ );\n+ socket.send(jsonEncode(loginMessage.toJson()));\n+ }\n+\n+ void sendStats(Map data, String sessionId) {\n+ var statParams = StatParams(\n+ debugReportId: sessionId,\n+ reportData: data,\n+ );\n+ socket.send(jsonEncode(statParams.toJson()));\n+ }\n+\n+ void stopStats(String sessionId) {\n+ debugReportStarted = false;\n+ var loginMessage = InitiateOrStopStatParams(\n+ type: \"debug_report_stop\",\n+ debugReportId: sessionId,\n+ );\n+ socket.send(jsonEncode(loginMessage.toJson()));\n+ }\n+\n }\n" + }, + { + "date": 1732115013817, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -8,9 +8,10 @@\n class StatsManager {\n StatsManager(this.socket,this.peerConnection,this.callId) \n \n final Timer? _timer = null;\n- bool debugReportStarted = false;\n+ bool debugReportStarted = false;\n+ final String callId;\n final Uuid uuid = Uuid();\n final int STATS_INITIAL = 1000; // Replace with appropriate initial delay\n final int STATS_INTERVAL = 1000; // Replace with appropriate interval\n final int CANDIDATE_LIMIT = 10; // Adjust as needed\n@@ -41,9 +42,9 @@\n isDebugStats = true;\n \n if (socket != null && !(socket.debugReportStarted ?? false)) {\n debugStatsId = uuid.v4();\n- socket?.startStats(debugStatsId);\n+ startStats(debugStatsId!);\n }\n \n Timer.periodic(Duration(milliseconds: STATS_INTERVAL), (timer) {\n mainObject = {\n@@ -52,10 +53,10 @@\n \"peerId\": \"stats\",\n \"connectionId\": callId,\n };\n \n- peerConnection?.getStats((stats) {\n- stats['statsMap']?.forEach((key, value) {\n+peerConnection.getStats(null).then((value) {\n+ stats['statsMap']?.forEach((key, value) {\n if (value['type'] == 'inbound-rtp') {\n inBoundStats.add(value);\n }\n if (value['type'] == 'outbound-rtp') {\n@@ -96,8 +97,10 @@\n socket?.sendStats(mainObject, debugStatsId);\n }\n }\n });\n+ \n+ \n });\n }\n \n \n" + }, + { + "date": 1732116892090, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -53,10 +53,13 @@\n \"peerId\": \"stats\",\n \"connectionId\": callId,\n };\n \n-peerConnection.getStats(null).then((value) {\n- stats['statsMap']?.forEach((key, value) {\n+ peerConnection.getStats(null).then((stats) {\n+ stats.forEach((report) {\n+\n+ report.values.forEach((key, value) {\n+ \n if (value['type'] == 'inbound-rtp') {\n inBoundStats.add(value);\n }\n if (value['type'] == 'outbound-rtp') {\n@@ -65,8 +68,9 @@\n if (value['type'] == 'candidate-pair' &&\n candidatePairs.length < CANDIDATE_LIMIT) {\n candidatePairs.add(value);\n }\n+ });\n });\n \n audio = {\n \"inbound\": inBoundStats,\n" + }, + { + "date": 1732116920203, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -54,22 +54,22 @@\n \"connectionId\": callId,\n };\n \n peerConnection.getStats(null).then((stats) {\n- stats.forEach((report) {\n+ stats.forEach((report) {\n \n report.values.forEach((key, value) {\n \n- if (value['type'] == 'inbound-rtp') {\n- inBoundStats.add(value);\n- }\n- if (value['type'] == 'outbound-rtp') {\n- outBoundStats.add(value);\n- }\n- if (value['type'] == 'candidate-pair' &&\n- candidatePairs.length < CANDIDATE_LIMIT) {\n- candidatePairs.add(value);\n- }\n+ if (value['type'] == 'inbound-rtp') {\n+ inBoundStats.add(value);\n+ }\n+ if (value['type'] == 'outbound-rtp') {\n+ outBoundStats.add(value);\n+ }\n+ if (value['type'] == 'candidate-pair' &&\n+ candidatePairs.length < CANDIDATE_LIMIT) {\n+ candidatePairs.add(value);\n+ }\n });\n });\n \n audio = {\n" + }, + { + "date": 1732190410632, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -22,29 +22,29 @@\n List inBoundStats = [];\n List outBoundStats = [];\n List candidatePairs = [];\n \n- String? debugStatsId;\n+ String debugStatsId = const Uuid().v4();\n bool isDebugStats = false;\n \n // Placeholder for `client` and `peerConnection`, replace with actual implementation.\n final TxSocket socket;\n final RTCPeerConnection peerConnection;\n final String callId = 'sampleCallId'; // Replace with actual callId.\n \n void stopTimer() {\n- socket?.stopStats(debugStatsId);\n+ stopStats(debugStatsId);\n debugStatsId = null;\n mainObject = {};\n _timer?.cancel();\n }\n \n void startTimer() {\n isDebugStats = true;\n \n- if (socket != null && !(socket.debugReportStarted ?? false)) {\n- debugStatsId = uuid.v4();\n- startStats(debugStatsId!);\n+ if (!debugReportStarted) {\n+ debugStatsId = uuid.v4();\n+ startStats(debugStatsId);\n }\n \n Timer.periodic(Duration(milliseconds: STATS_INTERVAL), (timer) {\n mainObject = {\n@@ -82,8 +82,9 @@\n \"audio\": audio,\n };\n \n mainObject[\"data\"] = statsData;\n+ mainObject[\"data\"] = statsData;\n mainObject[\"timestamp\"] = DateTime.now().millisecondsSinceEpoch;\n \n if (inBoundStats.isNotEmpty &&\n outBoundStats.isNotEmpty &&\n@@ -97,9 +98,9 @@\n \n print(\"Stats Inbound: ${jsonEncode(mainObject)}\");\n \n if (debugStatsId != null) {\n- socket?.sendStats(mainObject, debugStatsId);\n+ sendStats(mainObject, debugStatsId);\n }\n }\n });\n \n" + }, + { + "date": 1732190422706, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -82,9 +82,9 @@\n \"audio\": audio,\n };\n \n mainObject[\"data\"] = statsData;\n- mainObject[\"data\"] = statsData;\n+ mainObject[\"connectionId\"] = callId;\n mainObject[\"timestamp\"] = DateTime.now().millisecondsSinceEpoch;\n \n if (inBoundStats.isNotEmpty &&\n outBoundStats.isNotEmpty &&\n" + } + ], + "date": 1732105758108, + "name": "Commit-0", + "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'package:uuid/uuid.dart';\n\nclass StatsManager {\n final Timer? _timer = null;\n final Uuid uuid = Uuid();\n final int STATS_INITIAL = 1000; // Replace with appropriate initial delay\n final int STATS_INTERVAL = 1000; // Replace with appropriate interval\n final int CANDIDATE_LIMIT = 10; // Adjust as needed\n\n Map mainObject = {};\n Map audio = {};\n Map statsData = {};\n List inBoundStats = [];\n List outBoundStats = [];\n List candidatePairs = [];\n \n String? debugStatsId;\n bool isDebugStats = false;\n\n // Placeholder for `client` and `peerConnection`, replace with actual implementation.\n final dynamic client = null;\n final dynamic peerConnection = null;\n final String callId = 'sampleCallId'; // Replace with actual callId.\n\n void stopTimer() {\n client?.stopStats(debugStatsId);\n debugStatsId = null;\n mainObject = {};\n _timer?.cancel();\n }\n\n void startTimer() {\n isDebugStats = true;\n\n if (client != null && !(client.debugReportStarted ?? false)) {\n debugStatsId = uuid.v4();\n client?.startStats(debugStatsId);\n }\n\n Timer.periodic(Duration(milliseconds: STATS_INTERVAL), (timer) {\n mainObject = {\n \"event\": \"stats\",\n \"tag\": \"stats\",\n \"peerId\": \"stats\",\n \"connectionId\": callId,\n };\n\n peerConnection?.getStats((stats) {\n stats['statsMap']?.forEach((key, value) {\n if (value['type'] == 'inbound-rtp') {\n inBoundStats.add(value);\n }\n if (value['type'] == 'outbound-rtp') {\n outBoundStats.add(value);\n }\n if (value['type'] == 'candidate-pair' &&\n candidatePairs.length < CANDIDATE_LIMIT) {\n candidatePairs.add(value);\n }\n });\n\n audio = {\n \"inbound\": inBoundStats,\n \"outbound\": outBoundStats,\n \"candidatePair\": candidatePairs,\n };\n\n statsData = {\n \"audio\": audio,\n };\n\n mainObject[\"data\"] = statsData;\n mainObject[\"timestamp\"] = DateTime.now().millisecondsSinceEpoch;\n\n if (inBoundStats.isNotEmpty &&\n outBoundStats.isNotEmpty &&\n candidatePairs.isNotEmpty) {\n // Reset for next interval\n inBoundStats = [];\n outBoundStats = [];\n candidatePairs = [];\n statsData = {};\n audio = {};\n\n print(\"Stats Inbound: ${jsonEncode(mainObject)}\");\n\n if (debugStatsId != null) {\n client?.sendStats(mainObject, debugStatsId);\n }\n }\n });\n });\n }\n}\n" + } + ] +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 7059632..3aa4bb1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -24,7 +24,8 @@ import 'package:telnyx_webrtc/model/socket_method.dart'; final logger = Logger(); final mainViewModel = MainViewModel(); const MOCK_USER = ""; -const MOCK_PASSWORD = "MOCK_PASSWORD"; +const MOCK_PASSWORD = ""; +const CALL_MISSED_TIMEOUT = 30; // Android Only - Push Notifications @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { diff --git a/lib/main_view_model.dart b/lib/main_view_model.dart index 93e94d3..3377a6c 100644 --- a/lib/main_view_model.dart +++ b/lib/main_view_model.dart @@ -213,6 +213,7 @@ class MainViewModel with ChangeNotifier { _localName, _localNumber, destination, "Fake State", customHeaders: {"X-Header-1": "Value1", "X-Header-2": "Value2"}); observeCurrentCall(); + _currentCall?.startDebugStats(); } void toggleSpeakerPhone() { @@ -228,6 +229,8 @@ class MainViewModel with ChangeNotifier { _currentCall = _telnyxClient.acceptCall( _incomingInvite!, _localName, _localNumber, "State"); + _currentCall?.startDebugStats(); + if (Platform.isIOS) { // only for iOS FlutterCallkitIncoming.setCallConnected(_incomingInvite!.callID!); @@ -256,7 +259,6 @@ class MainViewModel with ChangeNotifier { // Hide notfication when call is accepted FlutterCallkitIncoming.hideCallkitIncoming(callKitParams); } - notifyListeners(); } else { waitingForInvite = true; diff --git a/packages/telnyx_webrtc/lib/call.dart b/packages/telnyx_webrtc/lib/call.dart index c4e7896..384051a 100644 --- a/packages/telnyx_webrtc/lib/call.dart +++ b/packages/telnyx_webrtc/lib/call.dart @@ -179,6 +179,16 @@ class Call { peerConnection?.enableSpeakerPhone(enable); } + Future startDebugStats() async { + if (peerConnection != null) { + _logger.d("Peer connection debug started for $callId"); + return await peerConnection?.startStats(callId ?? "") ?? false; + } else { + _logger.d("Peer connection null"); + return false; + } + } + /// Either places the call on hold, or unholds the call based on the current /// hold state. void onHoldUnholdPressed() { diff --git a/packages/telnyx_webrtc/lib/peer/peer.dart b/packages/telnyx_webrtc/lib/peer/peer.dart index a6df528..fd889d5 100644 --- a/packages/telnyx_webrtc/lib/peer/peer.dart +++ b/packages/telnyx_webrtc/lib/peer/peer.dart @@ -6,6 +6,7 @@ import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:telnyx_webrtc/config.dart'; import 'package:telnyx_webrtc/model/socket_method.dart'; import 'package:telnyx_webrtc/model/verto/send/invite_answer_message_body.dart'; +import 'package:telnyx_webrtc/stats/statsmanager.dart'; import 'package:telnyx_webrtc/tx_socket.dart' if (dart.library.js) 'package:telnyx_webrtc/tx_socket_web.dart'; import 'package:uuid/uuid.dart'; @@ -31,6 +32,10 @@ class Session { } class Peer { + RTCPeerConnection? peerConnection; + + final debugStatsDelay = const Duration(milliseconds: 20000); + Peer(this._socket); final _logger = Logger(); @@ -38,6 +43,7 @@ class Peer { final String _selfId = randomNumeric(6); final TxSocket _socket; + StatsManager? _statsManager; final Map _sessions = {}; MediaStream? _localStream; @@ -113,7 +119,7 @@ class Peer { var sessionId = _selfId; Session session = await _createSession(null, - peerId: "0", sessionId: sessionId, media: "audio"); + peerId: "0", sessionId: sessionId, callId: callId, media: "audio"); _sessions[sessionId] = session; @@ -206,7 +212,7 @@ class Peer { bool isAttach) async { var sessionId = _selfId; Session session = await _createSession(null, - peerId: "0", sessionId: sessionId, media: "audio"); + peerId: "0", sessionId: sessionId, callId: callId, media: "audio"); _sessions[sessionId] = session; await session.peerConnection @@ -310,26 +316,28 @@ class Peer { Future _createSession(Session? session, {required String peerId, required String sessionId, + required String callId, required String media}) async { var newSession = session ?? Session(sid: sessionId, pid: peerId); if (media != 'data') _localStream = await createStream(media); - RTCPeerConnection peerConnection = await createPeerConnection({ + peerConnection = await createPeerConnection({ ..._iceServers, ...{'sdpSemantics': sdpSemantics} }, _dcConstraints); + if (media != 'data') { switch (sdpSemantics) { case 'plan-b': - peerConnection.onAddStream = (MediaStream stream) { + peerConnection?.onAddStream = (MediaStream stream) { onAddRemoteStream?.call(newSession, stream); _remoteStreams.add(stream); }; - await peerConnection.addStream(_localStream!); + await peerConnection?.addStream(_localStream!); break; case 'unified-plan': // Unified-Plan - peerConnection.onTrack = (event) { + peerConnection?.onTrack = (event) { if (event.track.kind == 'video') { onAddRemoteStream?.call(newSession, event.streams[0]); } else if (event.track.kind == 'audio') { @@ -337,15 +345,16 @@ class Peer { } }; _localStream!.getTracks().forEach((track) { - peerConnection.addTrack(track, _localStream!); + peerConnection?.addTrack(track, _localStream!); }); break; } } - peerConnection.onIceCandidate = (candidate) async { + + peerConnection?.onIceCandidate = (candidate) async { if (!candidate.candidate.toString().contains("127.0.0.1")) { _logger.i("Peer :: Adding ICE candidate :: ${candidate.toString()}"); - peerConnection.addCandidate(candidate); + peerConnection?.addCandidate(candidate); } else { _logger.i("Peer :: Local candidate skipped!"); } @@ -355,25 +364,28 @@ class Peer { } }; - peerConnection.onIceConnectionState = (state) { + peerConnection?.onIceConnectionState = (state) { _logger.i("Peer :: ICE Connection State change :: $state"); switch (state) { case RTCIceConnectionState.RTCIceConnectionStateFailed: - peerConnection.restartIce(); + peerConnection?.restartIce(); + return; + case RTCIceConnectionState.RTCIceConnectionStateDisconnected: + _statsManager?.stopTimer(); return; default: return; } }; - peerConnection.onRemoveStream = (stream) { + peerConnection?.onRemoveStream = (stream) { onRemoveRemoteStream?.call(newSession, stream); _remoteStreams.removeWhere((it) { return (it.id == stream.id); }); }; - peerConnection.onDataChannel = (channel) { + peerConnection?.onDataChannel = (channel) { _addDataChannel(newSession, channel); }; @@ -390,14 +402,21 @@ class Peer { onDataChannel?.call(session, channel); } - /*Future _createDataChannel(Session session, - {label = 'fileTransfer'}) async { - RTCDataChannelInit dataChannelDict = RTCDataChannelInit() - ..maxRetransmits = 30; - RTCDataChannel channel = - await session.peerConnection!.createDataChannel(label, dataChannelDict); - _addDataChannel(session, channel); - }*/ + Future startStats(String callId) async { + // Delay to allow call to be established + await Future.delayed(debugStatsDelay); + + if (peerConnection == null) { + _logger.d("Peer connection null"); + return false; + } + + _statsManager = StatsManager(_socket, peerConnection!, callId); + _statsManager?.startTimer(); + _logger.d("Peer :: Stats Manager started for $callId"); + + return true; + } _send(event) { _socket.send(event); @@ -437,9 +456,10 @@ class Peer { }); await _localStream?.dispose(); _localStream = null; - await session.peerConnection?.close(); + await session.peerConnection?.dispose(); await session.dc?.close(); + _statsManager?.stopTimer(); } } diff --git a/packages/telnyx_webrtc/lib/stats/stats_params.dart b/packages/telnyx_webrtc/lib/stats/stats_params.dart new file mode 100644 index 0000000..0bd62c3 --- /dev/null +++ b/packages/telnyx_webrtc/lib/stats/stats_params.dart @@ -0,0 +1,56 @@ +import 'package:uuid/uuid.dart'; + +class StatParams { + final String type; + final String debugReportId; + final Map reportData; + final int debugReportVersion; + final String id; + final String jsonrpc; + + StatParams({ + this.type = "debug_report_data", + required this.debugReportId, + required this.reportData, + this.debugReportVersion = 1, + String? id, + this.jsonrpc = "2.0", + }) : id = id ?? const Uuid().v4(); + + Map toJson() { + return { + "type": type, + "debug_report_id": debugReportId, + "debug_report_data": reportData, + "debug_report_version": debugReportVersion, + "id": id, + "jsonrpc": jsonrpc, + }; + } +} + +class InitiateOrStopStatParams { + final String type; + final String debugReportId; + final int debugReportVersion; + final String id; + final String jsonrpc; + + InitiateOrStopStatParams({ + required this.type, + required this.debugReportId, + this.debugReportVersion = 1, + String? id, + this.jsonrpc = "2.0", + }) : id = id ?? Uuid().v4(); + + Map toJson() { + return { + "type": type, + "debug_report_id": debugReportId, + "debug_report_version": debugReportVersion, + "id": id, + "jsonrpc": jsonrpc, + }; + } +} diff --git a/packages/telnyx_webrtc/lib/stats/statsmanager.dart b/packages/telnyx_webrtc/lib/stats/statsmanager.dart new file mode 100644 index 0000000..9840a77 --- /dev/null +++ b/packages/telnyx_webrtc/lib/stats/statsmanager.dart @@ -0,0 +1,128 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:logger/logger.dart'; +import 'package:telnyx_webrtc/stats/stats_params.dart'; +import 'package:telnyx_webrtc/tx_socket.dart'; +import 'package:uuid/uuid.dart'; + +class StatsManager { + StatsManager(this.socket, this.peerConnection, this.callId); + final _logger = Logger(); + + Timer? _timer; + bool debugReportStarted = false; + final Uuid uuid = Uuid(); + final int STATS_INITIAL = 1000; // Replace with appropriate initial delay + final int STATS_INTERVAL = 1000; // Replace with appropriate interval + final int CANDIDATE_LIMIT = 10; // Adjust as needed + + Map mainObject = {}; + Map audio = {}; + Map statsData = {}; + List inBoundStats = []; + List outBoundStats = []; + List candidatePairs = []; + + String debugStatsId = const Uuid().v4(); + + // Placeholder for `client` and `peerConnection`, replace with actual implementation. + final TxSocket socket; + final RTCPeerConnection? peerConnection; + final String callId; + + void stopTimer() { + stopStats(debugStatsId); + mainObject = {}; + peerConnection?.close(); + peerConnection?.dispose(); + _timer?.cancel(); + } + + void startTimer() { + if (!debugReportStarted) { + debugStatsId = uuid.v4(); + _startStats(debugStatsId); + } + + _timer = Timer.periodic(Duration(milliseconds: STATS_INTERVAL), (_) { + mainObject = { + "event": "stats", + "tag": "stats", + "peerId": "stats", + "connectionId": callId, + }; + + peerConnection?.getStats(null).then((stats) { + for (int i = 0; i < stats.length; i++) { + final report = stats[i]; + if (report.type == "inbound-rtp") { + _logger.d("Stats: ${report.type} => ${report.values}"); + inBoundStats.add(report.values); + } else if (report.type == "outbound-rtp") { + _logger.d("Stats: ${report.type} => ${report.values}"); + outBoundStats.add(report.values); + } else if (report.type == "candidate-pair") { + _logger.d("Stats: ${report.type} => ${report.values}"); + candidatePairs.add(report.values); + } + } + + audio = { + "inbound": inBoundStats, + "outbound": outBoundStats, + "candidatePair": candidatePairs, + }; + + statsData = { + "audio": audio, + }; + + mainObject["data"] = statsData; + mainObject["connectionId"] = callId; + mainObject["timestamp"] = DateTime.now().millisecondsSinceEpoch; + + if (inBoundStats.isNotEmpty && + outBoundStats.isNotEmpty && + candidatePairs.isNotEmpty) { + // Reset for next interval + inBoundStats = []; + outBoundStats = []; + candidatePairs = []; + statsData = {}; + audio = {}; + + print("Stats Inbound: ${jsonEncode(mainObject)}"); + + sendStats(mainObject, debugStatsId); + } + }); + }); + } + + void _startStats(String sessionId) { + debugReportStarted = true; + var loginMessage = InitiateOrStopStatParams( + type: "debug_report_start", + debugReportId: sessionId, + ); + socket.send(jsonEncode(loginMessage.toJson())); + } + + void sendStats(Map data, String sessionId) { + var statParams = StatParams( + debugReportId: sessionId, + reportData: data, + ); + socket.send(jsonEncode(statParams.toJson())); + } + + void stopStats(String sessionId) { + debugReportStarted = false; + var loginMessage = InitiateOrStopStatParams( + type: "debug_report_stop", + debugReportId: sessionId, + ); + socket.send(jsonEncode(loginMessage.toJson())); + } +}