diff --git a/lib/matrix.dart b/lib/matrix.dart index 26452956..aa77870b 100644 --- a/lib/matrix.dart +++ b/lib/matrix.dart @@ -32,6 +32,7 @@ export 'src/voip/call.dart'; export 'src/voip/group_call.dart'; export 'src/voip/voip.dart'; export 'src/voip/voip_content.dart'; +export 'src/voip/conn_tester.dart'; export 'src/voip/utils.dart'; export 'src/room.dart'; export 'src/timeline.dart'; diff --git a/lib/src/voip/conn_tester.dart b/lib/src/voip/conn_tester.dart new file mode 100644 index 00000000..62b1b23b --- /dev/null +++ b/lib/src/voip/conn_tester.dart @@ -0,0 +1,113 @@ +import 'dart:async'; + +import 'package:webrtc_interface/webrtc_interface.dart'; + +import 'package:matrix/matrix.dart'; + +class ConnectionTester { + Client client; + WebRTCDelegate delegate; + RTCPeerConnection? pc1, pc2; + ConnectionTester(this.client, this.delegate); + TurnServerCredentials? _turnServerCredentials; + + Future verifyTurnServer() async { + final iceServers = await getIceSevers(); + final configuration = { + 'iceServers': iceServers, + 'sdpSemantics': 'unified-plan', + 'iceCandidatePoolSize': 1, + 'iceTransportPolicy': 'relay' + }; + pc1 = await delegate.createPeerConnection(configuration); + pc2 = await delegate.createPeerConnection(configuration); + + pc1!.onIceCandidate = (candidate) { + if (candidate.candidate!.contains('relay')) { + pc2!.addCandidate(candidate); + } + }; + pc2!.onIceCandidate = (candidate) { + if (candidate.candidate!.contains('relay')) { + pc1!.addCandidate(candidate); + } + }; + + await pc1!.createDataChannel('conn-tester', RTCDataChannelInit()); + + final offer = await pc1!.createOffer(); + + await pc2!.setRemoteDescription(offer); + final answer = await pc2!.createAnswer(); + + await pc1!.setLocalDescription(offer); + await pc2!.setLocalDescription(answer); + + await pc1!.setRemoteDescription(answer); + + void dispose() { + pc1!.close(); + pc1!.dispose(); + pc2!.close(); + pc2!.dispose(); + } + + bool connected = false; + try { + await waitUntilAsync(() async { + if (pc1!.connectionState == + RTCPeerConnectionState.RTCPeerConnectionStateConnected && + pc2!.connectionState == + RTCPeerConnectionState.RTCPeerConnectionStateConnected) { + connected = true; + return true; + } + return false; + }); + } catch (e) { + Logs().e('[VOIP] ConnectionTester Error while testing TURN server: $e'); + } + + dispose(); + return connected; + } + + Future waitUntilAsync(Future Function() test, + {final int maxIterations = 1000, + final Duration step = const Duration(milliseconds: 10)}) async { + int iterations = 0; + for (; iterations < maxIterations; iterations++) { + await Future.delayed(step); + if (await test()) { + break; + } + } + if (iterations >= maxIterations) { + throw TimeoutException( + 'Condition not reached within ${iterations * step.inMilliseconds}ms'); + } + return iterations; + } + + Future>> getIceSevers() async { + if (_turnServerCredentials == null) { + try { + _turnServerCredentials = await client.getTurnServer(); + } catch (e) { + Logs().v('[VOIP] getTurnServerCredentials error => ${e.toString()}'); + } + } + + if (_turnServerCredentials == null) { + return []; + } + + return [ + { + 'username': _turnServerCredentials!.username, + 'credential': _turnServerCredentials!.password, + 'url': _turnServerCredentials!.uris[0] + } + ]; + } +}