|
@@ -1,4 +1,158 @@
|
|
|
-var app = angular.module('troubleshoot', ['ngSanitize']);
|
|
|
+/**
|
|
|
+ * Create peer connection and bind events to be logged.
|
|
|
+ * @param role The role (offerer or answerer).
|
|
|
+ * @returns {RTCPeerConnection}
|
|
|
+ */
|
|
|
+function createPeerConnection(role) {
|
|
|
+ // Detect safari
|
|
|
+ const uagent = window.navigator.userAgent.toLowerCase();
|
|
|
+ const isSafari = /safari/.test(uagent) && /applewebkit/.test(uagent) && !/chrome/.test(uagent);
|
|
|
+
|
|
|
+ // Determine ICE servers
|
|
|
+ let iceServers;
|
|
|
+ if (isSafari) {
|
|
|
+ iceServers = [
|
|
|
+ 'turn:turn.threema.ch:443?transport=udp',
|
|
|
+ 'turn:turn.threema.ch:443?transport=tcp',
|
|
|
+ 'turns:turn.threema.ch:443',
|
|
|
+ ];
|
|
|
+ } else {
|
|
|
+ iceServers = [
|
|
|
+ 'turn:ds-turn.threema.ch:443?transport=udp',
|
|
|
+ 'turn:ds-turn.threema.ch:443?transport=tcp',
|
|
|
+ 'turns:ds-turn.threema.ch:443',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ console.debug('Using ICE servers: ' + iceServers);
|
|
|
+ const configuration = {iceServers: [{
|
|
|
+ urls: iceServers,
|
|
|
+ username: 'threema-angular-test',
|
|
|
+ credential: 'VaoVnhxKGt2wD20F9bTOgiew6yHQmj4P7y7SE4lrahAjTQC0dpnG32FR4fnrlpKa',
|
|
|
+ }]};
|
|
|
+
|
|
|
+ // Create peer connection
|
|
|
+ const pc = new RTCPeerConnection(configuration);
|
|
|
+ pc.addEventListener('negotiationneeded', async () => {
|
|
|
+ console.info(role, 'Negotiation needed');
|
|
|
+ });
|
|
|
+ pc.addEventListener('signalingstatechange', () => {
|
|
|
+ console.debug(role, 'Signaling state:', pc.signalingState);
|
|
|
+ });
|
|
|
+ pc.addEventListener('iceconnectionstatechange', () => {
|
|
|
+ console.debug(role, 'ICE connection state:', pc.iceConnectionState);
|
|
|
+ });
|
|
|
+ pc.addEventListener('icegatheringstatechange', () => {
|
|
|
+ console.debug(role, 'ICE gathering state:', pc.iceGatheringState);
|
|
|
+ });
|
|
|
+ pc.addEventListener('connectionstatechange', () => {
|
|
|
+ console.debug(role, 'Connection state:', pc.connectionState);
|
|
|
+ });
|
|
|
+ pc.addEventListener('icecandidate', (event) => {
|
|
|
+ console.debug(role, 'ICE candidate:', event.candidate);
|
|
|
+ });
|
|
|
+ pc.addEventListener('icecandidateerror', (event) => {
|
|
|
+ console.error(role, 'ICE candidate error:', event);
|
|
|
+ });
|
|
|
+ pc.addEventListener('datachannel', (event) => {
|
|
|
+ console.info(role, 'Incoming data channel:', event.channel.label);
|
|
|
+ });
|
|
|
+ return pc;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Create a data channel and bind events to be logged.
|
|
|
+ * @param pc The peer connection instance.
|
|
|
+ * @param role The role (offerer or answerer).
|
|
|
+ * @param label The label of the data channel.
|
|
|
+ * @param options The options passed to the RTCDataChannel instance.
|
|
|
+ * @returns {RTCDataChannel}
|
|
|
+ */
|
|
|
+function createDataChannel(pc, role, label, options) {
|
|
|
+ // Create data channel and bind events
|
|
|
+ const dc = pc.createDataChannel(label, options);
|
|
|
+ dc.addEventListener('open', () => {
|
|
|
+ console.info(role, label, 'open');
|
|
|
+ });
|
|
|
+ dc.addEventListener('close', () => {
|
|
|
+ console.info(role, label, 'closed');
|
|
|
+ });
|
|
|
+ dc.addEventListener('error', () => {
|
|
|
+ console.error(role, label, 'error:', error);
|
|
|
+ });
|
|
|
+ dc.addEventListener('bufferedamountlow', () => {
|
|
|
+ console.debug(role, label, 'buffered amount low:', dc.bufferedAmount);
|
|
|
+ });
|
|
|
+ dc.addEventListener('message', (event) => {
|
|
|
+ console.debug(role, label, `incoming message:`, event.data);
|
|
|
+ });
|
|
|
+ return dc;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Connect the peer connection instances to each other.
|
|
|
+ * @param offerer The offerer's peer connection instance.
|
|
|
+ * @param answerer The answerer's peer connection instance.
|
|
|
+ * @returns {Promise<void>} resolves once connected.
|
|
|
+ */
|
|
|
+async function connectPeerConnections(offerer, answerer) {
|
|
|
+ const pcs = [offerer, answerer];
|
|
|
+
|
|
|
+ // Forward ICE candidates to each other
|
|
|
+ for (const [me, other] of [pcs, pcs.slice().reverse()]) {
|
|
|
+ me.addEventListener('icecandidate', (event) => {
|
|
|
+ if (event.candidate !== null) {
|
|
|
+ other.addIceCandidate(event.candidate);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Promise for the peer connections being connected
|
|
|
+ const [offererConnected, answererConnected] = pcs.map((pc) => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ pc.addEventListener('iceconnectionstatechange', () => {
|
|
|
+ switch (pc.iceConnectionState) {
|
|
|
+ case 'connected':
|
|
|
+ case 'completed':
|
|
|
+ resolve(pc.iceConnectionState);
|
|
|
+ break;
|
|
|
+ case 'closed':
|
|
|
+ case 'failed':
|
|
|
+ reject(pc.iceConnectionState);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // Start the offer/answer dance
|
|
|
+ const signalingDone = new Promise((resolve, reject) => {
|
|
|
+ offerer.addEventListener('negotiationneeded', async () => {
|
|
|
+ try {
|
|
|
+ console.debug('Start signaling');
|
|
|
+ const offer = await offerer.createOffer();
|
|
|
+ await offerer.setLocalDescription(offer);
|
|
|
+ await answerer.setRemoteDescription(offer);
|
|
|
+ const answer = await answerer.createAnswer();
|
|
|
+ await answerer.setLocalDescription(answer);
|
|
|
+ await offerer.setRemoteDescription(answer);
|
|
|
+ console.debug('Signaling complete');
|
|
|
+ resolve();
|
|
|
+ } catch (error) {
|
|
|
+ reject(error);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // Wait until all is done
|
|
|
+ await Promise.all([
|
|
|
+ offererConnected,
|
|
|
+ answererConnected,
|
|
|
+ signalingDone,
|
|
|
+ ]);
|
|
|
+}
|
|
|
+
|
|
|
+// Here beginneth the Angular stuff
|
|
|
+const app = angular.module('troubleshoot', ['ngSanitize']);
|
|
|
|
|
|
app.filter('osName', function() {
|
|
|
return function(id) {
|
|
@@ -41,6 +195,9 @@ app.component('check', {
|
|
|
`,
|
|
|
});
|
|
|
|
|
|
+const SIGNALING_DATA_CHANNEL_LABEL = 'saltyrtc';
|
|
|
+const APP_DATA_CHANNEL_LABEL = 'therme';
|
|
|
+
|
|
|
app.controller('ChecksController', function($scope, $timeout) {
|
|
|
// Initialize state
|
|
|
this.state = 'init'; // Either 'init' or 'check'
|
|
@@ -65,13 +222,10 @@ app.controller('ChecksController', function($scope, $timeout) {
|
|
|
showLogs: false,
|
|
|
logs: [],
|
|
|
};
|
|
|
- this.resultPc = {
|
|
|
- state: 'unknown',
|
|
|
- showLogs: false,
|
|
|
- };
|
|
|
this.resultDc = {
|
|
|
state: 'unknown',
|
|
|
showLogs: false,
|
|
|
+ logs: [],
|
|
|
};
|
|
|
this.resultTurn = {
|
|
|
state: 'unknown',
|
|
@@ -86,75 +240,31 @@ app.controller('ChecksController', function($scope, $timeout) {
|
|
|
this.doChecks();
|
|
|
};
|
|
|
|
|
|
- // Helper: Local storage
|
|
|
- function localStorageAvailable() {
|
|
|
- var test = 'test';
|
|
|
+ // Local store can be used
|
|
|
+ const localStorageAvailable = () => {
|
|
|
+ const test = 'test';
|
|
|
try {
|
|
|
localStorage.setItem(test, test);
|
|
|
localStorage.removeItem(test);
|
|
|
- return true;
|
|
|
- } catch(e) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Helper: Desktop notifications
|
|
|
- function desktopNotificationsAvailable() {
|
|
|
- return 'Notification' in window;
|
|
|
- }
|
|
|
-
|
|
|
- // Helper: Peer connection
|
|
|
- function peerConnectionAvailable() {
|
|
|
- return window.RTCPeerConnection;
|
|
|
- }
|
|
|
-
|
|
|
- // Helper: Data channel
|
|
|
- function dataChannelAvailable() {
|
|
|
- return window.RTCPeerConnection && (new RTCPeerConnection()).createDataChannel;
|
|
|
- }
|
|
|
-
|
|
|
- // Run all the checks and update results
|
|
|
- this.doChecks = () => {
|
|
|
- // Check for JS
|
|
|
- this.resultJs.state = 'yes';
|
|
|
-
|
|
|
- // Check for LocalStorage
|
|
|
- if (localStorageAvailable()) {
|
|
|
this.resultLs.state = 'yes';
|
|
|
- } else {
|
|
|
+ } catch(e) {
|
|
|
this.resultLs.state = 'no';
|
|
|
}
|
|
|
+ };
|
|
|
|
|
|
- // Check for desktop notifications
|
|
|
- if (desktopNotificationsAvailable()) {
|
|
|
- this.resultDn.state = 'yes';
|
|
|
- } else {
|
|
|
- this.resultDn.state = 'no';
|
|
|
- }
|
|
|
-
|
|
|
- // Check for RTCPeerConnection
|
|
|
- if (peerConnectionAvailable()) {
|
|
|
- this.resultPc.state = 'yes';
|
|
|
- } else {
|
|
|
- this.resultPc.state = 'no';
|
|
|
- }
|
|
|
-
|
|
|
- // Check for RTCDataChannel
|
|
|
- if (dataChannelAvailable()) {
|
|
|
- this.resultDc.state = 'yes';
|
|
|
- this.resultTurn.state = 'loading';
|
|
|
- } else {
|
|
|
- this.resultDc.state = 'no';
|
|
|
- this.resultTurn.state = 'no';
|
|
|
- }
|
|
|
+ // The desktop notification API is available
|
|
|
+ const desktopNotificationsAvailable = () => {
|
|
|
+ this.resultDn.state = 'Notification' in window ? 'yes' : 'no';
|
|
|
+ };
|
|
|
|
|
|
- // Check for WebSocket connectivity
|
|
|
+ // A WebSocket connection can be established to the SaltyRTC server
|
|
|
+ const canEstablishWebSocket = () => {
|
|
|
const subprotocol = 'v1.saltyrtc.org';
|
|
|
const path = 'ffffffffffffffff00000000000000000000000000000000ffffffffffffffff';
|
|
|
this.resultWs.showLogs = true;
|
|
|
const ws = new WebSocket('wss://saltyrtc-ff.threema.ch/' + path, subprotocol);
|
|
|
ws.binaryType = 'arraybuffer';
|
|
|
- ws.addEventListener('open', (event) => {
|
|
|
+ ws.addEventListener('open', () => {
|
|
|
$scope.$apply(() => {
|
|
|
this.resultWs.logs.push('Connected');
|
|
|
});
|
|
@@ -183,9 +293,7 @@ app.controller('ChecksController', function($scope, $timeout) {
|
|
|
console.log('Message bytes:', bytes);
|
|
|
|
|
|
// Validate length
|
|
|
- let valid;
|
|
|
if (bytes.length < 81) {
|
|
|
- valid = false;
|
|
|
return fail(`Invalid length: ${bytes.length}`);
|
|
|
}
|
|
|
|
|
@@ -225,84 +333,260 @@ app.controller('ChecksController', function($scope, $timeout) {
|
|
|
this.resultWs.logs.push('Error');
|
|
|
});
|
|
|
});
|
|
|
- ws.addEventListener('close', (event) => {
|
|
|
+ ws.addEventListener('close', () => {
|
|
|
$scope.$apply(() => {
|
|
|
this.resultWs.logs.push('Connection closed');
|
|
|
});
|
|
|
});
|
|
|
this.resultWs.logs.push('Connecting');
|
|
|
+ };
|
|
|
|
|
|
- // Check for TURN connectivity
|
|
|
- let timeout = null;
|
|
|
- const testTurn = () => {
|
|
|
- timeout = $timeout(() => this.turnSuccess = 'no', 10000);
|
|
|
- const noop = () => {};
|
|
|
-
|
|
|
- // Detect safari
|
|
|
- const uagent = window.navigator.userAgent.toLowerCase();
|
|
|
- const isSafari = /safari/.test(uagent) && /applewebkit/.test(uagent) && !/chrome/.test(uagent);
|
|
|
-
|
|
|
- // Determine ICE servers
|
|
|
- let iceServers;
|
|
|
- if (isSafari) {
|
|
|
- iceServers = [
|
|
|
- 'turn:turn.threema.ch:443?transport=udp',
|
|
|
- 'turn:turn.threema.ch:443?transport=tcp',
|
|
|
- 'turns:turn.threema.ch:443',
|
|
|
- ];
|
|
|
- } else {
|
|
|
- iceServers = [
|
|
|
- 'turn:ds-turn.threema.ch:443?transport=udp',
|
|
|
- 'turn:ds-turn.threema.ch:443?transport=tcp',
|
|
|
- 'turns:ds-turn.threema.ch:443',
|
|
|
- ];
|
|
|
- }
|
|
|
- console.debug('Using ICE servers: ' + iceServers);
|
|
|
+ // A peer-to-peer connection can be established and a data channel can be
|
|
|
+ // used to send data.
|
|
|
+ const canEstablishDataChannels = () => {
|
|
|
+ this.resultDc.showLogs = true;
|
|
|
|
|
|
- const pc = new RTCPeerConnection({iceServers: [{
|
|
|
- urls: iceServers,
|
|
|
- username: 'threema-angular-test',
|
|
|
- credential: 'VaoVnhxKGt2wD20F9bTOgiew6yHQmj4P7y7SE4lrahAjTQC0dpnG32FR4fnrlpKa',
|
|
|
- }]});
|
|
|
+ // Check for the RTCPeerConnecton object
|
|
|
+ if (window.RTCPeerConnection) {
|
|
|
+ this.resultDc.logs.push('RTCPeerConnection available');
|
|
|
+ } else {
|
|
|
+ this.resultDc.state = 'no';
|
|
|
+ this.resultDc.logs.push('RTCPeerConnection unavailable');
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- this.resultTurn.showLogs = true;
|
|
|
+ // Check for the RTCDataChannel object
|
|
|
+ if (window.RTCPeerConnection && (new RTCPeerConnection()).createDataChannel) {
|
|
|
+ this.resultDc.logs.push('RTCDataChannel available');
|
|
|
+ } else {
|
|
|
+ this.resultDc.state = 'no';
|
|
|
+ this.resultDc.logs.push('RTCDataChannel unavailable');
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- pc.createDataChannel('test');
|
|
|
+ // Create two peer connection instances
|
|
|
+ let offerer, answerer;
|
|
|
+ try {
|
|
|
+ [offerer, answerer] = [
|
|
|
+ createPeerConnection('Offerer'),
|
|
|
+ createPeerConnection('Answerer'),
|
|
|
+ ];
|
|
|
+ } catch (error) {
|
|
|
+ this.resultDc.state = 'no';
|
|
|
+ this.resultDc.logs.push(`Peer connection could not be created (${error.toString()})`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Async phase begins
|
|
|
+ this.resultDc.state = 'loading';
|
|
|
+ const done = (success, message) => {
|
|
|
+ if (this.resultDc.state === 'loading') {
|
|
|
+ this.resultDc.state = success ? 'yes' : 'no';
|
|
|
+ }
|
|
|
+ this.resultDc.logs.push(message);
|
|
|
+ offerer.close();
|
|
|
+ answerer.close();
|
|
|
+ };
|
|
|
|
|
|
- this.resultTurn.logs.push('Creating offer...');
|
|
|
- pc.createOffer(function(sdp) { pc.setLocalDescription(sdp, noop, noop) }, noop);
|
|
|
+ // Connect the peer connection instances to each other
|
|
|
+ let peerConnectionsEstablished;
|
|
|
+ try {
|
|
|
+ peerConnectionsEstablished = connectPeerConnections(offerer, answerer);
|
|
|
+ } catch (error) {
|
|
|
+ return done(false, `Peer connections could not be connected (${error.toString()})`);
|
|
|
+ }
|
|
|
+ peerConnectionsEstablished
|
|
|
+ .then(() => {
|
|
|
+ $scope.$apply(() => this.resultDc.logs.push('Connected'));
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ $scope.$apply(() => done(false, `Cannot connect (error: ${error.toString()})`));
|
|
|
+ });
|
|
|
|
|
|
- pc.onicecandidate = (ice) => {
|
|
|
+ // Create data channels for each peer connection instance. We mimic
|
|
|
+ // what SaltyRTC and the web client would do here:
|
|
|
+ //
|
|
|
+ // - create a negotiated data channel with id 0 and send once open, and
|
|
|
+ // - create a data channel for the ARP on the offerer's side.
|
|
|
+ const canUseDataChannel = (role, dc, resolve, reject) => {
|
|
|
+ dc.addEventListener('open', () => {
|
|
|
$scope.$apply(() => {
|
|
|
- if (ice.candidate === null) {
|
|
|
- this.resultTurn.logs.push('Done collecting candidates.');
|
|
|
- if (this.resultTurn.state === 'loading') {
|
|
|
- this.resultTurn.state = 'no';
|
|
|
- $timeout.cancel(timeout);
|
|
|
- }
|
|
|
- } else if (ice.candidate.candidate) {
|
|
|
- const candidate = SDPUtils.parseCandidate(ice.candidate.candidate);
|
|
|
- console.debug(candidate);
|
|
|
-
|
|
|
- let info = `[${candidate.type}] ${candidate.ip}:${candidate.port}`;
|
|
|
- if (candidate.relatedAddress) {
|
|
|
- info += ` via ${candidate.relatedAddress}`;
|
|
|
- }
|
|
|
- info += ` (${candidate.protocol})`;
|
|
|
- this.resultTurn.logs.push(info);
|
|
|
-
|
|
|
- if (candidate.type === 'relay') {
|
|
|
- this.resultTurn.state = 'yes';
|
|
|
- $timeout.cancel(timeout);
|
|
|
- }
|
|
|
- } else {
|
|
|
- console.warn('Invalid candidate:', ice.candidate.candidate);
|
|
|
- this.resultTurn.logs.push('Invalid candidate (see debug log)');
|
|
|
- }
|
|
|
+ this.resultDc.logs.push(`${role}: Channel '${dc.label}' open`);
|
|
|
+ try {
|
|
|
+ dc.send('hello!');
|
|
|
+ } catch (error) {
|
|
|
+ this.resultDc.logs.push(
|
|
|
+ `${role}: Channel '${dc.label}' was unable to send (${error.toString()})`);
|
|
|
+ reject();
|
|
|
+ }
|
|
|
});
|
|
|
+ });
|
|
|
+ dc.addEventListener('close', () => {
|
|
|
+ $scope.$apply(() => {
|
|
|
+ if (this.resultDc.state === 'loading') {
|
|
|
+ this.resultDc.logs.push(`${role}: Channel '${dc.label}' closed`);
|
|
|
+ reject();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ dc.addEventListener('error', () => {
|
|
|
+ $scope.$apply(() => {
|
|
|
+ this.resultDc.logs.push(`${role}: Channel '${dc.label}' error (${error.message})`);
|
|
|
+ reject();
|
|
|
+ });
|
|
|
+ });
|
|
|
+ dc.addEventListener('message', (event) => {
|
|
|
+ $scope.$apply(() => {
|
|
|
+ if (event.data === 'hello!') {
|
|
|
+ this.resultDc.logs.push(`${role}: Channel '${dc.label}' working`);
|
|
|
+ resolve();
|
|
|
+ } else {
|
|
|
+ this.resultDc.logs.push(
|
|
|
+ `${role}: Channel '${dc.label}' received an unexpected message ('${event.data}')`);
|
|
|
+ reject();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ };
|
|
|
+ try {
|
|
|
+ Promise.all([
|
|
|
+ new Promise((resolve, reject) => {
|
|
|
+ const dc = createDataChannel(
|
|
|
+ offerer, 'Offerer', SIGNALING_DATA_CHANNEL_LABEL, {id: 0, negotiated: true});
|
|
|
+ canUseDataChannel('Offerer', dc, resolve, reject);
|
|
|
+ }),
|
|
|
+ new Promise((resolve, reject) => {
|
|
|
+ const dc = createDataChannel(
|
|
|
+ answerer, 'Answerer', SIGNALING_DATA_CHANNEL_LABEL, {id: 0, negotiated: true});
|
|
|
+ canUseDataChannel('Answerer', dc, resolve, reject);
|
|
|
+ }),
|
|
|
+ // Mimic handover by waiting until the peer connection has
|
|
|
+ // been established (and an additional second).
|
|
|
+ peerConnectionsEstablished
|
|
|
+ .then(() => new Promise((resolve) => setTimeout(resolve, 1000)))
|
|
|
+ .then(() => new Promise((resolve, reject) => {
|
|
|
+ const dc = createDataChannel(offerer, 'Offerer', APP_DATA_CHANNEL_LABEL);
|
|
|
+ canUseDataChannel('Offerer', dc, resolve, reject);
|
|
|
+ })),
|
|
|
+ new Promise((resolve, reject) => {
|
|
|
+ answerer.addEventListener('datachannel', (event) => {
|
|
|
+ $scope.$apply(() => {
|
|
|
+ const dc = event.channel;
|
|
|
+ if (dc.label !== APP_DATA_CHANNEL_LABEL) {
|
|
|
+ return done(false, `Unexpected 'datachannel' event (channel: ${dc.label})`);
|
|
|
+ } else {
|
|
|
+ canUseDataChannel('Answerer', dc, resolve, reject);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }),
|
|
|
+ ])
|
|
|
+ .then(() => {
|
|
|
+ $scope.$apply(() => done(true, 'Data channels open and working'));
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ $scope.$apply(() => done(false, `Cannot connect (error: ${error.toString()})`));
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ return done(false, `Data channels could not be created (${error.toString()})`);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const haveTurnCandidates = () => {
|
|
|
+ this.resultTurn.showLogs = true;
|
|
|
+
|
|
|
+ // Create a peer connection instance
|
|
|
+ let pc;
|
|
|
+ try {
|
|
|
+ pc = createPeerConnection('TURN');
|
|
|
+ } catch (error) {
|
|
|
+ this.resultTurn.state = 'no';
|
|
|
+ this.resultTurn.logs.push(`Peer connection could not be created (${error.toString()})`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Async phase begins
|
|
|
+ this.resultTurn.state = 'loading';
|
|
|
+ const done = (success, message) => {
|
|
|
+ if (this.resultTurn.state === 'loading') {
|
|
|
+ this.resultTurn.state = success ? 'yes' : 'no';
|
|
|
}
|
|
|
+ this.resultTurn.logs.push(message);
|
|
|
+ pc.close();
|
|
|
+ };
|
|
|
+
|
|
|
+ // Just trigger negotiation...
|
|
|
+ try {
|
|
|
+ pc.createDataChannel('kick-the-peer-connection-to-life');
|
|
|
+ } catch (error) {
|
|
|
+ return done(false, `Data channel could not be created (${error.toString()})`);
|
|
|
}
|
|
|
- testTurn();
|
|
|
+
|
|
|
+ // Create timeout
|
|
|
+ const timer = $timeout(() => this.resultTurn.state = 'no', 10000);
|
|
|
+
|
|
|
+ // Create and apply local offer (async)
|
|
|
+ (async () => {
|
|
|
+ try {
|
|
|
+ const offer = await pc.createOffer();
|
|
|
+ await pc.setLocalDescription(offer);
|
|
|
+ } catch (error) {
|
|
|
+ $scope.$apply(() => done(false, `Offer could not be created (${error.toString()})`));
|
|
|
+ }
|
|
|
+ })();
|
|
|
+
|
|
|
+ // Check for TURN ICE candidates
|
|
|
+ pc.addEventListener('icecandidate', (event) => {
|
|
|
+ $scope.$apply(() => {
|
|
|
+ // Check for end-of-candidates indicator
|
|
|
+ if (event.candidate === null) {
|
|
|
+ $timeout.cancel(timer);
|
|
|
+ return done(false, 'Done');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle ICE candidate
|
|
|
+ if (event.candidate.candidate) {
|
|
|
+ const candidate = SDPUtils.parseCandidate(event.candidate.candidate);
|
|
|
+ let info = `[${candidate.type}] ${candidate.ip}:${candidate.port}`;
|
|
|
+ if (candidate.relatedAddress) {
|
|
|
+ info += ` via ${candidate.relatedAddress}`;
|
|
|
+ }
|
|
|
+ info += ` (${candidate.protocol})`;
|
|
|
+
|
|
|
+ // Relay candidate found: Cancel timer
|
|
|
+ if (candidate.type === 'relay') {
|
|
|
+ $timeout.cancel(timer);
|
|
|
+ return done(true, info);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Normal candidate: Log and continue
|
|
|
+ this.resultTurn.logs.push(info);
|
|
|
+ } else {
|
|
|
+ this.resultTurn.logs.push(`Invalid candidate (${event.candidate.candidate})`);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // Run all the checks and update results
|
|
|
+ this.doChecks = () => {
|
|
|
+ // Check for JS
|
|
|
+ this.resultJs.state = 'yes';
|
|
|
+
|
|
|
+ // Check for LocalStorage
|
|
|
+ localStorageAvailable();
|
|
|
+
|
|
|
+ // Check for desktop notifications
|
|
|
+ desktopNotificationsAvailable();
|
|
|
+
|
|
|
+ // Check for data channel connectivity
|
|
|
+ canEstablishDataChannels();
|
|
|
+
|
|
|
+ // Check for WebSocket connectivity
|
|
|
+ canEstablishWebSocket();
|
|
|
+
|
|
|
+ // Check for TURN connectivity
|
|
|
+ haveTurnCandidates();
|
|
|
};
|
|
|
|
|
|
});
|