Upstream version 11.39.266.0
[platform/framework/web/crosswalk.git] / src / chrome / test / data / webrtc / manual / peerconnection_manual.js
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 }