2 * Copyright 2014 The Chromium Authors. All rights reserved.
3 * Use of this source code is governed by a BSD-style license that can be
4 * found in the LICENSE file.
8 * We need a STUN server for some API calls.
11 var STUN_SERVER = 'stun.l.google.com:19302';
14 * The one and only peer connection in this page.
17 var gPeerConnection = null;
20 * This stores ICE candidates generated on this side.
23 var gIceCandidates = [];
26 * Keeps track of whether we have seen crypto information in the SDP.
29 var gHasSeenCryptoInSdp = 'no-crypto-seen';
31 // Public interface to tests. These are expected to be called with
32 // ExecuteJavascript invocations from the browser tests and will return answers
33 // through the DOM automation controller.
36 * Creates a peer connection. Must be called before most other public functions
39 function preparePeerConnection() {
40 if (gPeerConnection != null)
41 throw failTest('creating peer connection, but we already have one.');
43 gPeerConnection = createPeerConnection_(STUN_SERVER);
44 returnToTest('ok-peerconnection-created');
48 * Asks this page to create a local offer.
50 * Returns a string on the format ok-(JSON encoded session description).
52 * @param {!Object} constraints Any createOffer constraints.
54 function createLocalOffer(constraints) {
55 peerConnection_().createOffer(
56 function(localOffer) {
57 success_('createOffer');
58 setLocalDescription(peerConnection, localOffer);
60 returnToTest('ok-' + stringifyDOMObject_(localOffer));
62 function(error) { failure_('createOffer', error); },
67 * Asks this page to accept an offer and generate an answer.
69 * Returns a string on the format ok-(JSON encoded session description).
71 * @param {!string} sessionDescJson A JSON-encoded session description of type
73 * @param {!Object} constraints Any createAnswer constraints.
75 function receiveOfferFromPeer(sessionDescJson, constraints) {
76 offer = parseJson_(sessionDescJson);
78 failTest('Got invalid session description from peer: ' + sessionDescJson);
79 if (offer.type != 'offer')
80 failTest('Expected to receive offer from peer, got ' + offer.type);
82 var sessionDescription = new RTCSessionDescription(offer);
83 peerConnection_().setRemoteDescription(
85 function() { success_('setRemoteDescription'); },
86 function(error) { failure_('setRemoteDescription', error); });
88 peerConnection_().createAnswer(
90 success_('createAnswer');
91 setLocalDescription(peerConnection, answer);
92 returnToTest('ok-' + stringifyDOMObject_(answer));
94 function(error) { failure_('createAnswer', error); },
99 * Asks this page to accept an answer generated by the peer in response to a
100 * previous offer by this page
102 * Returns a string ok-accepted-answer on success.
104 * @param {!string} sessionDescJson A JSON-encoded session description of type
107 function receiveAnswerFromPeer(sessionDescJson) {
108 answer = parseJson_(sessionDescJson);
110 failTest('Got invalid session description from peer: ' + sessionDescJson);
111 if (answer.type != 'answer')
112 failTest('Expected to receive answer from peer, got ' + answer.type);
114 var sessionDescription = new RTCSessionDescription(answer);
115 peerConnection_().setRemoteDescription(
118 success_('setRemoteDescription');
119 returnToTest('ok-accepted-answer');
121 function(error) { failure_('setRemoteDescription', error); });
125 * Adds the local stream to the peer connection. You will have to re-negotiate
126 * the call for this to take effect in the call.
128 function addLocalStream() {
129 addLocalStreamToPeerConnection(peerConnection_());
130 returnToTest('ok-added');
134 * Loads a file with WebAudio and connects it to the peer connection.
136 * The loadAudioAndAddToPeerConnection will return ok-added to the test when
137 * the sound is loaded and added to the peer connection. The sound will start
138 * playing when you call playAudioFile.
140 * @param url URL pointing to the file to play. You can assume that you can
141 * serve files from the repository's file system. For instance, to serve a
142 * file from chrome/test/data/pyauto_private/webrtc/file.wav, pass in a path
143 * relative to this directory (e.g. ../pyauto_private/webrtc/file.wav).
145 function addAudioFile(url) {
146 loadAudioAndAddToPeerConnection(url, peerConnection_());
150 * Must be called after addAudioFile.
152 function playAudioFile() {
153 playPreviouslyLoadedAudioFile(peerConnection_());
154 returnToTest('ok-playing');
158 * Hangs up a started call. Returns ok-call-hung-up on success.
161 peerConnection_().close();
162 gPeerConnection = null;
163 returnToTest('ok-call-hung-up');
167 * Retrieves all ICE candidates generated on this side. Must be called after
168 * ICE candidate generation is triggered (for instance by running a call
169 * negotiation). This function will wait if necessary if we're not done
170 * generating ICE candidates on this side.
172 * Returns a JSON-encoded array of RTCIceCandidate instances to the test.
174 function getAllIceCandidates() {
175 if (peerConnection_().iceGatheringState != 'complete') {
176 console.log('Still ICE gathering - waiting...');
177 setTimeout(getAllIceCandidates, 100);
181 returnToTest(stringifyDOMObject_(gIceCandidates));
185 * Receives ICE candidates from the peer.
187 * Returns ok-received-candidates to the test on success.
189 * @param iceCandidatesJson a JSON-encoded array of RTCIceCandidate instances.
191 function receiveIceCandidates(iceCandidatesJson) {
192 var iceCandidates = parseJson_(iceCandidatesJson);
193 if (!iceCandidates.length)
194 throw failTest('Received invalid ICE candidate list from peer: ' +
197 iceCandidates.forEach(function(iceCandidate) {
198 if (!iceCandidate.candidate)
199 failTest('Received invalid ICE candidate from peer: ' +
202 peerConnection_().addIceCandidate(new RTCIceCandidate(iceCandidate,
203 function() { success_('addIceCandidate'); },
204 function(error) { failure_('addIceCandidate', error); }
208 returnToTest('ok-received-candidates');
214 function hasSeenCryptoInSdp() {
215 returnToTest(gHasSeenCryptoInSdp);
221 function createPeerConnection_(stun_server) {
222 servers = {iceServers: [{url: 'stun:' + stun_server}]};
224 peerConnection = new RTCPeerConnection(servers, {});
225 } catch (exception) {
226 throw failTest('Failed to create peer connection: ' + exception);
228 peerConnection.onaddstream = addStreamCallback_;
229 peerConnection.onremovestream = removeStreamCallback_;
230 peerConnection.onicecandidate = iceCallback_;
231 return peerConnection;
235 function peerConnection_() {
236 if (gPeerConnection == null)
237 throw failTest('Trying to use peer connection, but none was created.');
238 return gPeerConnection;
242 function success_(method) {
243 debug(method + '(): success.');
247 function failure_(method, error) {
248 throw failTest(method + '() failed: ' + stringifyDOMObject_(error));
252 function iceCallback_(event) {
254 gIceCandidates.push(event.candidate);
258 function setLocalDescription(peerConnection, sessionDescription) {
259 if (sessionDescription.sdp.search('a=crypto') != -1 ||
260 sessionDescription.sdp.search('a=fingerprint') != -1)
261 gHasSeenCryptoInSdp = 'crypto-seen';
263 peerConnection.setLocalDescription(
265 function() { success_('setLocalDescription'); },
266 function(error) { failure_('setLocalDescription', error); });
270 function addStreamCallback_(event) {
271 debug('Receiving remote stream...');
272 var videoTag = document.getElementById('remote-view');
273 attachMediaStream(videoTag, event.stream);
277 function removeStreamCallback_(event) {
278 debug('Call ended.');
279 document.getElementById('remote-view').src = '';
283 * Stringifies a DOM object.
285 * This function stringifies not only own properties but also DOM attributes
286 * which are on a prototype chain. Note that JSON.stringify only stringifies
290 function stringifyDOMObject_(object)
292 function deepCopy(src) {
293 if (typeof src != "object")
295 var dst = Array.isArray(src) ? [] : {};
296 for (var property in src) {
297 dst[property] = deepCopy(src[property]);
301 return JSON.stringify(deepCopy(object));
305 * Parses JSON-encoded session descriptions and ICE candidates.
308 function parseJson_(json) {
309 // Escape since the \r\n in the SDP tend to get unescaped.
310 jsonWithEscapedLineBreaks = json.replace(/\r\n/g, '\\r\\n');
312 return JSON.parse(jsonWithEscapedLineBreaks);
313 } catch (exception) {
314 failTest('Failed to parse JSON: ' + jsonWithEscapedLineBreaks + ', got ' +