| /** |
| * Copyright 2014 The Chromium Authors. All rights reserved. |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| /** |
| * The one and only peer connection in this page. |
| * @private |
| */ |
| var gPeerConnection = null; |
| |
| /** |
| * This stores ICE candidates generated on this side. |
| * @private |
| */ |
| var gIceCandidates = []; |
| |
| /** |
| * Keeps track of whether we have seen crypto information in the SDP. |
| * @private |
| */ |
| var gHasSeenCryptoInSdp = 'no-crypto-seen'; |
| |
| /** |
| * The default video codec that should be used. |
| * @private |
| */ |
| var gDefaultVideoCodec = null; |
| |
| /** |
| * Flag to indicate if Opus Dtx should be enabled. |
| * @private |
| */ |
| var gOpusDtx = false; |
| |
| // Public interface to tests. These are expected to be called with |
| // ExecuteJavascript invocations from the browser tests and will return answers |
| // through the DOM automation controller. |
| |
| /** |
| * Creates a peer connection. Must be called before most other public functions |
| * in this file. Alternatively, see |preparePeerConnectionWithCertificate|. |
| * @param {Object} keygenAlgorithm Unless null, this is an |AlgorithmIdentifier| |
| * to be used as parameter to |RTCPeerConnection.generateCertificate|. The |
| * resulting certificate will be used by the peer connection. If null, a default |
| * certificate is generated by the |RTCPeerConnection| instead. |
| */ |
| function preparePeerConnection(keygenAlgorithm = null) { |
| if (gPeerConnection !== null) |
| throw failTest('Creating peer connection, but we already have one.'); |
| |
| if (keygenAlgorithm === null) { |
| gPeerConnection = createPeerConnection_(null); |
| returnToTest('ok-peerconnection-created'); |
| } else { |
| RTCPeerConnection.generateCertificate(keygenAlgorithm).then( |
| function(certificate) { |
| preparePeerConnectionWithCertificate(certificate); |
| }, |
| function() { |
| failTest('Certificate generation failed. keygenAlgorithm: ' + |
| JSON.stringify(keygenAlgorithm)); |
| }); |
| } |
| } |
| |
| /** |
| * Creates a peer connection. Must be called before most other public functions |
| * in this file. Alternatively, see |preparePeerConnection|. |
| * @param {!Object} certificate The |RTCCertificate| that will be used by the |
| * peer connection. |
| */ |
| function preparePeerConnectionWithCertificate(certificate) { |
| if (gPeerConnection !== null) |
| throw failTest('Creating peer connection, but we already have one.'); |
| gPeerConnection = createPeerConnection_( |
| {iceServers:[], certificates:[certificate]}); |
| returnToTest('ok-peerconnection-created'); |
| } |
| |
| /** |
| * Sets the flag to force Opus Dtx to be used when creating an offer. |
| */ |
| function forceOpusDtx() { |
| gOpusDtx = true; |
| returnToTest('ok-forced'); |
| } |
| |
| /** |
| * Sets the default video codec be used when creating an offer. |
| * @param {string} videoCodec promotes the specified codec to be the default |
| * video codec, e.g. the first one in the list on the 'm=video' SDP offer |
| * line. |videoCodec| is the case-sensitive codec name, e.g. 'VP8' or |
| * 'H264'. |
| */ |
| function forceVideoCodec(videoCodec) { |
| gDefaultVideoCodec = videoCodec; |
| returnToTest('ok-forced'); |
| } |
| |
| /** |
| * Asks this page to create a local offer. |
| * |
| * Returns a string on the format ok-(JSON encoded session description). |
| * |
| * @param {!Object} constraints Any createOffer constraints. |
| */ |
| function createLocalOffer(constraints) { |
| peerConnection_().createOffer( |
| function(localOffer) { |
| success('createOffer'); |
| |
| setLocalDescription(peerConnection, localOffer); |
| if (gDefaultVideoCodec !== null) { |
| localOffer.sdp = setSdpDefaultVideoCodec(localOffer.sdp, |
| gDefaultVideoCodec); |
| } |
| if (gOpusDtx) { |
| localOffer.sdp = setOpusDtxEnabled(localOffer.sdp); |
| } |
| returnToTest('ok-' + JSON.stringify(localOffer)); |
| }, |
| function(error) { failure('createOffer', error); }, |
| constraints); |
| } |
| |
| /** |
| * Asks this page to accept an offer and generate an answer. |
| * |
| * Returns a string on the format ok-(JSON encoded session description). |
| * |
| * @param {!string} sessionDescJson A JSON-encoded session description of type |
| * 'offer'. |
| * @param {!Object} constraints Any createAnswer constraints. |
| */ |
| function receiveOfferFromPeer(sessionDescJson, constraints) { |
| offer = parseJson_(sessionDescJson); |
| if (!offer.type) |
| failTest('Got invalid session description from peer: ' + sessionDescJson); |
| if (offer.type != 'offer') |
| failTest('Expected to receive offer from peer, got ' + offer.type); |
| |
| var sessionDescription = new RTCSessionDescription(offer); |
| peerConnection_().setRemoteDescription( |
| sessionDescription, |
| function() { success('setRemoteDescription'); }, |
| function(error) { failure('setRemoteDescription', error); }); |
| |
| peerConnection_().createAnswer( |
| function(answer) { |
| success('createAnswer'); |
| setLocalDescription(peerConnection, answer); |
| if (gOpusDtx) { |
| answer.sdp = setOpusDtxEnabled(answer.sdp); |
| } |
| returnToTest('ok-' + JSON.stringify(answer)); |
| }, |
| function(error) { failure('createAnswer', error); }, |
| constraints); |
| } |
| |
| /** |
| * Verifies that the codec previously set using forceVideoCodec() is the |
| * default video codec, e.g. the first one in the list on the 'm=video' SDP |
| * answer line. If this is not the case, |failure| occurs. If no codec was |
| * previously set using forceVideoCodec(), this function will return |
| * 'ok-no-default-set'. |
| * |
| * @param {!string} sessionDescJson A JSON-encoded session description. |
| */ |
| function verifyDefaultVideoCodec(sessionDescJson) { |
| var sessionDesc = parseJson_(sessionDescJson); |
| if (gDefaultVideoCodec === null) { |
| returnToTest('ok-no-default-set'); |
| return; |
| } |
| if (!sessionDesc.type) { |
| failure('verifyDefaultVideoCodec', |
| 'Invalid session description: ' + sessionDescJson); |
| } |
| var defaultVideoCodec = getSdpDefaultVideoCodec(sessionDesc.sdp); |
| if (defaultVideoCodec === null) { |
| failure('verifyDefaultVideoCodec', |
| 'Could not determine default video codec.'); |
| } |
| if (gDefaultVideoCodec !== defaultVideoCodec) { |
| failure('verifyDefaultVideoCodec', |
| 'Expected default video codec ' + gDefaultVideoCodec + |
| ', got ' + defaultVideoCodec + '.'); |
| } |
| returnToTest('ok-verified'); |
| } |
| |
| /** |
| * Asks this page to accept an answer generated by the peer in response to a |
| * previous offer by this page |
| * |
| * Returns a string ok-accepted-answer on success. |
| * |
| * @param {!string} sessionDescJson A JSON-encoded session description of type |
| * 'answer'. |
| */ |
| function receiveAnswerFromPeer(sessionDescJson) { |
| answer = parseJson_(sessionDescJson); |
| if (!answer.type) |
| failTest('Got invalid session description from peer: ' + sessionDescJson); |
| if (answer.type != 'answer') |
| failTest('Expected to receive answer from peer, got ' + answer.type); |
| |
| var sessionDescription = new RTCSessionDescription(answer); |
| peerConnection_().setRemoteDescription( |
| sessionDescription, |
| function() { |
| success('setRemoteDescription'); |
| returnToTest('ok-accepted-answer'); |
| }, |
| function(error) { failure('setRemoteDescription', error); }); |
| } |
| |
| /** |
| * Adds the local stream to the peer connection. You will have to re-negotiate |
| * the call for this to take effect in the call. |
| */ |
| function addLocalStream() { |
| addLocalStreamToPeerConnection(peerConnection_()); |
| returnToTest('ok-added'); |
| } |
| |
| /** |
| * Loads a file with WebAudio and connects it to the peer connection. |
| * |
| * The loadAudioAndAddToPeerConnection will return ok-added to the test when |
| * the sound is loaded and added to the peer connection. The sound will start |
| * playing when you call playAudioFile. |
| * |
| * @param url URL pointing to the file to play. You can assume that you can |
| * serve files from the repository's file system. For instance, to serve a |
| * file from chrome/test/data/pyauto_private/webrtc/file.wav, pass in a path |
| * relative to this directory (e.g. ../pyauto_private/webrtc/file.wav). |
| */ |
| function addAudioFile(url) { |
| loadAudioAndAddToPeerConnection(url, peerConnection_()); |
| } |
| |
| /** |
| * Must be called after addAudioFile. |
| */ |
| function playAudioFile() { |
| playPreviouslyLoadedAudioFile(peerConnection_()); |
| returnToTest('ok-playing'); |
| } |
| |
| /** |
| * Hangs up a started call. Returns ok-call-hung-up on success. |
| */ |
| function hangUp() { |
| peerConnection_().close(); |
| gPeerConnection = null; |
| returnToTest('ok-call-hung-up'); |
| } |
| |
| /** |
| * Retrieves all ICE candidates generated on this side. Must be called after |
| * ICE candidate generation is triggered (for instance by running a call |
| * negotiation). This function will wait if necessary if we're not done |
| * generating ICE candidates on this side. |
| * |
| * Returns a JSON-encoded array of RTCIceCandidate instances to the test. |
| */ |
| function getAllIceCandidates() { |
| if (peerConnection_().iceGatheringState != 'complete') { |
| console.log('Still ICE gathering - waiting...'); |
| setTimeout(getAllIceCandidates, 100); |
| return; |
| } |
| |
| returnToTest(JSON.stringify(gIceCandidates)); |
| } |
| |
| /** |
| * Receives ICE candidates from the peer. |
| * |
| * Returns ok-received-candidates to the test on success. |
| * |
| * @param iceCandidatesJson a JSON-encoded array of RTCIceCandidate instances. |
| */ |
| function receiveIceCandidates(iceCandidatesJson) { |
| var iceCandidates = parseJson_(iceCandidatesJson); |
| if (!iceCandidates.length) |
| throw failTest('Received invalid ICE candidate list from peer: ' + |
| iceCandidatesJson); |
| |
| iceCandidates.forEach(function(iceCandidate) { |
| if (!iceCandidate.candidate) |
| failTest('Received invalid ICE candidate from peer: ' + |
| iceCandidatesJson); |
| |
| peerConnection_().addIceCandidate(new RTCIceCandidate(iceCandidate, |
| function() { success('addIceCandidate'); }, |
| function(error) { failure('addIceCandidate', error); } |
| )); |
| }); |
| |
| returnToTest('ok-received-candidates'); |
| } |
| |
| /** |
| * Sets the mute state of the selected media element. |
| * |
| * Returns ok-muted on success. |
| * |
| * @param elementId The id of the element to mute. |
| * @param muted The mute state to set. |
| */ |
| function setMediaElementMuted(elementId, muted) { |
| var element = document.getElementById(elementId); |
| if (!element) |
| throw failTest('Cannot mute ' + elementId + '; does not exist.'); |
| element.muted = muted; |
| returnToTest('ok-muted'); |
| } |
| |
| /** |
| * Returns |
| */ |
| function hasSeenCryptoInSdp() { |
| returnToTest(gHasSeenCryptoInSdp); |
| } |
| |
| /** |
| * Verifies that |RTCPeerConnection.getStats| returns stats. |
| * |
| * Returns ok-got-stats on success. |
| */ |
| function verifyStatsGenerated() { |
| peerConnection_().getStats( |
| function(response) { |
| var reports = response.result(); |
| var numStats = 0; |
| for (var i = 0; i < reports.length; i++) { |
| var statNames = reports[i].names(); |
| numStats += statNames.length; |
| for (var j = 0; j < statNames.length; j++) { |
| var statValue = reports[i].stat(statNames[j]); |
| if (typeof statValue != 'string') |
| throw failTest('A stat was returned that is not a string.'); |
| } |
| } |
| if (numStats === 0) |
| throw failTest('No stats was returned by getStats.'); |
| returnToTest('ok-got-stats'); |
| }); |
| } |
| |
| // Internals. |
| |
| /** @private */ |
| function createPeerConnection_(rtcConfig) { |
| try { |
| peerConnection = new RTCPeerConnection(rtcConfig, {}); |
| } catch (exception) { |
| throw failTest('Failed to create peer connection: ' + exception); |
| } |
| peerConnection.onaddstream = addStreamCallback_; |
| peerConnection.onremovestream = removeStreamCallback_; |
| peerConnection.onicecandidate = iceCallback_; |
| return peerConnection; |
| } |
| |
| /** @private */ |
| function peerConnection_() { |
| if (gPeerConnection == null) |
| throw failTest('Trying to use peer connection, but none was created.'); |
| return gPeerConnection; |
| } |
| |
| /** @private */ |
| function iceCallback_(event) { |
| if (event.candidate) |
| gIceCandidates.push(event.candidate); |
| } |
| |
| /** @private */ |
| function setLocalDescription(peerConnection, sessionDescription) { |
| if (sessionDescription.sdp.search('a=crypto') != -1 || |
| sessionDescription.sdp.search('a=fingerprint') != -1) |
| gHasSeenCryptoInSdp = 'crypto-seen'; |
| |
| peerConnection.setLocalDescription( |
| sessionDescription, |
| function() { success('setLocalDescription'); }, |
| function(error) { failure('setLocalDescription', error); }); |
| } |
| |
| /** @private */ |
| function addStreamCallback_(event) { |
| debug('Receiving remote stream...'); |
| var videoTag = document.getElementById('remote-view'); |
| attachMediaStream(videoTag, event.stream); |
| } |
| |
| /** @private */ |
| function removeStreamCallback_(event) { |
| debug('Call ended.'); |
| document.getElementById('remote-view').src = ''; |
| } |
| |
| /** |
| * Parses JSON-encoded session descriptions and ICE candidates. |
| * @private |
| */ |
| function parseJson_(json) { |
| // Escape since the \r\n in the SDP tend to get unescaped. |
| jsonWithEscapedLineBreaks = json.replace(/\r\n/g, '\\r\\n'); |
| try { |
| return JSON.parse(jsonWithEscapedLineBreaks); |
| } catch (exception) { |
| failTest('Failed to parse JSON: ' + jsonWithEscapedLineBreaks + ', got ' + |
| exception); |
| } |
| } |