| OLD | NEW |
| 1 /** | 1 /** |
| 2 * Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 * Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 3 * Use of this source code is governed by a BSD-style license that can be | 3 * Use of this source code is governed by a BSD-style license that can be |
| 4 * found in the LICENSE file. | 4 * found in the LICENSE file. |
| 5 */ | 5 */ |
| 6 | 6 |
| 7 // This file requires these functions to be defined globally by someone else: | |
| 8 // function createPeerConnection(stun_server, useRtpDataChannel) | |
| 9 // function createOffer(peerConnection, constraints, callback) | |
| 10 // function receiveOffer(peerConnection, offer, constraints, callback) | |
| 11 // function receiveAnswer(peerConnection, answer, callback) | |
| 12 | |
| 13 // Currently these functions are supplied by jsep01_call.js. | |
| 14 | |
| 15 /** | 7 /** |
| 16 * We need a STUN server for some API calls. | 8 * We need a STUN server for some API calls. |
| 17 * @private | 9 * @private |
| 18 */ | 10 */ |
| 19 var STUN_SERVER = 'stun.l.google.com:19302'; | 11 var STUN_SERVER = 'stun.l.google.com:19302'; |
| 20 | 12 |
| 21 /** | 13 /** |
| 22 * This object represents the call. | 14 * The one and only peer connection in this page. |
| 23 * @private | 15 * @private |
| 24 */ | 16 */ |
| 25 var gPeerConnection = null; | 17 var gPeerConnection = null; |
| 26 | 18 |
| 27 /** | 19 /** |
| 28 * If true, any created peer connection will use RTP data | |
| 29 * channels. Otherwise it will use SCTP data channels. | |
| 30 */ | |
| 31 var gUseRtpDataChannels = true; | |
| 32 | |
| 33 /** | |
| 34 * This stores ICE candidates generated on this side. | 20 * This stores ICE candidates generated on this side. |
| 35 * @private | 21 * @private |
| 36 */ | 22 */ |
| 37 var gIceCandidates = []; | 23 var gIceCandidates = []; |
| 38 | 24 |
| 25 /** |
| 26 * Keeps track of whether we have seen crypto information in the SDP. |
| 27 * @private |
| 28 */ |
| 29 var gHasSeenCryptoInSdp = 'no-crypto-seen'; |
| 30 |
| 39 // Public interface to tests. These are expected to be called with | 31 // Public interface to tests. These are expected to be called with |
| 40 // ExecuteJavascript invocations from the browser tests and will return answers | 32 // ExecuteJavascript invocations from the browser tests and will return answers |
| 41 // through the DOM automation controller. | 33 // through the DOM automation controller. |
| 42 | 34 |
| 43 /** | 35 /** |
| 44 * Creates a peer connection. Must be called before most other public functions | 36 * Creates a peer connection. Must be called before most other public functions |
| 45 * in this file. | 37 * in this file. |
| 46 */ | 38 */ |
| 47 function preparePeerConnection() { | 39 function preparePeerConnection() { |
| 48 if (gPeerConnection != null) | 40 if (gPeerConnection != null) |
| 49 throw failTest('creating peer connection, but we already have one.'); | 41 throw failTest('creating peer connection, but we already have one.'); |
| 50 | 42 |
| 51 gPeerConnection = createPeerConnection(STUN_SERVER, gUseRtpDataChannels); | 43 gPeerConnection = createPeerConnection_(STUN_SERVER); |
| 52 returnToTest('ok-peerconnection-created'); | 44 returnToTest('ok-peerconnection-created'); |
| 53 } | 45 } |
| 54 | 46 |
| 55 /** | 47 /** |
| 56 * Asks this page to create a local offer. | 48 * Asks this page to create a local offer. |
| 57 * | 49 * |
| 58 * Returns a string on the format ok-(JSON encoded session description). | 50 * Returns a string on the format ok-(JSON encoded session description). |
| 59 * | 51 * |
| 60 * @param {!object} constraints Any createOffer constraints. | 52 * @param {!object} constraints Any createOffer constraints. |
| 61 */ | 53 */ |
| 62 function createLocalOffer(constraints) { | 54 function createLocalOffer(constraints) { |
| 63 if (gPeerConnection == null) | 55 peerConnection_().createOffer( |
| 64 throw failTest('Negotiating call, but we have no peer connection.'); | 56 function(localOffer) { |
| 57 success_('createOffer'); |
| 58 setLocalDescription(peerConnection, localOffer); |
| 65 | 59 |
| 66 // TODO(phoglund): move jsep01.call stuff into this file and remove need | 60 returnToTest('ok-' + JSON.stringify(localOffer)); |
| 67 // of the createOffer method, etc. | 61 }, |
| 68 createOffer(gPeerConnection, constraints, function(localOffer) { | 62 function() { failure_('createOffer'); }, |
| 69 returnToTest('ok-' + JSON.stringify(localOffer)); | 63 constraints); |
| 70 }); | |
| 71 } | 64 } |
| 72 | 65 |
| 73 /** | 66 /** |
| 74 * Asks this page to accept an offer and generate an answer. | 67 * Asks this page to accept an offer and generate an answer. |
| 75 * | 68 * |
| 76 * Returns a string on the format ok-(JSON encoded session description). | 69 * Returns a string on the format ok-(JSON encoded session description). |
| 77 * | 70 * |
| 78 * @param {!string} sessionDescJson A JSON-encoded session description of type | 71 * @param {!string} sessionDescJson A JSON-encoded session description of type |
| 79 * 'offer'. | 72 * 'offer'. |
| 80 * @param {!object} constraints Any createAnswer constraints. | 73 * @param {!object} constraints Any createAnswer constraints. |
| 81 */ | 74 */ |
| 82 function receiveOfferFromPeer(sessionDescJson, constraints) { | 75 function receiveOfferFromPeer(sessionDescJson, constraints) { |
| 83 if (gPeerConnection == null) | |
| 84 throw failTest('Receiving offer, but we have no peer connection.'); | |
| 85 | |
| 86 offer = parseJson_(sessionDescJson); | 76 offer = parseJson_(sessionDescJson); |
| 87 if (!offer.type) | 77 if (!offer.type) |
| 88 failTest('Got invalid session description from peer: ' + sessionDescJson); | 78 failTest('Got invalid session description from peer: ' + sessionDescJson); |
| 89 if (offer.type != 'offer') | 79 if (offer.type != 'offer') |
| 90 failTest('Expected to receive offer from peer, got ' + offer.type); | 80 failTest('Expected to receive offer from peer, got ' + offer.type); |
| 91 | 81 |
| 92 receiveOffer(gPeerConnection, offer , constraints, function(answer) { | 82 var sessionDescription = new RTCSessionDescription(offer); |
| 93 returnToTest('ok-' + JSON.stringify(answer)); | 83 peerConnection_().setRemoteDescription( |
| 94 }); | 84 sessionDescription, |
| 85 function() { success_('setRemoteDescription'); }, |
| 86 function() { failure_('setRemoteDescription'); }); |
| 87 |
| 88 peerConnection_().createAnswer( |
| 89 function(answer) { |
| 90 success_('createAnswer'); |
| 91 setLocalDescription(peerConnection, answer); |
| 92 returnToTest('ok-' + JSON.stringify(answer)); |
| 93 }, |
| 94 function() { failure_('createAnswer'); }, |
| 95 constraints); |
| 95 } | 96 } |
| 96 | 97 |
| 97 /** | 98 /** |
| 98 * Asks this page to accept an answer generated by the peer in response to a | 99 * Asks this page to accept an answer generated by the peer in response to a |
| 99 * previous offer by this page | 100 * previous offer by this page |
| 100 * | 101 * |
| 101 * Returns a string ok-accepted-answer on success. | 102 * Returns a string ok-accepted-answer on success. |
| 102 * | 103 * |
| 103 * @param {!string} sessionDescJson A JSON-encoded session description of type | 104 * @param {!string} sessionDescJson A JSON-encoded session description of type |
| 104 * 'answer'. | 105 * 'answer'. |
| 105 */ | 106 */ |
| 106 function receiveAnswerFromPeer(sessionDescJson) { | 107 function receiveAnswerFromPeer(sessionDescJson) { |
| 107 if (gPeerConnection == null) | |
| 108 throw failTest('Receiving offer, but we have no peer connection.'); | |
| 109 | |
| 110 answer = parseJson_(sessionDescJson); | 108 answer = parseJson_(sessionDescJson); |
| 111 if (!answer.type) | 109 if (!answer.type) |
| 112 failTest('Got invalid session description from peer: ' + sessionDescJson); | 110 failTest('Got invalid session description from peer: ' + sessionDescJson); |
| 113 if (answer.type != 'answer') | 111 if (answer.type != 'answer') |
| 114 failTest('Expected to receive answer from peer, got ' + answer.type); | 112 failTest('Expected to receive answer from peer, got ' + answer.type); |
| 115 | 113 |
| 116 receiveAnswer(gPeerConnection, answer, function() { | 114 var sessionDescription = new RTCSessionDescription(answer); |
| 117 returnToTest('ok-accepted-answer'); | 115 peerConnection_().setRemoteDescription( |
| 118 }); | 116 sessionDescription, |
| 117 function() { |
| 118 success_('setRemoteDescription'); |
| 119 returnToTest('ok-accepted-answer'); |
| 120 }, |
| 121 function() { failure_('setRemoteDescription'); }); |
| 119 } | 122 } |
| 120 | 123 |
| 121 /** | 124 /** |
| 122 * Adds the local stream to the peer connection. You will have to re-negotiate | 125 * Adds the local stream to the peer connection. You will have to re-negotiate |
| 123 * the call for this to take effect in the call. | 126 * the call for this to take effect in the call. |
| 124 */ | 127 */ |
| 125 function addLocalStream() { | 128 function addLocalStream() { |
| 126 if (gPeerConnection == null) | 129 addLocalStreamToPeerConnection(peerConnection_()); |
| 127 throw failTest('adding local stream, but we have no peer connection.'); | |
| 128 | |
| 129 addLocalStreamToPeerConnection(gPeerConnection); | |
| 130 returnToTest('ok-added'); | 130 returnToTest('ok-added'); |
| 131 } | 131 } |
| 132 | 132 |
| 133 /** | 133 /** |
| 134 * Loads a file with WebAudio and connects it to the peer connection. | 134 * Loads a file with WebAudio and connects it to the peer connection. |
| 135 * | 135 * |
| 136 * The loadAudioAndAddToPeerConnection will return ok-added to the test when | 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 | 137 * the sound is loaded and added to the peer connection. The sound will start |
| 138 * playing when you call playAudioFile. | 138 * playing when you call playAudioFile. |
| 139 * | 139 * |
| 140 * @param url URL pointing to the file to play. You can assume that you can | 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 | 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 | 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). | 143 * relative to this directory (e.g. ../pyauto_private/webrtc/file.wav). |
| 144 */ | 144 */ |
| 145 function addAudioFile(url) { | 145 function addAudioFile(url) { |
| 146 if (gPeerConnection == null) | 146 loadAudioAndAddToPeerConnection(url, peerConnection_()); |
| 147 throw failTest('adding audio file, but we have no peer connection.'); | |
| 148 | |
| 149 loadAudioAndAddToPeerConnection(url, gPeerConnection); | |
| 150 } | 147 } |
| 151 | 148 |
| 152 /** | 149 /** |
| 153 * Mixes the local audio stream with an audio file through WebAudio. | 150 * Mixes the local audio stream with an audio file through WebAudio. |
| 154 * | 151 * |
| 155 * You must have successfully requested access to the user's microphone through | 152 * You must have successfully requested access to the user's microphone through |
| 156 * getUserMedia before calling this function (see getUserMedia.js). | 153 * getUserMedia before calling this function (see getUserMedia.js). |
| 157 * Additionally, you must have loaded an audio file to mix with. | 154 * Additionally, you must have loaded an audio file to mix with. |
| 158 * | 155 * |
| 159 * When playAudioFile is called, WebAudio will effectively mix the user's | 156 * When playAudioFile is called, WebAudio will effectively mix the user's |
| 160 * microphone input with the previously loaded file and feed that into the | 157 * microphone input with the previously loaded file and feed that into the |
| 161 * peer connection. | 158 * peer connection. |
| 162 */ | 159 */ |
| 163 function mixLocalStreamWithPreviouslyLoadedAudioFile() { | 160 function mixLocalStreamWithPreviouslyLoadedAudioFile() { |
| 164 if (gPeerConnection == null) | |
| 165 throw failTest('trying to mix in stream, but we have no peer connection.'); | |
| 166 if (getLocalStream() == null) | 161 if (getLocalStream() == null) |
| 167 throw failTest('trying to mix in stream, but we have no stream to mix in.'); | 162 throw failTest('trying to mix in stream, but we have no stream to mix in.'); |
| 168 | 163 |
| 169 mixLocalStreamIntoPeerConnection(gPeerConnection, getLocalStream()); | 164 mixLocalStreamIntoPeerConnection(peerConnection_(), getLocalStream()); |
| 170 } | 165 } |
| 171 | 166 |
| 172 /** | 167 /** |
| 173 * Must be called after addAudioFile. | 168 * Must be called after addAudioFile. |
| 174 */ | 169 */ |
| 175 function playAudioFile() { | 170 function playAudioFile() { |
| 176 if (gPeerConnection == null) | 171 playPreviouslyLoadedAudioFile(peerConnection_()); |
| 177 throw failTest('trying to play file, but we have no peer connection.'); | |
| 178 | |
| 179 playPreviouslyLoadedAudioFile(gPeerConnection); | |
| 180 returnToTest('ok-playing'); | 172 returnToTest('ok-playing'); |
| 181 } | 173 } |
| 182 | 174 |
| 183 /** | 175 /** |
| 184 * Hangs up a started call. Returns ok-call-hung-up on success. | 176 * Hangs up a started call. Returns ok-call-hung-up on success. |
| 185 */ | 177 */ |
| 186 function hangUp() { | 178 function hangUp() { |
| 187 if (gPeerConnection == null) | 179 peerConnection_().close(); |
| 188 throw failTest('hanging up, but has no peer connection'); | |
| 189 gPeerConnection.close(); | |
| 190 gPeerConnection = null; | 180 gPeerConnection = null; |
| 191 returnToTest('ok-call-hung-up'); | 181 returnToTest('ok-call-hung-up'); |
| 192 } | 182 } |
| 193 | 183 |
| 194 /** | 184 /** |
| 195 * Retrieves all ICE candidates generated on this side. Must be called after | 185 * Retrieves all ICE candidates generated on this side. Must be called after |
| 196 * ICE candidate generation is triggered (for instance by running a call | 186 * ICE candidate generation is triggered (for instance by running a call |
| 197 * negotiation). This function will wait if necessary if we're not done | 187 * negotiation). This function will wait if necessary if we're not done |
| 198 * generating ICE candidates on this side. | 188 * generating ICE candidates on this side. |
| 199 * | 189 * |
| 200 * Returns a JSON-encoded array of RTCIceCandidate instances to the test. | 190 * Returns a JSON-encoded array of RTCIceCandidate instances to the test. |
| 201 */ | 191 */ |
| 202 function getAllIceCandidates() { | 192 function getAllIceCandidates() { |
| 203 if (gPeerConnection == null) | 193 if (peerConnection_().iceGatheringState != 'complete') { |
| 204 throw failTest('Trying to get ICE candidates, but has no peer connection.'); | |
| 205 | |
| 206 if (gPeerConnection.iceGatheringState != 'complete') { | |
| 207 console.log('Still ICE gathering - waiting...'); | 194 console.log('Still ICE gathering - waiting...'); |
| 208 setTimeout(getAllIceCandidates, 100); | 195 setTimeout(getAllIceCandidates, 100); |
| 209 return; | 196 return; |
| 210 } | 197 } |
| 211 | 198 |
| 212 returnToTest(JSON.stringify(gIceCandidates)); | 199 returnToTest(JSON.stringify(gIceCandidates)); |
| 213 } | 200 } |
| 214 | 201 |
| 215 /** | 202 /** |
| 216 * Receives ICE candidates from the peer. | 203 * Receives ICE candidates from the peer. |
| 217 * | 204 * |
| 218 * Returns ok-received-candidates to the test on success. | 205 * Returns ok-received-candidates to the test on success. |
| 219 * | 206 * |
| 220 * @param iceCandidatesJson a JSON-encoded array of RTCIceCandidate instances. | 207 * @param iceCandidatesJson a JSON-encoded array of RTCIceCandidate instances. |
| 221 */ | 208 */ |
| 222 function receiveIceCandidates(iceCandidatesJson) { | 209 function receiveIceCandidates(iceCandidatesJson) { |
| 223 if (gPeerConnection == null) | |
| 224 throw failTest('Received ICE candidate, but has no peer connection'); | |
| 225 | |
| 226 var iceCandidates = parseJson_(iceCandidatesJson); | 210 var iceCandidates = parseJson_(iceCandidatesJson); |
| 227 if (!iceCandidates.length) | 211 if (!iceCandidates.length) |
| 228 throw failTest('Received invalid ICE candidate list from peer: ' + | 212 throw failTest('Received invalid ICE candidate list from peer: ' + |
| 229 iceCandidatesJson); | 213 iceCandidatesJson); |
| 230 | 214 |
| 231 iceCandidates.forEach(function(iceCandidate) { | 215 iceCandidates.forEach(function(iceCandidate) { |
| 232 if (!iceCandidate.candidate) | 216 if (!iceCandidate.candidate) |
| 233 failTest('Received invalid ICE candidate from peer: ' + | 217 failTest('Received invalid ICE candidate from peer: ' + |
| 234 iceCandidatesJson); | 218 iceCandidatesJson); |
| 235 | 219 |
| 236 gPeerConnection.addIceCandidate(new RTCIceCandidate(iceCandidate)); | 220 peerConnection_().addIceCandidate(new RTCIceCandidate(iceCandidate)); |
| 237 }); | 221 }); |
| 238 | 222 |
| 239 returnToTest('ok-received-candidates'); | 223 returnToTest('ok-received-candidates'); |
| 240 } | 224 } |
| 241 | 225 |
| 242 // Public interface to signaling implementations, such as JSEP. | 226 /** |
| 227 * Returns |
| 228 */ |
| 229 function hasSeenCryptoInSdp() { |
| 230 returnToTest(gHasSeenCryptoInSdp); |
| 231 } |
| 243 | 232 |
| 244 /** | 233 // Internals. |
| 245 * Enqueues an ICE candidate for sending to the peer. | 234 |
| 246 * | 235 /** @private */ |
| 247 * @param {!RTCIceCandidate} The ICE candidate to send. | 236 function createPeerConnection_(stun_server) { |
| 248 */ | 237 servers = {iceServers: [{url: 'stun:' + stun_server}]}; |
| 249 function sendIceCandidate(message) { | 238 try { |
| 250 gIceCandidates.push(message); | 239 peerConnection = new RTCPeerConnection(servers, {}); |
| 240 } catch (exception) { |
| 241 throw failTest('Failed to create peer connection: ' + exception); |
| 242 } |
| 243 peerConnection.onaddstream = addStreamCallback_; |
| 244 peerConnection.onremovestream = removeStreamCallback_; |
| 245 peerConnection.onicecandidate = iceCallback_; |
| 246 return peerConnection; |
| 247 } |
| 248 |
| 249 /** @private */ |
| 250 function peerConnection_() { |
| 251 if (gPeerConnection == null) |
| 252 throw failTest('Trying to use peer connection, but none was created.'); |
| 253 return gPeerConnection; |
| 254 } |
| 255 |
| 256 /** @private */ |
| 257 function success_(method) { |
| 258 debug(method + '(): success.'); |
| 259 } |
| 260 |
| 261 /** @private */ |
| 262 function failure_(method, error) { |
| 263 throw failTest(method + '() failed: ' + error); |
| 264 } |
| 265 |
| 266 /** @private */ |
| 267 function iceCallback_(event) { |
| 268 if (event.candidate) |
| 269 gIceCandidates.push(event.candidate); |
| 270 } |
| 271 |
| 272 /** @private */ |
| 273 function setLocalDescription(peerConnection, sessionDescription) { |
| 274 if (sessionDescription.sdp.search('a=crypto') != -1 || |
| 275 sessionDescription.sdp.search('a=fingerprint') != -1) |
| 276 gHasSeenCryptoInSdp = 'crypto-seen'; |
| 277 |
| 278 peerConnection.setLocalDescription( |
| 279 sessionDescription, |
| 280 function() { success_('setLocalDescription'); }, |
| 281 function() { failure_('setLocalDescription'); }); |
| 282 } |
| 283 |
| 284 /** @private */ |
| 285 function addStreamCallback_(event) { |
| 286 debug('Receiving remote stream...'); |
| 287 var videoTag = document.getElementById('remote-view'); |
| 288 attachMediaStream(videoTag, event.stream); |
| 289 } |
| 290 |
| 291 /** @private */ |
| 292 function removeStreamCallback_(event) { |
| 293 debug('Call ended.'); |
| 294 document.getElementById('remote-view').src = ''; |
| 251 } | 295 } |
| 252 | 296 |
| 253 /** | 297 /** |
| 254 * Parses JSON-encoded session descriptions and ICE candidates. | 298 * Parses JSON-encoded session descriptions and ICE candidates. |
| 255 * @private | 299 * @private |
| 256 */ | 300 */ |
| 257 function parseJson_(json) { | 301 function parseJson_(json) { |
| 258 // Escape since the \r\n in the SDP tend to get unescaped. | 302 // Escape since the \r\n in the SDP tend to get unescaped. |
| 259 jsonWithEscapedLineBreaks = json.replace(/\r\n/g, '\\r\\n'); | 303 jsonWithEscapedLineBreaks = json.replace(/\r\n/g, '\\r\\n'); |
| 260 try { | 304 try { |
| 261 return JSON.parse(jsonWithEscapedLineBreaks); | 305 return JSON.parse(jsonWithEscapedLineBreaks); |
| 262 } catch (exception) { | 306 } catch (exception) { |
| 263 failTest('Failed to parse JSON: ' + jsonWithEscapedLineBreaks + ', got ' + | 307 failTest('Failed to parse JSON: ' + jsonWithEscapedLineBreaks + ', got ' + |
| 264 exception); | 308 exception); |
| 265 } | 309 } |
| 266 } | 310 } |
| OLD | NEW |