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