/* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const addButton = document.querySelector('button#add'); const candidateTBody = document.querySelector('tbody#candidatesBody'); const gatherButton = document.querySelector('button#gather'); const passwordInput = document.querySelector('input#password'); const removeButton = document.querySelector('button#remove'); const resetButton = document.querySelector('button#reset'); const servers = document.querySelector('select#servers'); const urlInput = document.querySelector('input#url'); const usernameInput = document.querySelector('input#username'); const iceCandidatePoolInput = document.querySelector('input#iceCandidatePool'); addButton.onclick = addServer; gatherButton.onclick = start; removeButton.onclick = removeServer; resetButton.onclick = (e) => { window.localStorage.clear(); document.querySelectorAll('select#servers option').forEach(option => option.remove()); const serversSelect = document.querySelector('select#servers'); setDefaultServer(serversSelect); }; iceCandidatePoolInput.onchange = function(e) { const span = e.target.parentElement.querySelector('span'); span.textContent = e.target.value; }; let begin; let pc; let candidates; const allServersKey = 'servers'; function setDefaultServer(serversSelect) { //o.value = '{"urls":["stun:stun.l.google.com:19302"]}'; // o.text = 'stun:stun.l.google.com:19302'; const o = document.createElement('option'); o.value = '{"urls":["stun:sipf.denaroo.com:3478"]}'; o.text = 'stun:sipf.denaroo.com:3478'; serversSelect.add(o); } // function writeServersToLocalStorage() { const serversSelect = document.querySelector('select#servers'); const allServers = JSON.stringify(Object.values(serversSelect.options).map(o => JSON.parse(o.value))); window.localStorage.setItem(allServersKey, allServers); } function readServersFromLocalStorage() { document.querySelectorAll('select#servers option').forEach(option => option.remove()); const serversSelect = document.querySelector('select#servers'); const storedServers = window.localStorage.getItem(allServersKey); if (storedServers === null || storedServers === '') { setDefaultServer(serversSelect); } else { JSON.parse(storedServers).forEach((server, key) => { const o = document.createElement('option'); o.value = JSON.stringify(server); o.text = server.urls[0]; o.ondblclick = selectServer; serversSelect.add(o); }); } } function selectServer(event) { const option = event.target; const value = JSON.parse(option.value); urlInput.value = value.urls[0]; usernameInput.value = value.username || ''; passwordInput.value = value.credential || ''; } function addServer() { const scheme = urlInput.value.split(':')[0]; if (scheme !== 'stun' && scheme !== 'turn' && scheme !== 'turns') { alert(`URI scheme ${scheme} is not valid`); return; } // Store the ICE server as a stringified JSON object in option.value. const option = document.createElement('option'); const iceServer = { urls: [urlInput.value], username: usernameInput.value, credential: passwordInput.value }; option.value = JSON.stringify(iceServer); option.text = `${urlInput.value} `; const username = usernameInput.value; const password = passwordInput.value; if (username || password) { option.text += (` [${username}:${password}]`); } option.ondblclick = selectServer; servers.add(option); urlInput.value = usernameInput.value = passwordInput.value = ''; writeServersToLocalStorage(); } function removeServer() { for (let i = servers.options.length - 1; i >= 0; --i) { if (servers.options[i].selected) { servers.remove(i); } } writeServersToLocalStorage(); } function start() { // Clean out the table. document.getElementById('success').style.display = 'none'; document.getElementById('error').style.display = 'none'; while (candidateTBody.firstChild) { candidateTBody.removeChild(candidateTBody.firstChild); } gatherButton.disabled = true; // Read the values from the input boxes. const iceServers = []; for (let i = 0; i < servers.length; ++i) { iceServers.push(JSON.parse(servers[i].value)); } const transports = document.getElementsByName('transports'); let iceTransports; for (let i = 0; i < transports.length; ++i) { if (transports[i].checked) { iceTransports = transports[i].value; break; } } // Create a PeerConnection with no streams, but force a m=audio line. const config = { iceServers: iceServers, iceTransportPolicy: iceTransports, iceCandidatePoolSize: iceCandidatePoolInput.value }; const offerOptions = {offerToReceiveAudio: 1}; // Whether we gather IPv6 candidates. // Whether we only gather a single set of candidates for RTP and RTCP. console.log(`Creating new PeerConnection with config=${JSON.stringify(config)}`); document.getElementById('error').innerText = ''; pc = new RTCPeerConnection(config); pc.onicecandidate = iceCallback; pc.onicegatheringstatechange = gatheringStateChange; pc.onicecandidateerror = iceCandidateError; pc.createOffer( offerOptions ).then( gotDescription, noDescription ); } function gotDescription(desc) { begin = window.performance.now(); candidates = []; pc.setLocalDescription(desc); } function noDescription(error) { console.log('Error creating offer: ', error); } // Parse the uint32 PRIORITY field into its constituent parts from RFC 5245, // type preference, local preference, and (256 - component ID). // ex: 126 | 32252 | 255 (126 is host preference, 255 is component ID 1) function formatPriority(priority) { return [ priority >> 24, (priority >> 8) & 0xFFFF, priority & 0xFF ].join(' | '); } function appendCell(row, val, span) { const cell = document.createElement('td'); cell.textContent = val; if (span) { cell.setAttribute('colspan', span); } row.appendChild(cell); } // Try to determine authentication failures and unreachable TURN // servers by using heuristics on the candidate types gathered. function getFinalResult() { let result = 'Done'; // if more than one server is used, it can not be determined // which server failed. if (servers.length === 1) { const server = JSON.parse(servers[0].value); // get the candidates types (host, srflx, relay) const types = candidates.map(function(cand) { return cand.type; }); // If the server is a TURN server we should have a relay candidate. // If we did not get a relay candidate but a srflx candidate // authentication might have failed. // If we did not get a relay candidate or a srflx candidate // we could not reach the TURN server. Either it is not running at // the target address or the clients access to the port is blocked. // // This only works for TURN/UDP since we do not get // srflx candidates from TURN/TCP. if (server.urls[0].indexOf('turn:') === 0 && server.urls[0].indexOf('?transport=tcp') === -1) { if (types.indexOf('relay') === -1) { if (types.indexOf('srflx') > -1) { // a binding response but no relay candidate suggests auth failure. result = 'Authentication failed?'; } else { // either the TURN server is down or the clients access is blocked. result = 'Not reachable?'; } } } } return result; } function iceCallback(event) { const elapsed = ((window.performance.now() - begin) / 1000).toFixed(3); const row = document.createElement('tr'); var successCandidate = false; appendCell(row, elapsed); if (event.candidate) { if (event.candidate.candidate === '') { return; } const {candidate} = event; appendCell(row, candidate.component); appendCell(row, candidate.type); appendCell(row, candidate.foundation); appendCell(row, candidate.protocol); appendCell(row, candidate.address); appendCell(row, candidate.port); appendCell(row, formatPriority(candidate.priority)); if ( candidate.type === 'srflx' && candidate.component === 'rtp'){ successCandidate = candidate; } candidates.push(candidate); } else if (!('onicegatheringstatechange' in RTCPeerConnection.prototype)) { // should not be done if its done in the icegatheringstatechange callback. appendCell(row, getFinalResult(), 7); pc.close(); pc = null; gatherButton.disabled = false; } candidateTBody.appendChild(row); if ( successCandidate ){ document.getElementById('error').style.display = 'none'; document.getElementById('error').innerHTML = ''; document.getElementById('success').style.display = 'block'; document.getElementById('success').innerHTML = 'Tests OK.
The candidate ' + successCandidate.protocol + '/' + successCandidate.type + ' with address ' + successCandidate.address + ' port ' + successCandidate.port +' is a match.'; } } function gatheringStateChange() { if (pc.iceGatheringState !== 'complete') { return; } if ( document.getElementById('success').style.display == 'none' ) { document.getElementById('success').style.display = 'none'; document.getElementById('success').innerHTML = ''; document.getElementById('error').style.display = 'block'; document.getElementById('error').innerHTML = 'Tests FAILED.
No matching candidate rtp/srflx found in results'; } const elapsed = ((window.performance.now() - begin) / 1000).toFixed(3); const row = document.createElement('tr'); appendCell(row, elapsed); appendCell(row, getFinalResult(), 7); pc.close(); pc = null; gatherButton.disabled = false; candidateTBody.appendChild(row); } function iceCandidateError(e) { // The interesting attributes of the error are // * the url (which allows looking up the server) // * the errorCode and errorText document.getElementById('error-note').style.display = 'block'; document.getElementById('error').innerText += 'The server ' + e.url + ' returned an error with code=' + e.errorCode + ':\n' + e.errorText + '\n'; } readServersFromLocalStorage(); // check if we have getUserMedia permissions. navigator.mediaDevices .enumerateDevices() .then(function(devices) { devices.forEach(function(device) { if (device.label !== '') { if ( document.getElementById('getUserMediaPermissions') ) { document.getElementById('getUserMediaPermissions').style.display = 'block'; } } }); }); gatherButton.click();