| /** |
| * 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'; |
| |
| // 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. |
| */ |
| function preparePeerConnection() { |
| if (gPeerConnection != null) |
| throw failTest('creating peer connection, but we already have one.'); |
| |
| gPeerConnection = createPeerConnection_(); |
| returnToTest('ok-peerconnection-created'); |
| } |
| |
| /** |
| * 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. |
| * @param {string} videoCodec If not null, 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 createLocalOffer(constraints, videoCodec = null) { |
| peerConnection_().createOffer( |
| function(localOffer) { |
| success('createOffer'); |
| |
| setLocalDescription(peerConnection, localOffer); |
| if (videoCodec !== null) |
| localOffer.sdp = setSdpDefaultVideoCodec(localOffer.sdp, videoCodec); |
| |
| 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); |
| returnToTest('ok-' + JSON.stringify(answer)); |
| }, |
| function(error) { failure('createAnswer', error); }, |
| constraints); |
| } |
| |
| /** |
| * Verifies that the specified codec 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. |
| * |
| * @param {!string} sessionDescJson A JSON-encoded session description. |
| * @param {!string} expectedVideoCodec The case-sensitive codec name, e.g. |
| * 'VP8' or 'H264'. |
| */ |
| function verifyDefaultVideoCodec(sessionDescJson, expectedVideoCodec) { |
| var sessionDesc = parseJson_(sessionDescJson); |
| 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 (expectedVideoCodec !== defaultVideoCodec) { |
| failure('verifyDefaultVideoCodec', |
| 'Expected default video codec ' + expectedVideoCodec + |
| ', 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); |
| } |
| |
| // Internals. |
| |
| /** @private */ |
| function createPeerConnection_() { |
| try { |
| peerConnection = new RTCPeerConnection(null, {}); |
| } 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); |
| } |
| } |