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 /** |
| 8 * See http://dev.w3.org/2011/webrtc/editor/getusermedia.html for more |
| 9 * information on getUserMedia. See |
| 10 * http://dev.w3.org/2011/webrtc/editor/webrtc.html for more information on |
| 11 * peerconnection and webrtc in general. |
| 12 */ |
| 13 |
| 14 /** TODO(jansson) give it a better name |
| 15 * Global namespace object. |
| 16 */ |
| 17 var global = {}; |
| 18 |
7 /** | 19 /** |
8 * Used as a shortcut. Moved to the top of the page due to race conditions. | 20 * We need a STUN server for some API calls. |
| 21 * @private |
| 22 */ |
| 23 var STUN_SERVER = 'stun.l.google.com:19302'; |
| 24 |
| 25 /** @private */ |
| 26 global.transformOutgoingSdp = function(sdp) { return sdp; }; |
| 27 |
| 28 /** @private */ |
| 29 global.dataStatusCallback = function(status) {}; |
| 30 |
| 31 /** @private */ |
| 32 global.dataCallback = function(data) {}; |
| 33 |
| 34 /** @private */ |
| 35 global.dtmfOnToneChange = function(tone) {}; |
| 36 |
| 37 /** |
| 38 * Used as a shortcut for finding DOM elements by ID. |
9 * @param {string} id is a case-sensitive string representing the unique ID of | 39 * @param {string} id is a case-sensitive string representing the unique ID of |
10 * the element being sought. | 40 * the element being sought. |
11 * @return {object} id returns the element object specified as a parameter | 41 * @return {object} id returns the element object specified as a parameter |
12 */ | 42 */ |
13 $ = function(id) { | 43 $ = function(id) { |
14 return document.getElementById(id); | 44 return document.getElementById(id); |
15 }; | 45 }; |
16 | 46 |
17 /** | 47 /** |
18 * Sets the global variable gUseRtpDataChannel in message_handling.js | 48 * Prepopulate constraints from JS to the UI. Enumerate devices available |
19 * based on the check box element 'data-channel-type-rtp' checked status on | 49 * via getUserMedia, register elements to be used for local storage. |
20 * peerconnection.html. Also prints a help text to inform the user. | |
21 */ | 50 */ |
22 function setPcDataChannelType() { | 51 window.onload = function() { |
23 var useRtpDataChannels = $('data-channel-type-rtp').checked; | 52 hookupDataChannelCallbacks_(); |
24 useRtpDataChannelsForNewPeerConnections(useRtpDataChannels); | 53 hookupDtmfSenderCallback_(); |
25 if (useRtpDataChannels) { | 54 updateGetUserMediaConstraints(); |
26 debug('Make sure to tick the RTP check box on both the calling and ' + | 55 setupLocalStorageFieldValues(); |
27 'answering side to ensure a data channel can be setup properly as ' + | 56 acceptIncomingCalls(); |
28 'it has to use the same transport protocol (RTP to RTP and SCTP to ' + | 57 if ($('get-devices-onload').checked == true) { |
29 'SCTP).'); | 58 getDevices(); |
30 } | 59 } |
| 60 }; |
| 61 |
| 62 /** |
| 63 * Disconnect before the tab is closed. |
| 64 */ |
| 65 window.onbeforeunload = function() { |
| 66 disconnect_(); |
| 67 }; |
| 68 |
| 69 /** TODO Add element.id as a parameter and call this function instead? |
| 70 * A list of element id's to be registered for local storage. |
| 71 */ |
| 72 function setupLocalStorageFieldValues() { |
| 73 registerLocalStorage_('pc-server'); |
| 74 registerLocalStorage_('pc-createanswer-constraints'); |
| 75 registerLocalStorage_('pc-createoffer-constraints'); |
| 76 registerLocalStorage_('get-devices-onload'); |
31 } | 77 } |
32 | 78 |
| 79 // Public HTML functions |
| 80 |
33 // The *Here functions are called from peerconnection.html and will make calls | 81 // The *Here functions are called from peerconnection.html and will make calls |
34 // into our underlying JavaScript library with the values from the page | 82 // into our underlying JavaScript library with the values from the page |
35 // (have to be named differently to avoid name clashes with existing functions). | 83 // (have to be named differently to avoid name clashes with existing functions). |
36 | 84 |
37 function getUserMediaFromHere() { | 85 function getUserMediaFromHere() { |
38 var constraints = $('getusermedia-constraints').value; | 86 var constraints = $('getusermedia-constraints').value; |
39 try { | 87 try { |
40 doGetUserMedia(constraints); | 88 doGetUserMedia_(constraints); |
41 } catch (exception) { | 89 } catch (exception) { |
42 print_('getUserMedia says: ' + exception); | 90 print_('getUserMedia says: ' + exception); |
43 } | 91 } |
44 } | 92 } |
45 | 93 |
46 function connectFromHere() { | 94 function connectFromHere() { |
47 var server = $('pc-server').value; | 95 var server = $('pc-server').value; |
48 if ($('peer-id').value == '') { | 96 if ($('peer-id').value == '') { |
49 // Generate a random name to distinguish us from other tabs: | 97 // Generate a random name to distinguish us from other tabs: |
50 $('peer-id').value = 'peer_' + Math.floor(Math.random() * 10000); | 98 $('peer-id').value = 'peer_' + Math.floor(Math.random() * 10000); |
51 debug('Our name from now on will be ' + $('peer-id').value); | 99 print_('Our name from now on will be ' + $('peer-id').value); |
52 } | 100 } |
53 connect(server, $('peer-id').value); | 101 connect(server, $('peer-id').value); |
54 } | 102 } |
55 | 103 |
56 function negotiateCallFromHere() { | 104 function negotiateCallFromHere() { |
57 // Set the global variables used in jsep01_call.js with values from our UI. | 105 // Set the global variables with values from our UI. |
58 setCreateOfferConstraints(getEvaluatedJavaScript_( | 106 setCreateOfferConstraints(getEvaluatedJavaScript_( |
59 $('pc-createoffer-constraints').value)); | 107 $('pc-createoffer-constraints').value)); |
60 setCreateAnswerConstraints(getEvaluatedJavaScript_( | 108 setCreateAnswerConstraints(getEvaluatedJavaScript_( |
61 $('pc-createanswer-constraints').value)); | 109 $('pc-createanswer-constraints').value)); |
62 | 110 |
63 ensureHasPeerConnection_(); | 111 ensureHasPeerConnection_(); |
64 try { | 112 negotiateCall_(); |
65 negotiateCall(); | |
66 } catch (exception) { | |
67 error(exception); | |
68 } | |
69 } | 113 } |
70 | 114 |
71 function addLocalStreamFromHere() { | 115 function addLocalStreamFromHere() { |
72 ensureHasPeerConnection_(); | 116 ensureHasPeerConnection_(); |
73 addLocalStream(); | 117 addLocalStream(); |
74 } | 118 } |
75 | 119 |
76 function removeLocalStreamFromHere() { | 120 function removeLocalStreamFromHere() { |
77 removeLocalStream(); | 121 removeLocalStream(); |
78 } | 122 } |
79 | 123 |
80 function hangUpFromHere() { | 124 function hangUpFromHere() { |
81 hangUp(); | 125 hangUp(); |
82 acceptIncomingCallsAgain(); | 126 acceptIncomingCalls(); |
83 } | 127 } |
84 | 128 |
85 function toggleRemoteVideoFromHere() { | 129 function toggleRemoteVideoFromHere() { |
86 toggleRemoteStream(function(remoteStream) { | 130 toggleRemoteStream(function(remoteStream) { |
87 return remoteStream.getVideoTracks()[0]; | 131 return remoteStream.getVideoTracks()[0]; |
88 }, 'video'); | 132 }, 'video'); |
89 } | 133 } |
90 | 134 |
91 function toggleRemoteAudioFromHere() { | 135 function toggleRemoteAudioFromHere() { |
92 toggleRemoteStream(function(remoteStream) { | 136 toggleRemoteStream(function(remoteStream) { |
(...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
149 /** | 193 /** |
150 * Updates the constraints in the getusermedia-constraints text box with a | 194 * Updates the constraints in the getusermedia-constraints text box with a |
151 * MediaStreamConstraints string. This string is created based on the status of | 195 * MediaStreamConstraints string. This string is created based on the status of |
152 * the checkboxes for audio and video. If device enumeration is supported and | 196 * the checkboxes for audio and video. If device enumeration is supported and |
153 * device source id's are not null they will be added to the constraints string. | 197 * device source id's are not null they will be added to the constraints string. |
154 * Fetches the screen size using "screen" in Chrome as we need to pass a max | 198 * Fetches the screen size using "screen" in Chrome as we need to pass a max |
155 * resolution else it defaults to 640x480 in the constraints for screen | 199 * resolution else it defaults to 640x480 in the constraints for screen |
156 * capturing. | 200 * capturing. |
157 */ | 201 */ |
158 function updateGetUserMediaConstraints() { | 202 function updateGetUserMediaConstraints() { |
159 var audio_selected = $('audiosrc'); | 203 var audioSelected = $('audiosrc'); |
160 var video_selected = $('videosrc'); | 204 var videoSelected = $('videosrc'); |
161 var constraints = { | 205 var constraints = { |
162 audio: $('audio').checked, | 206 audio: $('audio').checked, |
163 video: $('video').checked | 207 video: $('video').checked |
164 }; | 208 }; |
165 if (audio_selected.disabled == false && video_selected.disabled == false) { | 209 |
166 var devices = getSourcesFromField(audio_selected, video_selected); | 210 if (audioSelected.disabled == false && videoSelected.disabled == false) { |
| 211 var devices = getSourcesFromField_(audioSelected, videoSelected); |
167 if ($('audio').checked == true) { | 212 if ($('audio').checked == true) { |
168 if (devices.audio_id != null) { | 213 if (devices.audioId != null) { |
169 constraints.audio = {optional: [{sourceId: devices.audio_id}]}; | 214 constraints.audio = {optional: [{sourceId: devices.audioId}]}; |
170 } | 215 } else { |
171 else { | |
172 constraints.audio = true; | 216 constraints.audio = true; |
173 } | 217 } |
174 } | 218 } |
175 if ($('video').checked == true) { | 219 if ($('video').checked == true) { |
176 // Default optional constraints placed here. | 220 // Default optional constraints placed here. |
177 constraints.video = {optional: [{minWidth: $('video-width').value}, | 221 constraints.video = {optional: [{minWidth: $('video-width').value}, |
178 {minHeight: $('video-height').value}, | 222 {minHeight: $('video-height').value}, |
179 {googCpuOveruseDetection: true}, | 223 {googCpuOveruseDetection: true}, |
180 {googLeakyBucket: true}] | 224 {googLeakyBucket: true}] |
181 }; | 225 }; |
182 if (devices.video_id != null) { | 226 if (devices.videoId != null) { |
183 constraints.video.optional.push({sourceId: devices.video_id}); | 227 constraints.video.optional.push({sourceId: devices.videoId}); |
184 } | 228 } |
185 } | 229 } |
186 } | 230 } |
| 231 |
187 if ($('screencapture').checked) { | 232 if ($('screencapture').checked) { |
188 var constraints = { | 233 var constraints = { |
189 audio: $('audio').checked, | 234 audio: $('audio').checked, |
190 video: {mandatory: {chromeMediaSource: 'screen', | 235 video: {mandatory: {chromeMediaSource: 'screen', |
191 maxWidth: screen.width, | 236 maxWidth: screen.width, |
192 maxHeight: screen.height}} | 237 maxHeight: screen.height}} |
193 }; | 238 }; |
194 if ($('audio').checked == true) | 239 if ($('audio').checked == true) |
195 debug('Audio for screencapture is not implemented yet, please ' + | 240 print_('Audio for screencapture is not implemented yet, please ' + |
196 'try to set audio = false prior requesting screencapture'); | 241 'try to set audio = false prior requesting screencapture'); |
197 } | 242 } |
198 $('getusermedia-constraints').value = | 243 $('getusermedia-constraints').value = JSON.stringify(constraints, null, ' '); |
199 JSON.stringify(constraints, null, ' '); | |
200 } | 244 } |
201 | 245 |
202 function showServerHelp() { | 246 function showServerHelp() { |
203 alert('You need to build and run a peerconnection_server on some ' + | 247 alert('You need to build and run a peerconnection_server on some ' + |
204 'suitable machine. To build it in chrome, just run make/ninja ' + | 248 'suitable machine. To build it in chrome, just run make/ninja ' + |
205 'peerconnection_server. Otherwise, read in https://code.google' + | 249 'peerconnection_server. Otherwise, read in https://code.google' + |
206 '.com/searchframe#xSWYf0NTG_Q/trunk/peerconnection/README&q=REA' + | 250 '.com/searchframe#xSWYf0NTG_Q/trunk/peerconnection/README&q=REA' + |
207 'DME%20package:webrtc%5C.googlecode%5C.com.'); | 251 'DME%20package:webrtc%5C.googlecode%5C.com.'); |
208 } | 252 } |
209 | 253 |
210 function toggleHelp() { | |
211 var help = $('help'); | |
212 if (help.style.display == 'none') | |
213 help.style.display = 'inline'; | |
214 else | |
215 help.style.display = 'none'; | |
216 } | |
217 | |
218 function clearLog() { | 254 function clearLog() { |
219 $('messages').innerHTML = ''; | 255 $('messages').innerHTML = ''; |
220 $('debug').innerHTML = ''; | 256 $('debug').innerHTML = ''; |
221 } | 257 } |
222 | 258 |
223 /** | 259 /** |
224 * Prepopulate constraints from JS to the UI and setup callbacks in the scripts | 260 * Stops the local stream. |
225 * shared with browser tests or automated tests. Enumerates devices available | 261 */ |
226 * via getUserMedia. | 262 function stopLocalStream() { |
227 */ | 263 if (global.localStream == null) |
228 window.onload = function() { | 264 error_('Tried to stop local stream, ' + |
229 $('pc-createoffer-constraints').value = JSON.stringify( | 265 'but media access is not granted.'); |
230 gCreateOfferConstraints, null, ' '); | 266 |
231 $('pc-createanswer-constraints').value = JSON.stringify( | 267 global.localStream.stop(); |
232 gCreateAnswerConstraints, null, ' '); | 268 } |
233 replaceReturnCallback(print_); | 269 |
234 replaceDebugCallback(debug_); | 270 /** |
235 doNotAutoAddLocalStreamWhenCalled(); | 271 * Adds the current local media stream to a peer connection. |
236 hookupDataChannelCallbacks_(); | 272 * @param {RTCPeerConnection} peerConnection |
237 hookupDtmfSenderCallback_(); | 273 */ |
238 displayVideoSize_($('local-view')); | 274 function addLocalStreamToPeerConnection(peerConnection) { |
239 displayVideoSize_($('remote-view')); | 275 if (global.localStream == null) |
240 updateGetUserMediaConstraints(); | 276 error_('Tried to add local stream to peer connection, but there is no ' + |
241 setPcDataChannelType(); | 277 'stream yet.'); |
242 setupLocalStorageFieldValues(); | 278 try { |
243 if ($('get-devices-onload').checked == true) { | 279 peerConnection.addStream(global.localStream, global.addStreamConstraints); |
244 getDevices(); | 280 } catch (exception) { |
245 } | 281 error_('Failed to add stream with constraints ' + |
246 }; | 282 global.addStreamConstraints + ': ' + exception); |
| 283 } |
| 284 print_('Added local stream.'); |
| 285 } |
| 286 |
| 287 /** |
| 288 * Removes the local stream from the peer connection. |
| 289 * @param {rtcpeerconnection} peerConnection |
| 290 */ |
| 291 function removeLocalStreamFromPeerConnection(peerConnection) { |
| 292 if (global.localStream == null) |
| 293 error_('Tried to remove local stream from peer connection, but there is ' + |
| 294 'no stream yet.'); |
| 295 try { |
| 296 peerConnection.removeStream(global.localStream); |
| 297 } catch (exception) { |
| 298 error_('Could not remove stream: ' + exception); |
| 299 } |
| 300 print_('Removed local stream.'); |
| 301 } |
| 302 |
| 303 /** |
| 304 * Enumerates the audio and video devices available in Chrome and adds the |
| 305 * devices to the HTML elements with Id 'audiosrc' and 'videosrc'. |
| 306 * Checks if device enumeration is supported and if the 'audiosrc' + 'videosrc' |
| 307 * elements exists, if not a debug printout will be displayed. |
| 308 * If the device label is empty, audio/video + sequence number will be used to |
| 309 * populate the name. Also makes sure the children has been loaded in order |
| 310 * to update the constraints. |
| 311 */ |
| 312 function getDevices() { |
| 313 var audio_select = $('audiosrc'); |
| 314 var video_select = $('videosrc'); |
| 315 var get_devices = $('get-devices'); |
| 316 audio_select.innerHTML = ''; |
| 317 video_select.innerHTML = ''; |
| 318 try { |
| 319 eval(MediaStreamTrack.getSources(function() {})); |
| 320 } catch (exception) { |
| 321 audio_select.disabled = true; |
| 322 video_select.disabled = true; |
| 323 refresh_devices.disabled = true; |
| 324 updateGetUserMediaConstraints(); |
| 325 error_('Device enumeration not supported. ' + exception); |
| 326 } |
| 327 MediaStreamTrack.getSources(function(devices) { |
| 328 for (var i = 0; i < devices.length; i++) { |
| 329 var option = document.createElement('option'); |
| 330 option.value = devices[i].id; |
| 331 option.text = devices[i].label; |
| 332 if (devices[i].kind == 'audio') { |
| 333 if (option.text == '') { |
| 334 option.text = devices[i].id; |
| 335 } |
| 336 audio_select.appendChild(option); |
| 337 } else if (devices[i].kind == 'video') { |
| 338 if (option.text == '') { |
| 339 option.text = devices[i].id; |
| 340 } |
| 341 video_select.appendChild(option); |
| 342 } else { |
| 343 error_('Device type ' + devices[i].kind + ' not recognized, ' + |
| 344 'cannot enumerate device. Currently only device types' + |
| 345 '\'audio\' and \'video\' are supported'); |
| 346 updateGetUserMediaConstraints(); |
| 347 return; |
| 348 } |
| 349 } |
| 350 }); |
| 351 checkIfDeviceDropdownsArePopulated_(); |
| 352 } |
| 353 |
| 354 /** |
| 355 * Sets the transform to apply just before setting the local description and |
| 356 * sending to the peer. |
| 357 * @param {function} transformFunction A function which takes one SDP string as |
| 358 * argument and returns the modified SDP string. |
| 359 */ |
| 360 function setOutgoingSdpTransform(transformFunction) { |
| 361 global.transformOutgoingSdp = transformFunction; |
| 362 } |
| 363 |
| 364 /** |
| 365 * Sets the MediaConstraints to be used for PeerConnection createAnswer() calls. |
| 366 * @param {string} mediaConstraints The constraints, as defined in the |
| 367 * PeerConnection JS API spec. |
| 368 */ |
| 369 function setCreateAnswerConstraints(mediaConstraints) { |
| 370 global.createAnswerConstraints = mediaConstraints; |
| 371 } |
| 372 |
| 373 /** |
| 374 * Sets the MediaConstraints to be used for PeerConnection createOffer() calls. |
| 375 * @param {string} mediaConstraints The constraints, as defined in the |
| 376 * PeerConnection JS API spec. |
| 377 */ |
| 378 function setCreateOfferConstraints(mediaConstraints) { |
| 379 global.createOfferConstraints = mediaConstraints; |
| 380 } |
| 381 |
| 382 /** |
| 383 * Sets the callback functions that will receive DataChannel readyState updates |
| 384 * and received data. |
| 385 * @param {function} status_callback The function that will receive a string |
| 386 * with |
| 387 * the current DataChannel readyState. |
| 388 * @param {function} data_callback The function that will a string with data |
| 389 * received from the remote peer. |
| 390 */ |
| 391 function setDataCallbacks(status_callback, data_callback) { |
| 392 global.dataStatusCallback = status_callback; |
| 393 global.dataCallback = data_callback; |
| 394 } |
| 395 |
| 396 /** |
| 397 * Sends data on an active DataChannel. |
| 398 * @param {string} data The string that will be sent to the remote peer. |
| 399 */ |
| 400 function sendDataOnChannel(data) { |
| 401 if (global.dataChannel == null) |
| 402 error_('Trying to send data, but there is no DataChannel.'); |
| 403 global.dataChannel.send(data); |
| 404 } |
| 405 |
| 406 /** |
| 407 * Sets the callback function that will receive DTMF sender ontonechange events. |
| 408 * @param {function} ontonechange The function that will receive a string with |
| 409 * the tone that has just begun playout. |
| 410 */ |
| 411 function setOnToneChange(ontonechange) { |
| 412 global.dtmfOnToneChange = ontonechange; |
| 413 } |
| 414 |
| 415 /** |
| 416 * Inserts DTMF tones on an active DTMF sender. |
| 417 * @param {string} tones to be sent. |
| 418 * @param {string} duration duration of the tones to be sent. |
| 419 * @param {string} interToneGap gap between the tones to be sent. |
| 420 */ |
| 421 function insertDtmf(tones, duration, interToneGap) { |
| 422 if (global.dtmfSender == null) |
| 423 error_('Trying to send DTMF, but there is no DTMF sender.'); |
| 424 global.dtmfSender.insertDTMF(tones, duration, interToneGap); |
| 425 } |
| 426 |
| 427 function handleMessage(peerConnection, message) { |
| 428 var parsed_msg = JSON.parse(message); |
| 429 if (parsed_msg.type) { |
| 430 var session_description = new RTCSessionDescription(parsed_msg); |
| 431 peerConnection.setRemoteDescription( |
| 432 session_description, |
| 433 function() { success_('setRemoteDescription'); }, |
| 434 function() { failure_('setRemoteDescription'); }); |
| 435 if (session_description.type == 'offer') { |
| 436 print_('createAnswer with constraints: ' + |
| 437 JSON.stringify(global.createAnswerConstraints, null, ' ')); |
| 438 peerConnection.createAnswer( |
| 439 setLocalAndSendMessage_, |
| 440 function() { failure_('createAnswer'); }, |
| 441 global.createAnswerConstraints); |
| 442 } |
| 443 return; |
| 444 } else if (parsed_msg.candidate) { |
| 445 var candidate = new RTCIceCandidate(parsed_msg); |
| 446 peerConnection.addIceCandidate(candidate); |
| 447 return; |
| 448 } |
| 449 error_('unknown message received'); |
| 450 } |
| 451 |
| 452 function createPeerConnection(stun_server, useRtpDataChannels) { |
| 453 servers = {iceServers: [{url: 'stun:' + stun_server}]}; |
| 454 try { |
| 455 var constraints = { optional: [{ RtpDataChannels: useRtpDataChannels }]}; |
| 456 peerConnection = new webkitRTCPeerConnection(servers, constraints); |
| 457 } catch (exception) { |
| 458 error_('Failed to create peer connection: ' + exception); |
| 459 } |
| 460 peerConnection.onaddstream = addStreamCallback_; |
| 461 peerConnection.onremovestream = removeStreamCallback_; |
| 462 peerConnection.onicecandidate = iceCallback_; |
| 463 peerConnection.ondatachannel = onCreateDataChannelCallback_; |
| 464 return peerConnection; |
| 465 } |
| 466 |
| 467 function setupCall(peerConnection) { |
| 468 print_('createOffer with constraints: ' + |
| 469 JSON.stringify(global.createOfferConstraints, null, ' ')); |
| 470 peerConnection.createOffer( |
| 471 setLocalAndSendMessage_, |
| 472 function() { failure_('createOffer'); }, |
| 473 global.createOfferConstraints); |
| 474 } |
| 475 |
| 476 function answerCall(peerConnection, message) { |
| 477 handleMessage(peerConnection, message); |
| 478 } |
| 479 |
| 480 function createDataChannel(peerConnection, label) { |
| 481 if (global.dataChannel != null && global.dataChannel.readyState != 'closed') |
| 482 error_('Creating DataChannel, but we already have one.'); |
| 483 |
| 484 global.dataChannel = peerConnection.createDataChannel(label, |
| 485 { reliable: false }); |
| 486 print_('DataChannel with label ' + global.dataChannel.label + ' initiated ' + |
| 487 'locally.'); |
| 488 hookupDataChannelEvents(); |
| 489 } |
| 490 |
| 491 function closeDataChannel(peerConnection) { |
| 492 if (global.dataChannel == null) |
| 493 error_('Closing DataChannel, but none exists.'); |
| 494 print_('DataChannel with label ' + global.dataChannel.label + |
| 495 ' is beeing closed.'); |
| 496 global.dataChannel.close(); |
| 497 } |
| 498 |
| 499 function createDtmfSender(peerConnection) { |
| 500 if (global.dtmfSender != null) |
| 501 error_('Creating DTMF sender, but we already have one.'); |
| 502 |
| 503 var localStream = global.localStream(); |
| 504 if (localStream == null) |
| 505 error_('Creating DTMF sender but local stream is null.'); |
| 506 local_audio_track = localStream.getAudioTracks()[0]; |
| 507 global.dtmfSender = peerConnection.createDTMFSender(local_audio_track); |
| 508 global.dtmfSender.ontonechange = global.dtmfOnToneChange; |
| 509 } |
| 510 |
| 511 /** |
| 512 * Connects to the provided peerconnection_server. |
| 513 * |
| 514 * @param {string} serverUrl The server URL in string form without an ending |
| 515 * slash, something like http://localhost:8888. |
| 516 * @param {string} clientName The name to use when connecting to the server. |
| 517 */ |
| 518 function connect(serverUrl, clientName) { |
| 519 if (global.ourPeerId != null) |
| 520 error_('connecting, but is already connected.'); |
| 521 |
| 522 print_('Connecting to ' + serverUrl + ' as ' + clientName); |
| 523 global.serverUrl = serverUrl; |
| 524 global.ourClientName = clientName; |
| 525 |
| 526 request = new XMLHttpRequest(); |
| 527 request.open('GET', serverUrl + '/sign_in?' + clientName, true); |
| 528 print_(serverUrl + '/sign_in?' + clientName); |
| 529 request.onreadystatechange = function() { |
| 530 connectCallback_(request); |
| 531 }; |
| 532 request.send(); |
| 533 } |
| 534 |
| 535 /** |
| 536 * Checks if the remote peer has connected. Returns peer-connected if that is |
| 537 * the case, otherwise no-peer-connected. |
| 538 */ |
| 539 function remotePeerIsConnected() { |
| 540 if (global.remotePeerId == null) |
| 541 print_('no-peer-connected'); |
| 542 else |
| 543 print_('peer-connected'); |
| 544 } |
| 545 |
| 546 /** |
| 547 * Creates a peer connection. Must be called before most other public functions |
| 548 * in this file. |
| 549 */ |
| 550 function preparePeerConnection() { |
| 551 if (global.peerConnection != null) |
| 552 error_('creating peer connection, but we already have one.'); |
| 553 |
| 554 global.useRtpDataChannels = $('data-channel-type-rtp').checked; |
| 555 global.peerConnection = createPeerConnection(STUN_SERVER, |
| 556 global.useRtpDataChannels); |
| 557 print_('ok-peerconnection-created'); |
| 558 } |
| 559 |
| 560 /** |
| 561 * Adds the local stream to the peer connection. You will have to re-negotiate |
| 562 * the call for this to take effect in the call. |
| 563 */ |
| 564 function addLocalStream() { |
| 565 if (global.peerConnection == null) |
| 566 error_('adding local stream, but we have no peer connection.'); |
| 567 |
| 568 addLocalStreamToPeerConnection(global.peerConnection); |
| 569 print_('ok-added'); |
| 570 } |
| 571 |
| 572 /** |
| 573 * Removes the local stream from the peer connection. You will have to |
| 574 * re-negotiate the call for this to take effect in the call. |
| 575 */ |
| 576 function removeLocalStream() { |
| 577 if (global.peerConnection == null) |
| 578 error_('attempting to remove local stream, but no call is up'); |
| 579 |
| 580 removeLocalStreamFromPeerConnection(global.peerConnection); |
| 581 print_('ok-local-stream-removed'); |
| 582 } |
| 583 |
| 584 /** |
| 585 * (see getReadyState_) |
| 586 */ |
| 587 function getPeerConnectionReadyState() { |
| 588 print_(getReadyState_()); |
| 589 } |
| 590 |
| 591 /** |
| 592 * Toggles the remote audio stream's enabled state on the peer connection, given |
| 593 * that a call is active. Returns ok-[typeToToggle]-toggled-to-[true/false] |
| 594 * on success. |
| 595 * |
| 596 * @param {function} selectAudioOrVideoTrack A function that takes a remote |
| 597 * stream as argument and returns a track (e.g. either the video or audio |
| 598 * track). |
| 599 * @param {function} typeToToggle Either "audio" or "video" depending on what |
| 600 * the selector function selects. |
| 601 */ |
| 602 function toggleRemoteStream(selectAudioOrVideoTrack, typeToToggle) { |
| 603 if (global.peerConnection == null) |
| 604 error_('Tried to toggle remote stream, but have no peer connection.'); |
| 605 if (global.peerConnection.getRemoteStreams().length == 0) |
| 606 error_('Tried to toggle remote stream, but not receiving any stream.'); |
| 607 |
| 608 var track = selectAudioOrVideoTrack( |
| 609 global.peerConnection.getRemoteStreams()[0]); |
| 610 toggle_(track, 'remote', typeToToggle); |
| 611 } |
| 612 |
| 613 /** |
| 614 * See documentation on toggleRemoteStream (this function is the same except |
| 615 * we are looking at local streams). |
| 616 */ |
| 617 function toggleLocalStream(selectAudioOrVideoTrack, typeToToggle) { |
| 618 if (global.peerConnection == null) |
| 619 error_('Tried to toggle local stream, but have no peer connection.'); |
| 620 if (global.peerConnection.getLocalStreams().length == 0) |
| 621 error_('Tried to toggle local stream, but there is no local stream in ' + |
| 622 'the call.'); |
| 623 |
| 624 var track = selectAudioOrVideoTrack( |
| 625 global.peerConnection.getLocalStreams()[0]); |
| 626 toggle_(track, 'local', typeToToggle); |
| 627 } |
| 628 |
| 629 /** |
| 630 * Hangs up a started call. Returns ok-call-hung-up on success. This tab will |
| 631 * not accept any incoming calls after this call. |
| 632 */ |
| 633 function hangUp() { |
| 634 if (global.peerConnection == null) |
| 635 error_('hanging up, but has no peer connection'); |
| 636 if (getReadyState_() != 'active') |
| 637 error_('hanging up, but ready state is not active (no call up).'); |
| 638 sendToPeer(global.remotePeerId, 'BYE'); |
| 639 closeCall_(); |
| 640 global.acceptsIncomingCalls = false; |
| 641 print_('ok-call-hung-up'); |
| 642 } |
| 643 |
| 644 /** |
| 645 * Start accepting incoming calls. |
| 646 */ |
| 647 function acceptIncomingCalls() { |
| 648 global.acceptsIncomingCalls = true; |
| 649 } |
| 650 |
| 651 /** |
| 652 * Creates a DataChannel on the current PeerConnection. Only one DataChannel can |
| 653 * be created on each PeerConnection. |
| 654 * Returns ok-datachannel-created on success. |
| 655 */ |
| 656 function createDataChannelOnPeerConnection() { |
| 657 if (global.peerConnection == null) |
| 658 error_('Tried to create data channel, but have no peer connection.'); |
| 659 |
| 660 createDataChannel(global.peerConnection, global.ourClientName); |
| 661 print_('ok-datachannel-created'); |
| 662 } |
| 663 |
| 664 /** |
| 665 * Close the DataChannel on the current PeerConnection. |
| 666 * Returns ok-datachannel-close on success. |
| 667 */ |
| 668 function closeDataChannelOnPeerConnection() { |
| 669 if (global.peerConnection == null) |
| 670 error_('Tried to close data channel, but have no peer connection.'); |
| 671 |
| 672 closeDataChannel(global.peerConnection); |
| 673 print_('ok-datachannel-close'); |
| 674 } |
| 675 |
| 676 /** |
| 677 * Creates a DTMF sender on the current PeerConnection. |
| 678 * Returns ok-dtmfsender-created on success. |
| 679 */ |
| 680 function createDtmfSenderOnPeerConnection() { |
| 681 if (global.peerConnection == null) |
| 682 error_('Tried to create DTMF sender, but have no peer connection.'); |
| 683 |
| 684 createDtmfSender(global.peerConnection); |
| 685 print_('ok-dtmfsender-created'); |
| 686 } |
| 687 |
| 688 /** |
| 689 * Send DTMF tones on the global.dtmfSender. |
| 690 * Returns ok-dtmf-sent on success. |
| 691 */ |
| 692 function insertDtmfOnSender(tones, duration, interToneGap) { |
| 693 if (global.dtmfSender == null) |
| 694 error_('Tried to insert DTMF tones, but have no DTMF sender.'); |
| 695 |
| 696 insertDtmf(tones, duration, interToneGap); |
| 697 print_('ok-dtmf-sent'); |
| 698 } |
| 699 |
| 700 /** |
| 701 * Sends a message to a peer through the peerconnection_server. |
| 702 */ |
| 703 function sendToPeer(peer, message) { |
| 704 var messageToLog = message.sdp ? message.sdp : message; |
| 705 print_('Sending message ' + messageToLog + ' to peer ' + peer + '.'); |
| 706 |
| 707 var request = new XMLHttpRequest(); |
| 708 var url = global.serverUrl + '/message?peer_id=' + global.ourPeerId + '&to=' + |
| 709 peer; |
| 710 request.open('POST', url, false); |
| 711 request.setRequestHeader('Content-Type', 'text/plain'); |
| 712 request.send(message); |
| 713 } |
| 714 |
| 715 /** |
| 716 * @param {!string} videoTagId The ID of the video tag to update. |
| 717 * @param {!string} width The width of the video to update the video tag, if |
| 718 * width or height is 0, size will be taken from videoTag.videoWidth. |
| 719 * @param {!string} height The height of the video to update the video tag, if |
| 720 * width or height is 0 size will be taken from the videoTag.videoHeight. |
| 721 */ |
| 722 function updateVideoTagSize(videoTagId, width, height) { |
| 723 var videoTag = $(videoTagId); |
| 724 if (width > 0 || height > 0) { |
| 725 videoTag.width = width; |
| 726 videoTag.height = height; |
| 727 } |
| 728 else { |
| 729 if (videoTag.videoWidth > 0 || videoTag.videoHeight > 0) { |
| 730 videoTag.width = videoTag.videoWidth; |
| 731 videoTag.height = videoTag.videoHeight; |
| 732 } |
| 733 else { |
| 734 print_('"' + videoTagId + '" video stream size is 0, skipping resize'); |
| 735 } |
| 736 } |
| 737 print_('Set video tag "' + videoTagId + '" size to ' + videoTag.width + 'x' + |
| 738 videoTag.height); |
| 739 displayVideoSize_(videoTag); |
| 740 } |
| 741 |
| 742 // Internals. |
| 743 |
| 744 /** |
| 745 * Disconnects from the peerconnection server. Returns ok-disconnected on |
| 746 * success. |
| 747 */ |
| 748 function disconnect_() { |
| 749 if (global.ourPeerId == null) |
| 750 return; |
| 751 |
| 752 request = new XMLHttpRequest(); |
| 753 request.open('GET', global.serverUrl + '/sign_out?peer_id=' + |
| 754 global.ourPeerId, false); |
| 755 request.send(); |
| 756 global.ourPeerId = null; |
| 757 print_('ok-disconnected'); |
| 758 } |
| 759 |
| 760 /** |
| 761 * Returns true if we are disconnected from peerconnection_server. |
| 762 */ |
| 763 function isDisconnected_() { |
| 764 return global.ourPeerId == null; |
| 765 } |
| 766 |
| 767 /** |
| 768 * @private |
| 769 * @return {!string} The current peer connection's ready state, or |
| 770 * 'no-peer-connection' if there is no peer connection up. |
| 771 * |
| 772 * NOTE: The PeerConnection states are changing and until chromium has |
| 773 * implemented the new states we have to use this interim solution of |
| 774 * always assuming that the PeerConnection is 'active'. |
| 775 */ |
| 776 function getReadyState_() { |
| 777 if (global.peerConnection == null) |
| 778 return 'no-peer-connection'; |
| 779 |
| 780 return 'active'; |
| 781 } |
| 782 |
| 783 /** |
| 784 * This function asks permission to use the webcam and mic from the browser. It |
| 785 * will return ok-requested to the test. This does not mean the request was |
| 786 * approved though. The test will then have to click past the dialog that |
| 787 * appears in Chrome, which will run either the OK or failed callback as a |
| 788 * a result. To see which callback was called, use obtainGetUserMediaResult_(). |
| 789 * @private |
| 790 * @param {string} constraints Defines what to be requested, with mandatory |
| 791 * and optional constraints defined. The contents of this parameter depends |
| 792 * on the WebRTC version. This should be JavaScript code that we eval(). |
| 793 */ |
| 794 function doGetUserMedia_(constraints) { |
| 795 if (!getUserMedia) { |
| 796 print_('Browser does not support WebRTC.'); |
| 797 return; |
| 798 } |
| 799 try { |
| 800 var evaluatedConstraints; |
| 801 eval('evaluatedConstraints = ' + constraints); |
| 802 } catch (exception) { |
| 803 error_('Not valid JavaScript expression: ' + constraints); |
| 804 } |
| 805 print_('Requesting doGetUserMedia: constraints: ' + constraints); |
| 806 getUserMedia(evaluatedConstraints, getUserMediaOkCallback_, |
| 807 getUserMediaFailedCallback_); |
| 808 } |
| 809 |
| 810 /** |
| 811 * Must be called after calling doGetUserMedia. |
| 812 * @private |
| 813 * @return {string} Returns not-called-yet if we have not yet been called back |
| 814 * by WebRTC. Otherwise it returns either ok-got-stream or |
| 815 * failed-with-error-x (where x is the error code from the error |
| 816 * callback) depending on which callback got called by WebRTC. |
| 817 */ |
| 818 function obtainGetUserMediaResult_() { |
| 819 if (global.requestWebcamAndMicrophoneResult == null) |
| 820 global.requestWebcamAndMicrophoneResult = ' not called yet'; |
| 821 |
| 822 return global.requestWebcamAndMicrophoneResult; |
| 823 |
| 824 } |
| 825 |
| 826 /** |
| 827 * Negotiates a call with the other side. This will create a peer connection on |
| 828 * the other side if there isn't one. |
| 829 * |
| 830 * To call this method we need to be aware of the other side, e.g. we must be |
| 831 * connected to peerconnection_server and we must have exactly one peer on that |
| 832 * server. |
| 833 * |
| 834 * This method may be called any number of times. If you haven't added any |
| 835 * streams to the call, an "empty" call will result. The method will return |
| 836 * ok-negotiating immediately to the test if the negotiation was successfully |
| 837 * sent. |
| 838 * @private |
| 839 */ |
| 840 function negotiateCall_() { |
| 841 if (global.peerConnection == null) |
| 842 error_('Negotiating call, but we have no peer connection.'); |
| 843 if (global.ourPeerId == null) |
| 844 error_('Negotiating call, but not connected.'); |
| 845 if (global.remotePeerId == null) |
| 846 error_('Negotiating call, but missing remote peer.'); |
| 847 |
| 848 setupCall(global.peerConnection); |
| 849 print_('ok-negotiating'); |
| 850 } |
| 851 |
| 852 /** |
| 853 * This provides the selected source id from the objects in the parameters |
| 854 * provided to this function. If the audioSelect or video_select objects does |
| 855 * not have any HTMLOptions children it will return null in the source object. |
| 856 * @param {!object} audioSelect HTML drop down element with audio devices added |
| 857 * as HTMLOptionsCollection children. |
| 858 * @param {!object} videoSelect HTML drop down element with audio devices added |
| 859 * as HTMLOptionsCollection children. |
| 860 * @return {!object} source contains audio and video source ID from |
| 861 * the selected devices in the drop down menu elements. |
| 862 * @private |
| 863 */ |
| 864 function getSourcesFromField_(audioSelect, videoSelect) { |
| 865 var source = { |
| 866 audioId: null, |
| 867 videoId: null |
| 868 }; |
| 869 if (audioSelect.options.length > 0) { |
| 870 source.audioId = audioSelect.options[audioSelect.selectedIndex].value; |
| 871 } |
| 872 if (videoSelect.options.length > 0) { |
| 873 source.videoId = videoSelect.options[videoSelect.selectedIndex].value; |
| 874 } |
| 875 return source; |
| 876 } |
| 877 |
| 878 /** |
| 879 * @private |
| 880 * @param {NavigatorUserMediaError} error Error containing details. |
| 881 */ |
| 882 function getUserMediaFailedCallback_(error) { |
| 883 print_('GetUserMedia FAILED: Maybe the camera is in use by another process?'); |
| 884 gRequestWebcamAndMicrophoneResult = 'failed-with-error-' + error.name; |
| 885 print_(gRequestWebcamAndMicrophoneResult); |
| 886 } |
| 887 |
| 888 /** @private */ |
| 889 function success_(method) { |
| 890 print_(method + '(): success.'); |
| 891 } |
| 892 |
| 893 /** @private */ |
| 894 function failure_(method, error) { |
| 895 error_(method + '() failed: ' + error); |
| 896 } |
| 897 |
| 898 /** @private */ |
| 899 function iceCallback_(event) { |
| 900 if (event.candidate) |
| 901 sendToPeer(global.remotePeerId, JSON.stringify(event.candidate)); |
| 902 } |
| 903 |
| 904 /** @private */ |
| 905 function setLocalAndSendMessage_(session_description) { |
| 906 session_description.sdp = |
| 907 global.transformOutgoingSdp(session_description.sdp); |
| 908 global.peerConnection.setLocalDescription( |
| 909 session_description, |
| 910 function() { success_('setLocalDescription'); }, |
| 911 function() { failure_('setLocalDescription'); }); |
| 912 print_('Sending SDP message:\n' + session_description.sdp); |
| 913 sendToPeer(global.remotePeerId, JSON.stringify(session_description)); |
| 914 } |
| 915 |
| 916 /** @private */ |
| 917 function addStreamCallback_(event) { |
| 918 print_('Receiving remote stream...'); |
| 919 var videoTag = document.getElementById('remote-view'); |
| 920 attachMediaStream(videoTag, event.stream); |
| 921 |
| 922 window.addEventListener('loadedmetadata', function() { |
| 923 displayVideoSize_(videoTag);}, true); |
| 924 } |
| 925 |
| 926 /** @private */ |
| 927 function removeStreamCallback_(event) { |
| 928 print_('Call ended.'); |
| 929 document.getElementById('remote-view').src = ''; |
| 930 } |
| 931 |
| 932 /** @private */ |
| 933 function onCreateDataChannelCallback_(event) { |
| 934 if (global.dataChannel != null && global.dataChannel.readyState != 'closed') { |
| 935 error_('Received DataChannel, but we already have one.'); |
| 936 } |
| 937 |
| 938 global.dataChannel = event.channel; |
| 939 print_('DataChannel with label ' + global.dataChannel.label + |
| 940 ' initiated by remote peer.'); |
| 941 hookupDataChannelEvents(); |
| 942 } |
| 943 |
| 944 /** @private */ |
| 945 function hookupDataChannelEvents() { |
| 946 global.dataChannel.onmessage = global.dataCallback; |
| 947 global.dataChannel.onopen = onDataChannelReadyStateChange_; |
| 948 global.dataChannel.onclose = onDataChannelReadyStateChange_; |
| 949 // Trigger global.dataStatusCallback so an application is notified |
| 950 // about the created data channel. |
| 951 onDataChannelReadyStateChange_(); |
| 952 } |
| 953 |
| 954 /** @private */ |
| 955 function onDataChannelReadyStateChange_() { |
| 956 var readyState = global.dataChannel.readyState; |
| 957 print_('DataChannel state:' + readyState); |
| 958 global.dataStatusCallback(readyState); |
| 959 } |
| 960 |
| 961 /** |
| 962 * @private |
| 963 * @param {MediaStream} stream Media stream. |
| 964 */ |
| 965 function getUserMediaOkCallback_(stream) { |
| 966 global.localStream = stream; |
| 967 global.requestWebcamAndMicrophoneResult = 'ok-got-stream'; |
| 968 |
| 969 if (stream.getVideoTracks().length > 0) { |
| 970 // Show the video tag if we did request video in the getUserMedia call. |
| 971 var videoTag = $('local-view'); |
| 972 attachMediaStream(videoTag, stream); |
| 973 |
| 974 window.addEventListener('loadedmetadata', function() { |
| 975 displayVideoSize_(videoTag);}, true); |
| 976 } |
| 977 } |
| 978 |
| 979 /** |
| 980 * @private |
| 981 * @param {string} videoTag The ID of the video tag + stream used to |
| 982 * write the size to a HTML tag based on id if the div's exists. |
| 983 */ |
| 984 function displayVideoSize_(videoTag) { |
| 985 if ($(videoTag.id + '-stream-size') && $(videoTag.id + '-size')) { |
| 986 if (videoTag.videoWidth > 0 || videoTag.videoHeight > 0) { |
| 987 $(videoTag.id + '-stream-size').innerHTML = '(stream size: ' + |
| 988 videoTag.videoWidth + 'x' + |
| 989 videoTag.videoHeight + ')'; |
| 990 $(videoTag.id + '-size').innerHTML = videoTag.width + 'x' + |
| 991 videoTag.height; |
| 992 } |
| 993 } else { |
| 994 print_('Skipping updating -stream-size and -size tags due to div\'s ' + |
| 995 'are missing'); |
| 996 } |
| 997 } |
247 | 998 |
248 /** | 999 /** |
249 * Checks if the 'audiosrc' and 'videosrc' drop down menu elements has had all | 1000 * Checks if the 'audiosrc' and 'videosrc' drop down menu elements has had all |
250 * of its children appended in order to provide device ID's to the function | 1001 * of its children appended in order to provide device ID's to the function |
251 * 'updateGetUserMediaConstraints()', used in turn to populate the getUserMedia | 1002 * 'updateGetUserMediaConstraints()', used in turn to populate the getUserMedia |
252 * constraints text box when the page has loaded. If not the user is informed | 1003 * constraints text box when the page has loaded. |
253 * and instructions on how to populate the field is provided. | 1004 * @private |
254 */ | 1005 */ |
255 function checkIfDeviceDropdownsArePopulated() { | 1006 function checkIfDeviceDropdownsArePopulated_() { |
256 if (document.addEventListener) { | 1007 if (document.addEventListener) { |
257 $('audiosrc').addEventListener('DOMNodeInserted', | 1008 $('audiosrc').addEventListener('DOMNodeInserted', |
258 updateGetUserMediaConstraints, false); | 1009 updateGetUserMediaConstraints, false); |
259 $('videosrc').addEventListener('DOMNodeInserted', | 1010 $('videosrc').addEventListener('DOMNodeInserted', |
260 updateGetUserMediaConstraints, false); | 1011 updateGetUserMediaConstraints, false); |
261 } | 1012 } else { |
262 else { | 1013 print_('addEventListener is not supported by your browser, cannot update ' + |
263 debug('addEventListener is not supported by your browser, cannot update ' + | 1014 'device source ID\'s automatically. Select a device from the audio' + |
264 'device source ID\'s automatically. Select a device from the audio ' + | 1015 ' or video source drop down menu to update device source id\'s'); |
265 'or video source drop down menu to update device source id\'s'); | 1016 } |
266 } | 1017 } |
267 } | 1018 |
268 | 1019 /** |
269 /** | 1020 * Register an input element to use local storage to remember its state between |
270 * Disconnect before the tab is closed. | 1021 * sessions (using local storage). Only input elements are supported. |
271 */ | 1022 * @private |
272 window.onbeforeunload = function() { | 1023 * @param {!string} element_id to be used as a key for local storage and the id |
273 if (!isDisconnected()) | 1024 * of the element to store the state for. |
274 disconnect(); | 1025 */ |
275 }; | 1026 function registerLocalStorage_(element_id) { |
276 | 1027 var element = $(element_id); |
277 // Internals. | 1028 if (element.tagName != 'INPUT') { |
| 1029 error_('You can only use registerLocalStorage_ for input elements. ' + |
| 1030 'Element \"' + element.tagName + '\" is not an input element. '); |
| 1031 } |
| 1032 |
| 1033 if (localStorage.getItem(element.id) == 'undefined') { |
| 1034 storeLocalStorageField_(element); |
| 1035 } else { |
| 1036 getLocalStorageField_(element); |
| 1037 } |
| 1038 // Registers the appropriate events for input elements. |
| 1039 if (element.type == 'checkbox') { |
| 1040 element.onclick = function() { storeLocalStorageField_(this); }; |
| 1041 } else if (element.type == 'text') { |
| 1042 element.onblur = function() { storeLocalStorageField_(this); }; |
| 1043 } else { |
| 1044 error_('Unsupportered input type: ' + '\"' + element.type + '\"'); |
| 1045 } |
| 1046 } |
| 1047 |
| 1048 /** |
| 1049 * Fetches the stored values from local storage and updates checkbox status. |
| 1050 * @private |
| 1051 * @param {!Object} element of which id is representing the key parameter for |
| 1052 * local storage. |
| 1053 */ |
| 1054 function getLocalStorageField_(element) { |
| 1055 // Makes sure the checkbox status is matching the local storage value. |
| 1056 if (element.type == 'checkbox') { |
| 1057 element.checked = (localStorage.getItem(element.id) == 'true'); |
| 1058 } else if (element.type == 'text') { |
| 1059 element.value = localStorage.getItem(element.id); |
| 1060 } else { |
| 1061 error_('Unsupportered input type: ' + '\"' + element.type + '\"'); |
| 1062 } |
| 1063 } |
| 1064 |
| 1065 /** |
| 1066 * Stores the string value of the element object using local storage. |
| 1067 * @private |
| 1068 * @param {!Object} element of which id is representing the key parameter for |
| 1069 * local storage. |
| 1070 */ |
| 1071 function storeLocalStorageField_(element) { |
| 1072 if (element.type == 'checkbox') { |
| 1073 localStorage.setItem(element.id, element.checked); |
| 1074 } else if (element.type == 'text') { |
| 1075 localStorage.setItem(element.id, element.value); |
| 1076 } |
| 1077 } |
278 | 1078 |
279 /** | 1079 /** |
280 * Create the peer connection if none is up (this is just convenience to | 1080 * Create the peer connection if none is up (this is just convenience to |
281 * avoid having a separate button for that). | 1081 * avoid having a separate button for that). |
282 * @private | 1082 * @private |
283 */ | 1083 */ |
284 function ensureHasPeerConnection_() { | 1084 function ensureHasPeerConnection_() { |
285 if (getReadyState() == 'no-peer-connection') { | 1085 if (getReadyState_() == 'no-peer-connection') { |
286 preparePeerConnection(); | 1086 preparePeerConnection(); |
287 } | 1087 } |
288 } | 1088 } |
289 | 1089 |
290 /** | 1090 /** |
291 * @private | 1091 * @private |
292 * @param {string} message Text to print. | 1092 * @param {string} message Text to print. |
293 */ | 1093 */ |
294 function print_(message) { | 1094 function print_(message) { |
295 // Filter out uninteresting noise. | |
296 if (message == 'ok-no-errors') | |
297 return; | |
298 | |
299 console.log(message); | 1095 console.log(message); |
300 $('messages').innerHTML += message + '<br>'; | 1096 $('messages').innerHTML += message + '<br>'; |
301 } | 1097 } |
302 | 1098 |
303 /** | 1099 /** |
304 * @private | 1100 * @private |
305 * @param {string} message Text to print. | 1101 * @param {string} message Text to print. |
306 */ | 1102 */ |
307 function debug_(message) { | 1103 function debug_(message) { |
308 console.log(message); | 1104 console.log(message); |
309 $('debug').innerHTML += message + '<br>'; | 1105 $('debug').innerHTML += message + '<br>'; |
310 } | 1106 } |
311 | 1107 |
312 /** | 1108 /** |
313 * Print error message in the debug log + JS console and throw an Error. | 1109 * Print error message in the debug log + JS console and throw an Error. |
314 * @private | 1110 * @private |
315 * @param {string} message Text to print. | 1111 * @param {string} message Text to print. |
316 */ | 1112 */ |
317 function error(message) { | 1113 function error_(message) { |
318 $('debug').innerHTML += '<span style="color:red;">' + message + '</span><br>'; | 1114 $('debug').innerHTML += '<span style="color:red;">' + message + '</span><br>'; |
319 throw new Error(message); | 1115 throw new Error(message); |
320 } | 1116 } |
321 | 1117 |
322 /** | 1118 /** |
323 * @private | 1119 * @private |
324 * @param {string} stringRepresentation JavaScript as a string. | 1120 * @param {string} stringRepresentation JavaScript as a string. |
325 * @return {Object} The PeerConnection constraints as a JavaScript dictionary. | 1121 * @return {Object} The PeerConnection constraints as a JavaScript dictionary. |
326 */ | 1122 */ |
327 function getEvaluatedJavaScript_(stringRepresentation) { | 1123 function getEvaluatedJavaScript_(stringRepresentation) { |
328 try { | 1124 try { |
329 var evaluatedJavaScript; | 1125 var evaluatedJavaScript; |
330 eval('evaluatedJavaScript = ' + stringRepresentation); | 1126 eval('evaluatedJavaScript = ' + stringRepresentation); |
331 return evaluatedJavaScript; | 1127 return evaluatedJavaScript; |
332 } catch (exception) { | 1128 } catch (exception) { |
333 error('Not valid JavaScript expression: ' + stringRepresentation); | 1129 error_('Not valid JavaScript expression: ' + stringRepresentation); |
334 } | 1130 } |
335 } | 1131 } |
336 | 1132 |
337 /** | 1133 /** |
338 * Swaps lines within a SDP message. | 1134 * Swaps lines within a SDP message. |
339 * @private | 1135 * @private |
340 * @param {string} sdp The full SDP message. | 1136 * @param {string} sdp The full SDP message. |
341 * @param {string} line The line to swap with swapWith. | 1137 * @param {string} line The line to swap with swapWith. |
342 * @param {string} swapWith The other line. | 1138 * @param {string} swapWith The other line. |
343 * @return {string} The altered SDP message. | 1139 * @return {string} The altered SDP message. |
344 */ | 1140 */ |
345 function swapSdpLines_(sdp, line, swapWith) { | 1141 function swapSdpLines_(sdp, line, swapWith) { |
346 var lines = sdp.split('\r\n'); | 1142 var lines = sdp.split('\r\n'); |
347 var lineStart = lines.indexOf(line); | 1143 var lineStart = lines.indexOf(line); |
348 var swapLineStart = lines.indexOf(swapWith); | 1144 var swapLineStart = lines.indexOf(swapWith); |
349 if (lineStart == -1 || swapLineStart == -1) | 1145 if (lineStart == -1 || swapLineStart == -1) |
350 return sdp; // This generally happens on the first message. | 1146 return sdp; // This generally happens on the first message. |
351 | 1147 |
352 var tmp = lines[lineStart]; | 1148 var tmp = lines[lineStart]; |
353 lines[lineStart] = lines[swapLineStart]; | 1149 lines[lineStart] = lines[swapLineStart]; |
354 lines[swapLineStart] = tmp; | 1150 lines[swapLineStart] = tmp; |
355 | 1151 |
356 return lines.join('\r\n'); | 1152 return lines.join('\r\n'); |
357 } | 1153 } |
358 | 1154 |
359 // TODO(phoglund): Not currently used; delete once it clear we do not need to | |
360 // test opus prioritization. | |
361 /** @private */ | |
362 function preferOpus_() { | |
363 setOutgoingSdpTransform(function(sdp) { | |
364 sdp = sdp.replace('103 104 111', '111 103 104'); | |
365 | |
366 // TODO(phoglund): We need to swap the a= lines too. I don't think this | |
367 // should be needed but it apparently is right now. | |
368 return swapSdpLines_(sdp, | |
369 'a=rtpmap:103 ISAC/16000', | |
370 'a=rtpmap:111 opus/48000'); | |
371 }); | |
372 } | |
373 | |
374 /** @private */ | 1155 /** @private */ |
375 function forceIsac_() { | 1156 function forceIsac_() { |
376 setOutgoingSdpTransform(function(sdp) { | 1157 setOutgoingSdpTransform(function(sdp) { |
377 // Remove all other codecs (not the video codecs though). | 1158 // Remove all other codecs (not the video codecs though). |
378 sdp = sdp.replace(/m=audio (\d+) RTP\/SAVPF.*\r\n/g, | 1159 sdp = sdp.replace(/m=audio (\d+) RTP\/SAVPF.*\r\n/g, |
379 'm=audio $1 RTP/SAVPF 104\r\n'); | 1160 'm=audio $1 RTP/SAVPF 104\r\n'); |
380 sdp = sdp.replace('a=fmtp:111 minptime=10', 'a=fmtp:104 minptime=10'); | 1161 sdp = sdp.replace('a=fmtp:111 minptime=10', 'a=fmtp:104 minptime=10'); |
381 sdp = sdp.replace(/a=rtpmap:(?!104)\d{1,3} (?!VP8|red|ulpfec).*\r\n/g, ''); | 1162 sdp = sdp.replace(/a=rtpmap:(?!104)\d{1,3} (?!VP8|red|ulpfec).*\r\n/g, ''); |
382 return sdp; | 1163 return sdp; |
383 }); | 1164 }); |
384 } | 1165 } |
385 | 1166 |
386 /** @private */ | 1167 /** @private */ |
387 function dontTouchSdp_() { | 1168 function dontTouchSdp_() { |
388 setOutgoingSdpTransform(function(sdp) { return sdp; }); | 1169 setOutgoingSdpTransform(function(sdp) { return sdp; }); |
389 } | 1170 } |
390 | 1171 |
391 /** @private */ | 1172 /** @private */ |
392 function hookupDataChannelCallbacks_() { | 1173 function hookupDataChannelCallbacks_() { |
393 setDataCallbacks(function(status) { | 1174 setDataCallbacks(function(status) { |
394 $('data-channel-status').value = status; | 1175 $('data-channel-status').value = status; |
395 }, | 1176 }, |
396 function(data_message) { | 1177 function(data_message) { |
397 debug('Received ' + data_message.data); | 1178 print_('Received ' + data_message.data); |
398 $('data-channel-receive').value = | 1179 $('data-channel-receive').value = |
399 data_message.data + '\n' + $('data-channel-receive').value; | 1180 data_message.data + '\n' + $('data-channel-receive').value; |
400 }); | 1181 }); |
401 } | 1182 } |
402 | 1183 |
403 /** @private */ | 1184 /** @private */ |
404 function hookupDtmfSenderCallback_() { | 1185 function hookupDtmfSenderCallback_() { |
405 setOnToneChange(function(tone) { | 1186 setOnToneChange(function(tone) { |
406 debug('Sent DTMF tone: ' + tone.tone); | 1187 print_('Sent DTMF tone: ' + tone.tone); |
407 $('dtmf-tones-sent').value = | 1188 $('dtmf-tones-sent').value = |
408 tone.tone + '\n' + $('dtmf-tones-sent').value; | 1189 tone.tone + '\n' + $('dtmf-tones-sent').value; |
409 }); | 1190 }); |
410 } | 1191 } |
411 | 1192 |
412 /** | 1193 /** @private */ |
413 * A list of element id's to be registered for local storage. | 1194 function toggle_(track, localOrRemote, audioOrVideo) { |
414 */ | 1195 if (!track) |
415 function setupLocalStorageFieldValues() { | 1196 error_('Tried to toggle ' + localOrRemote + ' ' + audioOrVideo + |
416 registerLocalStorage_('pc-server'); | 1197 ' stream, but has no such stream.'); |
417 registerLocalStorage_('pc-createanswer-constraints'); | 1198 |
418 registerLocalStorage_('pc-createoffer-constraints'); | 1199 track.enabled = !track.enabled; |
419 registerLocalStorage_('get-devices-onload'); | 1200 print_('ok-' + audioOrVideo + '-toggled-to-' + track.enabled); |
420 } | 1201 } |
421 | 1202 |
422 /** | 1203 /** @private */ |
423 * Register an input element to use local storage to remember its state between | 1204 function connectCallback_(request) { |
424 * sessions (using local storage). Only input elements are supported. | 1205 print_('Connect callback: ' + request.status + ', ' + request.readyState); |
425 * @private | 1206 if (request.status == 0) { |
426 * @param {!string} element_id to be used as a key for local storage and the id | 1207 print_('peerconnection_server doesn\'t seem to be up.'); |
427 * of the element to store the state for. | 1208 print_('failed-to-connect'); |
428 */ | |
429 function registerLocalStorage_(element_id) { | |
430 var element = $(element_id); | |
431 if (element.tagName != 'INPUT') { | |
432 error('You can only use registerLocalStorage_ for input elements. ' + | |
433 'Element \"' + element.tagName +'\" is not an input element. '); | |
434 } | 1209 } |
435 | 1210 if (request.readyState == 4 && request.status == 200) { |
436 if (localStorage.getItem(element.id) == 'undefined') { | 1211 global.ourPeerId = parseOurPeerId_(request.responseText); |
437 storeLocalStorageField_(element); | 1212 global.remotePeerId = parseRemotePeerIdIfConnected_(request.responseText); |
438 } else { | 1213 startHangingGet_(global.serverUrl, global.ourPeerId); |
439 getLocalStorageField_(element); | 1214 print_('ok-connected'); |
440 } | |
441 // Registers the appropriate events for input elements. | |
442 if (element.type == 'checkbox') { | |
443 element.onclick = function() { storeLocalStorageField_(this); }; | |
444 } else if (element.type == 'text') { | |
445 element.onblur = function() { storeLocalStorageField_(this); }; | |
446 } else { | |
447 error('Unsupportered input type: ' + '\"' + element.type + '\"' ); | |
448 } | 1215 } |
449 } | 1216 } |
450 | 1217 |
451 /** | 1218 /** @private */ |
452 * Fetches the stored values from local storage and updates checkbox status. | 1219 function parseOurPeerId_(responseText) { |
453 * @private | 1220 // According to peerconnection_server's protocol. |
454 * @param {!Object} element of which id is representing the key parameter for | 1221 var peerList = responseText.split('\n'); |
455 * local storage. | 1222 return parseInt(peerList[0].split(',')[1]); |
456 */ | 1223 } |
457 function getLocalStorageField_(element) { | 1224 |
458 // Makes sure the checkbox status is matching the local storage value. | 1225 /** @private */ |
459 if (element.type == 'checkbox') { | 1226 function parseRemotePeerIdIfConnected_(responseText) { |
460 element.checked = (localStorage.getItem(element.id) == 'true'); | 1227 var peerList = responseText.split('\n'); |
461 } else if (element.type == 'text' ) { | 1228 if (peerList.length == 1) { |
462 element.value = localStorage.getItem(element.id); | 1229 // No peers have connected yet - we'll get their id later in a notification. |
463 } else { | 1230 return null; |
464 error('Unsupportered input type: ' + '\"' + element.type + '\"' ); | 1231 } |
| 1232 var remotePeerId = null; |
| 1233 for (var i = 0; i < peerList.length; i++) { |
| 1234 if (peerList[i].length == 0) |
| 1235 continue; |
| 1236 |
| 1237 var parsed = peerList[i].split(','); |
| 1238 var name = parsed[0]; |
| 1239 var id = parsed[1]; |
| 1240 |
| 1241 if (id != global.ourPeerId) { |
| 1242 print_('Found remote peer with name ' + name + ', id ' + |
| 1243 id + ' when connecting.'); |
| 1244 |
| 1245 // There should be at most one remote peer in this test. |
| 1246 if (remotePeerId != null) |
| 1247 error_('Expected just one remote peer in this test: ' + |
| 1248 'found several.'); |
| 1249 |
| 1250 // Found a remote peer. |
| 1251 remotePeerId = id; |
| 1252 } |
| 1253 } |
| 1254 return remotePeerId; |
| 1255 } |
| 1256 |
| 1257 /** @private */ |
| 1258 function startHangingGet_(server, ourId) { |
| 1259 if (isDisconnected_()) |
| 1260 return; |
| 1261 |
| 1262 hangingGetRequest = new XMLHttpRequest(); |
| 1263 hangingGetRequest.onreadystatechange = function() { |
| 1264 hangingGetCallback_(hangingGetRequest, server, ourId); |
| 1265 }; |
| 1266 hangingGetRequest.ontimeout = function() { |
| 1267 hangingGetTimeoutCallback_(hangingGetRequest, server, ourId); |
| 1268 }; |
| 1269 callUrl = server + '/wait?peer_id=' + ourId; |
| 1270 print_('Sending ' + callUrl); |
| 1271 hangingGetRequest.open('GET', callUrl, true); |
| 1272 hangingGetRequest.send(); |
| 1273 } |
| 1274 |
| 1275 /** @private */ |
| 1276 function hangingGetCallback_(hangingGetRequest, server, ourId) { |
| 1277 if (hangingGetRequest.readyState != 4 || hangingGetRequest.status == 0) { |
| 1278 // Code 0 is not possible if the server actually responded. Ignore. |
| 1279 return; |
| 1280 } |
| 1281 if (hangingGetRequest.status != 200) { |
| 1282 error_('Error ' + hangingGetRequest.status + ' from server: ' + |
| 1283 hangingGetRequest.statusText); |
| 1284 } |
| 1285 var targetId = readResponseHeader_(hangingGetRequest, 'Pragma'); |
| 1286 if (targetId == ourId) |
| 1287 handleServerNotification_(hangingGetRequest.responseText); |
| 1288 else |
| 1289 handlePeerMessage_(targetId, hangingGetRequest.responseText); |
| 1290 |
| 1291 hangingGetRequest.abort(); |
| 1292 restartHangingGet_(server, ourId); |
| 1293 } |
| 1294 |
| 1295 /** @private */ |
| 1296 function hangingGetTimeoutCallback_(hangingGetRequest, server, ourId) { |
| 1297 print_('Hanging GET times out, re-issuing...'); |
| 1298 hangingGetRequest.abort(); |
| 1299 restartHangingGet_(server, ourId); |
| 1300 } |
| 1301 |
| 1302 /** @private */ |
| 1303 function handleServerNotification_(message) { |
| 1304 var parsed = message.split(','); |
| 1305 if (parseInt(parsed[2]) == 1) { |
| 1306 // Peer connected - this must be our remote peer, and it must mean we |
| 1307 // connected before them (except if we happened to connect to the server |
| 1308 // at precisely the same moment). |
| 1309 print_('Found remote peer with name ' + parsed[0] + ', id ' + parsed[1] + |
| 1310 ' when connecting.'); |
| 1311 global.remotePeerId = parseInt(parsed[1]); |
465 } | 1312 } |
466 } | 1313 } |
467 | 1314 |
468 /** | 1315 /** @private */ |
469 * Stores the string value of the element object using local storage. | 1316 function closeCall_() { |
470 * @private | 1317 if (global.peerConnection == null) |
471 * @param {!Object} element of which id is representing the key parameter for | 1318 debug_('Closing call, but no call active.'); |
472 * local storage. | 1319 global.peerConnection.close(); |
473 */ | 1320 global.peerConnection = null; |
474 function storeLocalStorageField_(element) { | 1321 } |
475 if (element.type == 'checkbox') { | 1322 |
476 localStorage.setItem(element.id, element.checked); | 1323 /** @private */ |
477 } else if (element.type == 'text') { | 1324 function handlePeerMessage_(peerId, message) { |
478 localStorage.setItem(element.id, element.value); | 1325 print_('Received message from peer ' + peerId + ': ' + message); |
| 1326 if (peerId != global.remotePeerId) { |
| 1327 error_('Received notification from unknown peer ' + peerId + |
| 1328 ' (only know about ' + global.remotePeerId + '.'); |
479 } | 1329 } |
| 1330 if (message.search('BYE') == 0) { |
| 1331 print_('Received BYE from peer: closing call'); |
| 1332 closeCall_(); |
| 1333 return; |
| 1334 } |
| 1335 if (global.peerConnection == null && global.acceptsIncomingCalls) { |
| 1336 // The other side is calling us. |
| 1337 print_('We are being called: answer...'); |
| 1338 |
| 1339 global.peerConnection = createPeerConnection(STUN_SERVER, |
| 1340 global.useRtpDataChannels); |
| 1341 if ($('auto-add-stream-oncall') && |
| 1342 obtainGetUserMediaResult_() == 'ok-got-stream') { |
| 1343 print_('We have a local stream, so hook it up automatically.'); |
| 1344 addLocalStreamToPeerConnection(global.peerConnection); |
| 1345 } |
| 1346 answerCall(global.peerConnection, message); |
| 1347 return; |
| 1348 } |
| 1349 handleMessage(global.peerConnection, message); |
480 } | 1350 } |
| 1351 |
| 1352 /** @private */ |
| 1353 function restartHangingGet_(server, ourId) { |
| 1354 window.setTimeout(function() { |
| 1355 startHangingGet_(server, ourId); |
| 1356 }, 0); |
| 1357 } |
| 1358 |
| 1359 /** @private */ |
| 1360 function readResponseHeader_(request, key) { |
| 1361 var value = request.getResponseHeader(key); |
| 1362 if (value == null || value.length == 0) { |
| 1363 error_('Received empty value ' + value + |
| 1364 ' for response header key ' + key + '.'); |
| 1365 } |
| 1366 return parseInt(value); |
| 1367 } |
OLD | NEW |