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.
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.
14 /** TODO(jansson) give it a better name
15 * Global namespace object.
20 * We need a STUN server for some API calls.
23 var STUN_SERVER = 'stun.l.google.com:19302';
26 global.transformOutgoingSdp = function(sdp) { return sdp; };
29 global.dataStatusCallback = function(status) {};
32 global.dataCallback = function(data) {};
35 global.dtmfOnToneChange = function(tone) {};
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
44 return document.getElementById(id);
48 * Prepopulate constraints from JS to the UI. Enumerate devices available
49 * via getUserMedia, register elements to be used for local storage.
51 window.onload = function() {
52 hookupDataChannelCallbacks_();
53 hookupDtmfSenderCallback_();
54 updateGetUserMediaConstraints();
55 setupLocalStorageFieldValues();
56 acceptIncomingCalls();
57 setPeerConnectionConstraints();
58 if ($('get-devices-onload').checked == true) {
64 * Disconnect before the tab is closed.
66 window.onbeforeunload = function() {
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.
74 function setupLocalStorageFieldValues() {
75 registerLocalStorage_('pc-server');
76 registerLocalStorage_('pc-createanswer-constraints');
77 registerLocalStorage_('pc-createoffer-constraints');
78 registerLocalStorage_('get-devices-onload');
81 // Public HTML functions
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).
87 function getUserMediaFromHere() {
88 var constraints = $('getusermedia-constraints').value;
90 doGetUserMedia_(constraints);
92 print_('getUserMedia says: ' + exception);
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);
103 connect(server, $('peer-id').value);
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));
113 ensureHasPeerConnection_();
117 function addLocalStreamFromHere() {
118 ensureHasPeerConnection_();
122 function removeLocalStreamFromHere() {
126 function hangUpFromHere() {
128 acceptIncomingCalls();
131 function toggleRemoteVideoFromHere() {
132 toggleRemoteStream(function(remoteStream) {
133 return remoteStream.getVideoTracks()[0];
137 function toggleRemoteAudioFromHere() {
138 toggleRemoteStream(function(remoteStream) {
139 return remoteStream.getAudioTracks()[0];
143 function toggleLocalVideoFromHere() {
144 toggleLocalStream(function(localStream) {
145 return localStream.getVideoTracks()[0];
149 function toggleLocalAudioFromHere() {
150 toggleLocalStream(function(localStream) {
151 return localStream.getAudioTracks()[0];
155 function stopLocalFromHere() {
159 function createDataChannelFromHere() {
160 ensureHasPeerConnection_();
161 createDataChannelOnPeerConnection();
164 function closeDataChannelFromHere() {
165 ensureHasPeerConnection_();
166 closeDataChannelOnPeerConnection();
169 function sendDataFromHere() {
170 var data = $('data-channel-send').value;
171 sendDataOnChannel(data);
174 function createDtmfSenderFromHere() {
175 ensureHasPeerConnection_();
176 createDtmfSenderOnPeerConnection();
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);
186 function forceIsacChanged() {
187 var forceIsac = $('force-isac').checked;
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.
202 function updateGetUserMediaConstraints() {
203 var selectedAudioDevice = $('audiosrc');
204 var selectedVideoDevice = $('videosrc');
205 var constraints = {audio: $('audio').checked,
206 video: $('video').checked
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}]};
216 if (!selectedAudioDevice.disabled && !selectedAudioDevice.disabled) {
217 var devices = getSourcesFromField_(selectedAudioDevice,
218 selectedVideoDevice);
220 if ($('audio').checked) {
221 if (devices.audioId != null)
222 constraints.audio = {optional: [{sourceId: devices.audioId}]};
225 if ($('video').checked) {
226 if (devices.videoId != null)
227 constraints.video.optional.push({sourceId: devices.videoId});
231 if ($('screencapture').checked) {
233 audio: $('audio').checked,
234 video: {mandatory: {chromeMediaSource: 'screen',
235 maxWidth: screen.width,
236 maxHeight: screen.height}}
238 if ($('audio').checked)
239 warning_('Audio for screencapture is not implemented yet, please ' +
240 'try to set audio = false prior requesting screencapture');
243 $('getusermedia-constraints').value = JSON.stringify(constraints, null, ' ');
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.');
254 function clearLog() {
255 $('messages').innerHTML = '';
256 $('debug').innerHTML = '';
260 * Stops the local stream.
262 function stopLocalStream() {
263 if (global.localStream == null)
264 error_('Tried to stop local stream, ' +
265 'but media access is not granted.');
267 global.localStream.stop();
271 * Adds the current local media stream to a peer connection.
272 * @param {RTCPeerConnection} peerConnection
274 function addLocalStreamToPeerConnection(peerConnection) {
275 if (global.localStream == null)
276 error_('Tried to add local stream to peer connection, but there is no ' +
279 peerConnection.addStream(global.localStream, global.addStreamConstraints);
280 } catch (exception) {
281 error_('Failed to add stream with constraints ' +
282 global.addStreamConstraints + ': ' + exception);
284 print_('Added local stream.');
288 * Removes the local stream from the peer connection.
289 * @param {rtcpeerconnection} peerConnection
291 function removeLocalStreamFromPeerConnection(peerConnection) {
292 if (global.localStream == null)
293 error_('Tried to remove local stream from peer connection, but there is ' +
296 peerConnection.removeStream(global.localStream);
297 } catch (exception) {
298 error_('Could not remove stream: ' + exception);
300 print_('Removed local stream.');
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.
312 function getDevices() {
313 selectedAudioDevice = $('audiosrc');
314 selectedVideoDevice = $('videosrc');
315 selectedAudioDevice.innerHTML = '';
316 selectedVideoDevice.innerHTML = '';
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);
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;
335 if (devices[i].kind == 'audio') {
336 if (option.text == '') {
337 option.text = devices[i].id;
339 selectedAudioDevice.appendChild(option);
340 } else if (devices[i].kind == 'video') {
341 if (option.text == '') {
342 option.text = devices[i].id;
344 selectedVideoDevice.appendChild(option);
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();
355 checkIfDeviceDropdownsArePopulated_();
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.
364 function setOutgoingSdpTransform(transformFunction) {
365 global.transformOutgoingSdp = transformFunction;
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.
373 function setCreateAnswerConstraints(mediaConstraints) {
374 global.createAnswerConstraints = mediaConstraints;
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.
382 function setCreateOfferConstraints(mediaConstraints) {
383 global.createOfferConstraints = mediaConstraints;
387 * Sets the callback functions that will receive DataChannel readyState updates
389 * @param {function} status_callback The function that will receive a string
391 * the current DataChannel readyState.
392 * @param {function} data_callback The function that will a string with data
393 * received from the remote peer.
395 function setDataCallbacks(status_callback, data_callback) {
396 global.dataStatusCallback = status_callback;
397 global.dataCallback = data_callback;
401 * Sends data on an active DataChannel.
402 * @param {string} data The string that will be sent to the remote peer.
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);
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.
415 function setOnToneChange(ontonechange) {
416 global.dtmfOnToneChange = ontonechange;
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.
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);
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(
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);
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); }
456 error_('unknown message received');
460 * Sets the peerConnection constraints based on checkboxes.
461 * TODO (jansson) Make it possible to use the text field for constraints like
464 function setPeerConnectionConstraints() {
465 // Only added optional for now.
466 global.pcConstraints = {
470 global.pcConstraints.optional.push(
471 {googCpuOveruseDetection: $('cpuoveruse-detection').checked});
473 global.pcConstraints.optional.push(
474 {RtpDataChannels: $('data-channel-type-rtp').checked});
476 $('pc-constraints').value = JSON.stringify(global.pcConstraints, null, ' ');
479 function createPeerConnection(stun_server) {
480 servers = {iceServers: [{url: 'stun:' + stun_server}]};
482 peerConnection = new RTCPeerConnection(servers, global.pcConstraints);
483 } catch (exception) {
484 error_('Failed to create peer connection: ' + exception);
486 peerConnection.onaddstream = addStreamCallback_;
487 peerConnection.onremovestream = removeStreamCallback_;
488 peerConnection.onicecandidate = iceCallback_;
489 peerConnection.ondatachannel = onCreateDataChannelCallback_;
490 return peerConnection;
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);
502 function answerCall(peerConnection, message) {
503 handleMessage(peerConnection, message);
506 function createDataChannel(peerConnection, label) {
507 if (global.dataChannel != null && global.dataChannel.readyState != 'closed')
508 error_('Creating DataChannel, but we already have one.');
510 global.dataChannel = peerConnection.createDataChannel(label,
511 { reliable: false });
512 print_('DataChannel with label ' + global.dataChannel.label + ' initiated ' +
514 hookupDataChannelEvents();
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();
525 function createDtmfSender(peerConnection) {
526 if (global.dtmfSender != null)
527 error_('Creating DTMF sender, but we already have one.');
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;
538 * Connects to the provided peerconnection_server.
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.
544 function connect(serverUrl, clientName) {
545 if (global.ourPeerId != null)
546 error_('connecting, but is already connected.');
548 print_('Connecting to ' + serverUrl + ' as ' + clientName);
549 global.serverUrl = serverUrl;
550 global.ourClientName = clientName;
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);
562 * Checks if the remote peer has connected. Returns peer-connected if that is
563 * the case, otherwise no-peer-connected.
565 function remotePeerIsConnected() {
566 if (global.remotePeerId == null)
567 print_('no-peer-connected');
569 print_('peer-connected');
573 * Creates a peer connection. Must be called before most other public functions
576 function preparePeerConnection() {
577 if (global.peerConnection != null)
578 error_('creating peer connection, but we already have one.');
580 global.peerConnection = createPeerConnection(STUN_SERVER);
581 success_('ok-peerconnection-created');
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.
588 function addLocalStream() {
589 if (global.peerConnection == null)
590 error_('adding local stream, but we have no peer connection.');
592 addLocalStreamToPeerConnection(global.peerConnection);
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.
600 function removeLocalStream() {
601 if (global.peerConnection == null)
602 error_('attempting to remove local stream, but no call is up');
604 removeLocalStreamFromPeerConnection(global.peerConnection);
605 print_('ok-local-stream-removed');
609 * (see getReadyState_)
611 function getPeerConnectionReadyState() {
612 print_(getReadyState_());
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]
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
623 * @param {function} typeToToggle Either "audio" or "video" depending on what
624 * the selector function selects.
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.');
632 var track = selectAudioOrVideoTrack(
633 global.peerConnection.getRemoteStreams()[0]);
634 toggle_(track, 'remote', typeToToggle);
638 * See documentation on toggleRemoteStream (this function is the same except
639 * we are looking at local streams).
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 ' +
648 var track = selectAudioOrVideoTrack(
649 global.peerConnection.getLocalStreams()[0]);
650 toggle_(track, 'local', typeToToggle);
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.
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');
664 global.acceptsIncomingCalls = false;
665 print_('ok-call-hung-up');
669 * Start accepting incoming calls.
671 function acceptIncomingCalls() {
672 global.acceptsIncomingCalls = true;
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.
680 function createDataChannelOnPeerConnection() {
681 if (global.peerConnection == null)
682 error_('Tried to create data channel, but have no peer connection.');
684 createDataChannel(global.peerConnection, global.ourClientName);
685 print_('ok-datachannel-created');
689 * Close the DataChannel on the current PeerConnection.
690 * Returns ok-datachannel-close on success.
692 function closeDataChannelOnPeerConnection() {
693 if (global.peerConnection == null)
694 error_('Tried to close data channel, but have no peer connection.');
696 closeDataChannel(global.peerConnection);
697 print_('ok-datachannel-close');
701 * Creates a DTMF sender on the current PeerConnection.
702 * Returns ok-dtmfsender-created on success.
704 function createDtmfSenderOnPeerConnection() {
705 if (global.peerConnection == null)
706 error_('Tried to create DTMF sender, but have no peer connection.');
708 createDtmfSender(global.peerConnection);
709 print_('ok-dtmfsender-created');
713 * Send DTMF tones on the global.dtmfSender.
714 * Returns ok-dtmf-sent on success.
716 function insertDtmfOnSender(tones, duration, interToneGap) {
717 if (global.dtmfSender == null)
718 error_('Tried to insert DTMF tones, but have no DTMF sender.');
720 insertDtmf(tones, duration, interToneGap);
721 print_('ok-dtmf-sent');
725 * Sends a message to a peer through the peerconnection_server.
727 function sendToPeer(peer, message) {
728 var messageToLog = message.sdp ? message.sdp : message;
729 print_('Sending message ' + messageToLog + ' to peer ' + peer + '.');
731 var request = new XMLHttpRequest();
732 var url = global.serverUrl + '/message?peer_id=' + global.ourPeerId + '&to=' +
734 request.open('POST', url, false);
735 request.setRequestHeader('Content-Type', 'text/plain');
736 request.send(message);
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.
746 function updateVideoTagSize(videoTagId, width, height) {
747 var videoTag = $(videoTagId);
748 if (width > 0 || height > 0) {
749 videoTag.width = width;
750 videoTag.height = height;
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);
760 print_('"' + videoTagId + '" video stream size is 0, skipping resize');
763 displayVideoSize_(videoTag);
769 * Disconnects from the peerconnection server. Returns ok-disconnected on
772 function disconnect_() {
773 if (global.ourPeerId == null)
776 request = new XMLHttpRequest();
777 request.open('GET', global.serverUrl + '/sign_out?peer_id=' +
778 global.ourPeerId, false);
780 global.ourPeerId = null;
781 print_('ok-disconnected');
785 * Returns true if we are disconnected from peerconnection_server.
787 function isDisconnected_() {
788 return global.ourPeerId == null;
793 * @return {!string} The current peer connection's ready state, or
794 * 'no-peer-connection' if there is no peer connection up.
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'.
800 function getReadyState_() {
801 if (global.peerConnection == null)
802 return 'no-peer-connection';
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_().
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().
818 function doGetUserMedia_(constraints) {
820 print_('Browser does not support WebRTC.');
824 var evaluatedConstraints;
825 eval('evaluatedConstraints = ' + constraints);
826 } catch (exception) {
827 error_('Not valid JavaScript expression: ' + constraints);
829 print_('Requesting doGetUserMedia: constraints: ' + constraints);
830 getUserMedia(evaluatedConstraints, getUserMediaOkCallback_,
831 getUserMediaFailedCallback_);
835 * Must be called after calling doGetUserMedia.
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.
842 function obtainGetUserMediaResult_() {
843 if (global.requestWebcamAndMicrophoneResult == null)
844 global.requestWebcamAndMicrophoneResult = ' not called yet';
846 return global.requestWebcamAndMicrophoneResult;
851 * Negotiates a call with the other side. This will create a peer connection on
852 * the other side if there isn't one.
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
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
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.');
872 setupCall(global.peerConnection);
873 print_('ok-negotiating');
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.
888 function getSourcesFromField_(audioSelect, videoSelect) {
893 if (audioSelect.options.length > 0) {
894 source.audioId = audioSelect.options[audioSelect.selectedIndex].value;
896 if (videoSelect.options.length > 0) {
897 source.videoId = videoSelect.options[videoSelect.selectedIndex].value;
904 * @param {NavigatorUserMediaError} error Error containing details.
906 function getUserMediaFailedCallback_(error) {
907 error_('GetUserMedia failed with error: ' + error.name);
911 function iceCallback_(event) {
913 sendToPeer(global.remotePeerId, JSON.stringify(event.candidate));
917 function setLocalAndSendMessage_(session_description) {
918 session_description.sdp =
919 global.transformOutgoingSdp(session_description.sdp);
920 global.peerConnection.setLocalDescription(
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));
929 function addStreamCallback_(event) {
930 print_('Receiving remote stream...');
931 var videoTag = document.getElementById('remote-view');
932 attachMediaStream(videoTag, event.stream);
934 window.addEventListener('loadedmetadata', function() {
935 displayVideoSize_(videoTag);}, true);
939 function removeStreamCallback_(event) {
940 print_('Call ended.');
941 document.getElementById('remote-view').src = '';
945 function onCreateDataChannelCallback_(event) {
946 if (global.dataChannel != null && global.dataChannel.readyState != 'closed') {
947 error_('Received DataChannel, but we already have one.');
950 global.dataChannel = event.channel;
951 print_('DataChannel with label ' + global.dataChannel.label +
952 ' initiated by remote peer.');
953 hookupDataChannelEvents();
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_();
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 = '';
981 * @param {MediaStream} stream Media stream.
983 function getUserMediaOkCallback_(stream) {
984 global.localStream = stream;
985 global.requestWebcamAndMicrophoneResult = 'ok-got-stream';
986 success_('getUserMedia');
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);
993 window.addEventListener('loadedmetadata', function() {
994 displayVideoSize_(videoTag);}, true);
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.');
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.');
1007 stream.getVideoTracks()[0].onunmute = function() {
1008 warning_(global.localStream + ' MediaStreamTrack.onunmute event has ' +
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.
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' +
1029 print_('Skipping updating -stream-size and -size tags due to div\'s ' +
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.
1041 function checkIfDeviceDropdownsArePopulated_() {
1042 if (document.addEventListener) {
1043 $('audiosrc').addEventListener('DOMNodeInserted',
1044 updateGetUserMediaConstraints, false);
1045 $('videosrc').addEventListener('DOMNodeInserted',
1046 updateGetUserMediaConstraints, false);
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');
1055 * Register an input element to use local storage to remember its state between
1056 * sessions (using local storage). Only input elements are supported.
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.
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. ');
1068 if (localStorage.getItem(element.id) == null) {
1069 storeLocalStorageField_(element);
1071 getLocalStorageField_(element);
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); };
1080 error_('Unsupportered input type: ' + '\"' + element.type + '\"');
1085 * Fetches the stored values from local storage and updates checkbox status.
1087 * @param {!Object} element of which id is representing the key parameter for
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);
1097 error_('Unsupportered input type: ' + '\"' + element.type + '\"');
1102 * Stores the string value of the element object using local storage.
1104 * @param {!Object} element of which id is representing the key parameter for
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);
1116 * Create the peer connection if none is up (this is just convenience to
1117 * avoid having a separate button for that).
1120 function ensureHasPeerConnection_() {
1121 if (getReadyState_() == 'no-peer-connection') {
1122 preparePeerConnection();
1128 * @param {string} message Text to print.
1130 function print_(message) {
1131 print_handler_(message, 'messages', 'black');
1136 * @param {string} message Text to print.
1138 function success_(message) {
1139 print_handler_(message, 'messages', 'green');
1144 * @param {string} message Text to print.
1146 function warning_(message) {
1147 print_handler_(message, 'debug', 'orange');
1152 * @param {string} message Text to print.
1154 function error_(message) {
1155 print_handler_(message, 'debug', 'red');
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.
1164 function print_handler_(message, textField, color) {
1165 if (color == 'green' )
1166 message += ' success';
1168 $(textField).innerHTML += '<span style="color:' + color + ';">' + message +
1170 console.log(message);
1172 if (color == 'red' )
1173 throw new Error(message);
1178 * @param {string} stringRepresentation JavaScript as a string.
1179 * @return {Object} The PeerConnection constraints as a JavaScript dictionary.
1181 function getEvaluatedJavaScript_(stringRepresentation) {
1183 var evaluatedJavaScript;
1184 eval('evaluatedJavaScript = ' + stringRepresentation);
1185 return evaluatedJavaScript;
1186 } catch (exception) {
1187 error_('Not valid JavaScript expression: ' + stringRepresentation);
1192 * Swaps lines within a SDP message.
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.
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.
1206 var tmp = lines[lineStart];
1207 lines[lineStart] = lines[swapLineStart];
1208 lines[swapLineStart] = tmp;
1210 return lines.join('\r\n');
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, '');
1226 function dontTouchSdp_() {
1227 setOutgoingSdpTransform(function(sdp) { return sdp; });
1231 function hookupDataChannelCallbacks_() {
1232 setDataCallbacks(function(status) {
1233 $('data-channel-status').value = status;
1235 function(data_message) {
1236 print_('Received ' + data_message.data);
1237 $('data-channel-receive').value =
1238 data_message.data + '\n' + $('data-channel-receive').value;
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;
1252 function toggle_(track, localOrRemote, audioOrVideo) {
1254 error_('Tried to toggle ' + localOrRemote + ' ' + audioOrVideo +
1255 ' stream, but has no such stream.');
1257 track.enabled = !track.enabled;
1258 print_('ok-' + audioOrVideo + '-toggled-to-' + track.enabled);
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');
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');
1277 function parseOurPeerId_(responseText) {
1278 // According to peerconnection_server's protocol.
1279 var peerList = responseText.split('\n');
1280 return parseInt(peerList[0].split(',')[1]);
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.
1290 var remotePeerId = null;
1291 for (var i = 0; i < peerList.length; i++) {
1292 if (peerList[i].length == 0)
1295 var parsed = peerList[i].split(',');
1296 var name = parsed[0];
1299 if (id != global.ourPeerId) {
1300 print_('Found remote peer with name ' + name + ', id ' +
1301 id + ' when connecting.');
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: ' +
1308 // Found a remote peer.
1312 return remotePeerId;
1316 function startHangingGet_(server, ourId) {
1317 if (isDisconnected_())
1320 hangingGetRequest = new XMLHttpRequest();
1321 hangingGetRequest.onreadystatechange = function() {
1322 hangingGetCallback_(hangingGetRequest, server, ourId);
1324 hangingGetRequest.ontimeout = function() {
1325 hangingGetTimeoutCallback_(hangingGetRequest, server, ourId);
1327 callUrl = server + '/wait?peer_id=' + ourId;
1328 print_('Sending ' + callUrl);
1329 hangingGetRequest.open('GET', callUrl, true);
1330 hangingGetRequest.send();
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.
1339 if (hangingGetRequest.status != 200) {
1340 error_('Error ' + hangingGetRequest.status + ' from server: ' +
1341 hangingGetRequest.statusText);
1343 var targetId = readResponseHeader_(hangingGetRequest, 'Pragma');
1344 if (targetId == ourId)
1345 handleServerNotification_(hangingGetRequest.responseText);
1347 handlePeerMessage_(targetId, hangingGetRequest.responseText);
1349 hangingGetRequest.abort();
1350 restartHangingGet_(server, ourId);
1354 function hangingGetTimeoutCallback_(hangingGetRequest, server, ourId) {
1355 print_('Hanging GET times out, re-issuing...');
1356 hangingGetRequest.abort();
1357 restartHangingGet_(server, ourId);
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]);
1374 function closeCall_() {
1375 if (global.peerConnection == null)
1376 debug_('Closing call, but no call active.');
1377 global.peerConnection.close();
1378 global.peerConnection = null;
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 + '.');
1388 if (message.search('BYE') == 0) {
1389 print_('Received BYE from peer: closing call');
1393 if (global.peerConnection == null && global.acceptsIncomingCalls) {
1394 // The other side is calling us.
1395 print_('We are being called: answer...');
1397 global.peerConnection = createPeerConnection(STUN_SERVER);
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);
1404 answerCall(global.peerConnection, message);
1407 handleMessage(global.peerConnection, message);
1411 function restartHangingGet_(server, ourId) {
1412 window.setTimeout(function() {
1413 startHangingGet_(server, ourId);
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 + '.');
1424 return parseInt(value);