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