2 * Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 * Use of this source code is governed by a BSD-style license that can be
4 * found in the LICENSE file.
7 // This file requires these functions to be defined globally by someone else:
8 // function handleMessage(peerConnection, message)
9 // function createPeerConnection(stun_server, useRtpDataChannel)
10 // function setupCall(peerConnection)
11 // function answerCall(peerConnection, message)
13 // Currently these functions are supplied by jsep01_call.js.
16 * This object represents the call.
19 var gPeerConnection
= null;
22 * True if we are accepting incoming calls.
25 var gAcceptsIncomingCalls
= true;
28 * Our peer id as assigned by the peerconnection_server.
31 var gOurPeerId
= null;
34 * The client id we use to identify to peerconnection_server.
37 var gOurClientName
= null;
40 * The URL to the peerconnection_server.
43 var gServerUrl
= null;
46 * The remote peer's id. We receive this one either when we connect (in the case
47 * our peer connects before us) or in a notification later.
50 var gRemotePeerId
= null;
53 * Whether or not to auto-respond by adding our local stream when we are called.
56 var gAutoAddLocalToPeerConnectionStreamWhenCalled
= true;
59 * The one and only data channel.
62 var gDataChannel
= null;
68 var gDtmfSender
= null;
71 * We need a STUN server for some API calls.
74 var STUN_SERVER
= 'stun.l.google.com:19302';
77 * If true, any created peer connection will use RTP data
78 * channels. Otherwise it will use SCTP data channels.
80 var gUseRtpDataChannels
= true;
82 // Public interface to tests.
86 * Connects to the provided peerconnection_server.
88 * @param{string} serverUrl The server URL in string form without an ending
89 * slash, something like http://localhost:8888.
90 * @param{string} clientName The name to use when connecting to the server.
92 function connect(serverUrl
, clientName
) {
93 if (gOurPeerId
!= null)
94 throw failTest('connecting, but is already connected.');
96 debug('Connecting to ' + serverUrl
+ ' as ' + clientName
);
97 gServerUrl
= serverUrl
;
98 gOurClientName
= clientName
;
100 request
= new XMLHttpRequest();
101 request
.open('GET', serverUrl
+ '/sign_in?' + clientName
, true);
102 debug(serverUrl
+ '/sign_in?' + clientName
);
103 request
.onreadystatechange = function() {
104 connectCallback_(request
);
110 * Checks if the remote peer has connected. Returns peer-connected if that is
111 * the case, otherwise no-peer-connected.
113 function remotePeerIsConnected() {
114 if (gRemotePeerId
== null)
115 returnToTest('no-peer-connected');
117 returnToTest('peer-connected');
121 * Set if RTP data channels should be used for peerconnections.
122 * @param{boolean} useRtpDataChannel
124 function useRtpDataChannelsForNewPeerConnections(useRtpDataChannels
) {
125 gUseRtpDataChannels
= useRtpDataChannels
;
129 * Creates a peer connection. Must be called before most other public functions
132 function preparePeerConnection() {
133 if (gPeerConnection
!= null)
134 throw failTest('creating peer connection, but we already have one.');
136 gPeerConnection
= createPeerConnection(STUN_SERVER
, gUseRtpDataChannels
);
137 returnToTest('ok-peerconnection-created');
141 * Negotiates a call with the other side. This will create a peer connection on
142 * the other side if there isn't one. The other side will automatically add any
143 * stream it has unless doNotAutoAddLocalStreamWhenCalled() has been called.
145 * To call this method we need to be aware of the other side, e.g. we must be
146 * connected to peerconnection_server and we must have exactly one peer on that
149 * This method may be called any number of times. If you haven't added any
150 * streams to the call, an "empty" call will result. The method will return
151 * ok-negotiating immediately to the test if the negotiation was successfully
154 function negotiateCall() {
155 if (gPeerConnection
== null)
156 throw failTest('negotiating call, but we have no peer connection.');
157 if (gOurPeerId
== null)
158 throw failTest('negotiating call, but not connected.');
159 if (gRemotePeerId
== null)
160 throw failTest('negotiating call, but missing remote peer.');
162 setupCall(gPeerConnection
);
163 returnToTest('ok-negotiating');
167 * Adds the local stream to the peer connection. You will have to re-negotiate
168 * the call for this to take effect in the call.
170 function addLocalStream() {
171 if (gPeerConnection
== null)
172 throw failTest('adding local stream, but we have no peer connection.');
174 addLocalStreamToPeerConnection(gPeerConnection
);
175 returnToTest('ok-added');
179 * Loads a file with WebAudio and connects it to the peer connection.
181 * The loadAudioAndAddToPeerConnection will return ok-added to the test when
182 * the sound is loaded and added to the peer connection. The sound will start
183 * playing when you call playAudioFile.
185 * @param url URL pointing to the file to play. You can assume that you can
186 * serve files from the repository's file system. For instance, to serve a
187 * file from chrome/test/data/pyauto_private/webrtc/file.wav, pass in a path
188 * relative to this directory (e.g. ../pyauto_private/webrtc/file.wav).
190 function addAudioFile(url
) {
191 if (gPeerConnection
== null)
192 throw failTest('adding audio file, but we have no peer connection.');
194 loadAudioAndAddToPeerConnection(url
, gPeerConnection
);
198 * Mixes the local audio stream with an audio file through WebAudio.
200 * You must have successfully requested access to the user's microphone through
201 * getUserMedia before calling this function (see getUserMedia.js).
202 * Additionally, you must have loaded an audio file to mix with.
204 * When playAudioFile is called, WebAudio will effectively mix the user's
205 * microphone input with the previously loaded file and feed that into the
208 function mixLocalStreamWithPreviouslyLoadedAudioFile() {
209 if (gPeerConnection
== null)
210 throw failTest('trying to mix in stream, but we have no peer connection.');
211 if (getLocalStream() == null)
212 throw failTest('trying to mix in stream, but we have no stream to mix in.');
214 mixLocalStreamIntoPeerConnection(gPeerConnection
, getLocalStream());
218 * Must be called after addAudioFile.
220 function playAudioFile() {
221 if (gPeerConnection
== null)
222 throw failTest('trying to play file, but we have no peer connection.');
224 playPreviouslyLoadedAudioFile(gPeerConnection
);
225 returnToTest('ok-playing');
229 * Removes the local stream from the peer connection. You will have to
230 * re-negotiate the call for this to take effect in the call.
232 function removeLocalStream() {
233 if (gPeerConnection
== null)
234 throw failTest('attempting to remove local stream, but no call is up');
236 removeLocalStreamFromPeerConnection(gPeerConnection
);
237 returnToTest('ok-local-stream-removed');
241 * (see getReadyState)
243 function getPeerConnectionReadyState() {
244 returnToTest(getReadyState());
248 * Toggles the remote audio stream's enabled state on the peer connection, given
249 * that a call is active. Returns ok-[typeToToggle]-toggled-to-[true/false]
252 * @param selectAudioOrVideoTrack: A function that takes a remote stream as
253 * argument and returns a track (e.g. either the video or audio track).
254 * @param typeToToggle: Either "audio" or "video" depending on what the selector
257 function toggleRemoteStream(selectAudioOrVideoTrack
, typeToToggle
) {
258 if (gPeerConnection
== null)
259 throw failTest('Tried to toggle remote stream, ' +
260 'but have no peer connection.');
261 if (gPeerConnection
.getRemoteStreams().length
== 0)
262 throw failTest('Tried to toggle remote stream, ' +
263 'but not receiving any stream.');
265 var track
= selectAudioOrVideoTrack(gPeerConnection
.getRemoteStreams()[0]);
266 toggle_(track
, 'remote', typeToToggle
);
270 * See documentation on toggleRemoteStream (this function is the same except
271 * we are looking at local streams).
273 function toggleLocalStream(selectAudioOrVideoTrack
, typeToToggle
) {
274 if (gPeerConnection
== null)
275 throw failTest('Tried to toggle local stream, ' +
276 'but have no peer connection.');
277 if (gPeerConnection
.getLocalStreams().length
== 0)
278 throw failTest('Tried to toggle local stream, but there is no local ' +
279 'stream in the call.');
281 var track
= selectAudioOrVideoTrack(gPeerConnection
.getLocalStreams()[0]);
282 toggle_(track
, 'local', typeToToggle
);
286 * Hangs up a started call. Returns ok-call-hung-up on success. This tab will
287 * not accept any incoming calls after this call.
290 if (gPeerConnection
== null)
291 throw failTest('hanging up, but has no peer connection');
292 if (getReadyState() != 'active')
293 throw failTest('hanging up, but ready state is not active (no call up).');
294 sendToPeer(gRemotePeerId
, 'BYE');
296 gAcceptsIncomingCalls
= false;
297 returnToTest('ok-call-hung-up');
301 * Start accepting incoming calls again after a hangup.
303 function acceptIncomingCallsAgain() {
304 gAcceptsIncomingCalls
= true;
308 * Do not auto-add the local stream when called.
310 function doNotAutoAddLocalStreamWhenCalled() {
311 gAutoAddLocalToPeerConnectionStreamWhenCalled
= false;
315 * Disconnects from the peerconnection server. Returns ok-disconnected on
318 function disconnect() {
319 if (gOurPeerId
== null)
320 throw failTest('Disconnecting, but we are not connected.');
322 request
= new XMLHttpRequest();
323 request
.open('GET', gServerUrl
+ '/sign_out?peer_id=' + gOurPeerId
, false);
326 returnToTest('ok-disconnected');
330 * Creates a DataChannel on the current PeerConnection. Only one DataChannel can
331 * be created on each PeerConnection.
332 * Returns ok-datachannel-created on success.
334 function createDataChannelOnPeerConnection() {
335 if (gPeerConnection
== null)
336 throw failTest('Tried to create data channel, ' +
337 'but have no peer connection.');
339 createDataChannel(gPeerConnection
, gOurClientName
);
340 returnToTest('ok-datachannel-created');
344 * Close the DataChannel on the current PeerConnection.
345 * Returns ok-datachannel-close on success.
347 function closeDataChannelOnPeerConnection() {
348 if (gPeerConnection
== null)
349 throw failTest('Tried to close data channel, ' +
350 'but have no peer connection.');
352 closeDataChannel(gPeerConnection
);
353 returnToTest('ok-datachannel-close');
357 * Creates a DTMF sender on the current PeerConnection.
358 * Returns ok-dtmfsender-created on success.
360 function createDtmfSenderOnPeerConnection() {
361 if (gPeerConnection
== null)
362 throw failTest('Tried to create DTMF sender, ' +
363 'but have no peer connection.');
365 createDtmfSender(gPeerConnection
);
366 returnToTest('ok-dtmfsender-created');
370 * Send DTMF tones on the gDtmfSender.
371 * Returns ok-dtmf-sent on success.
373 function insertDtmfOnSender(tones
, duration
, interToneGap
) {
374 if (gDtmfSender
== null)
375 throw failTest('Tried to insert DTMF tones, ' +
376 'but have no DTMF sender.');
378 insertDtmf(tones
, duration
, interToneGap
);
379 returnToTest('ok-dtmf-sent');
382 // Public interface to signaling implementations, such as JSEP.
385 * Sends a message to a peer through the peerconnection_server.
387 function sendToPeer(peer
, message
) {
388 var messageToLog
= message
.sdp
? message
.sdp
: message
;
389 debug('Sending message ' + messageToLog
+ ' to peer ' + peer
+ '.');
391 var request
= new XMLHttpRequest();
392 var url
= gServerUrl
+ '/message?peer_id=' + gOurPeerId
+ '&to=' + peer
;
393 request
.open('POST', url
, false);
394 request
.setRequestHeader('Content-Type', 'text/plain');
395 request
.send(message
);
399 * Returns true if we are disconnected from peerconnection_server.
401 function isDisconnected() {
402 return gOurPeerId
== null;
406 * @return {!string} The current peer connection's ready state, or
407 * 'no-peer-connection' if there is no peer connection up.
409 * NOTE: The PeerConnection states are changing and until chromium has
410 * implemented the new states we have to use this interim solution of
411 * always assuming that the PeerConnection is 'active'.
413 function getReadyState() {
414 if (gPeerConnection
== null)
415 return 'no-peer-connection';
423 function toggle_(track
, localOrRemote
, audioOrVideo
) {
425 throw failTest('Tried to toggle ' + localOrRemote
+ ' ' + audioOrVideo
+
426 ' stream, but has no such stream.');
428 track
.enabled
= !track
.enabled
;
429 returnToTest('ok-' + audioOrVideo
+ '-toggled-to-' + track
.enabled
);
433 function connectCallback_(request
) {
434 debug('Connect callback: ' + request
.status
+ ', ' + request
.readyState
);
435 if (request
.status
== 0) {
436 debug('peerconnection_server doesn\'t seem to be up.');
437 returnToTest('failed-to-connect');
439 if (request
.readyState
== 4 && request
.status
== 200) {
440 gOurPeerId
= parseOurPeerId_(request
.responseText
);
441 gRemotePeerId
= parseRemotePeerIdIfConnected_(request
.responseText
);
442 startHangingGet_(gServerUrl
, gOurPeerId
);
443 returnToTest('ok-connected');
448 function parseOurPeerId_(responseText
) {
449 // According to peerconnection_server's protocol.
450 var peerList
= responseText
.split('\n');
451 return parseInt(peerList
[0].split(',')[1]);
455 function parseRemotePeerIdIfConnected_(responseText
) {
456 var peerList
= responseText
.split('\n');
457 if (peerList
.length
== 1) {
458 // No peers have connected yet - we'll get their id later in a notification.
461 var remotePeerId
= null;
462 for (var i
= 0; i
< peerList
.length
; i
++) {
463 if (peerList
[i
].length
== 0)
466 var parsed
= peerList
[i
].split(',');
467 var name
= parsed
[0];
470 if (id
!= gOurPeerId
) {
471 debug('Found remote peer with name ' + name
+ ', id ' +
472 id
+ ' when connecting.');
474 // There should be at most one remote peer in this test.
475 if (remotePeerId
!= null)
476 throw failTest('Expected just one remote peer in this test: ' +
479 // Found a remote peer.
487 function startHangingGet_(server
, ourId
) {
488 if (isDisconnected())
490 hangingGetRequest
= new XMLHttpRequest();
491 hangingGetRequest
.onreadystatechange = function() {
492 hangingGetCallback_(hangingGetRequest
, server
, ourId
);
494 hangingGetRequest
.ontimeout = function() {
495 hangingGetTimeoutCallback_(hangingGetRequest
, server
, ourId
);
497 callUrl
= server
+ '/wait?peer_id=' + ourId
;
498 debug('Sending ' + callUrl
);
499 hangingGetRequest
.open('GET', callUrl
, true);
500 hangingGetRequest
.send();
504 function hangingGetCallback_(hangingGetRequest
, server
, ourId
) {
505 if (hangingGetRequest
.readyState
!= 4 || hangingGetRequest
.status
== 0) {
506 // Code 0 is not possible if the server actually responded. Ignore.
509 if (hangingGetRequest
.status
!= 200) {
510 throw failTest('Error ' + hangingGetRequest
.status
+ ' from server: ' +
511 hangingGetRequest
.statusText
);
513 var targetId
= readResponseHeader_(hangingGetRequest
, 'Pragma');
514 if (targetId
== ourId
)
515 handleServerNotification_(hangingGetRequest
.responseText
);
517 handlePeerMessage_(targetId
, hangingGetRequest
.responseText
);
519 hangingGetRequest
.abort();
520 restartHangingGet_(server
, ourId
);
524 function hangingGetTimeoutCallback_(hangingGetRequest
, server
, ourId
) {
525 debug('Hanging GET times out, re-issuing...');
526 hangingGetRequest
.abort();
527 restartHangingGet_(server
, ourId
);
531 function handleServerNotification_(message
) {
532 var parsed
= message
.split(',');
533 if (parseInt(parsed
[2]) == 1) {
534 // Peer connected - this must be our remote peer, and it must mean we
535 // connected before them (except if we happened to connect to the server
536 // at precisely the same moment).
537 debug('Found remote peer with name ' + parsed
[0] + ', id ' +
538 parsed
[1] + ' when connecting.');
539 gRemotePeerId
= parseInt(parsed
[1]);
544 function closeCall_() {
545 if (gPeerConnection
== null)
546 throw failTest('Closing call, but no call active.');
547 gPeerConnection
.close();
548 gPeerConnection
= null;
552 function handlePeerMessage_(peerId
, message
) {
553 debug('Received message from peer ' + peerId
+ ': ' + message
);
554 if (peerId
!= gRemotePeerId
) {
555 throw failTest('Received notification from unknown peer ' + peerId
+
556 ' (only know about ' + gRemotePeerId
+ '.');
558 if (message
.search('BYE') == 0) {
559 debug('Received BYE from peer: closing call');
563 if (gPeerConnection
== null && gAcceptsIncomingCalls
) {
564 // The other side is calling us.
565 debug('We are being called: answer...');
567 gPeerConnection
= createPeerConnection(STUN_SERVER
, gUseRtpDataChannels
);
568 if (gAutoAddLocalToPeerConnectionStreamWhenCalled
&&
569 obtainGetUserMediaResult() == 'ok-got-stream') {
570 debug('We have a local stream, so hook it up automatically.');
571 addLocalStreamToPeerConnection(gPeerConnection
);
573 answerCall(gPeerConnection
, message
);
576 if (gPeerConnection
== null) {
577 debug('Discarding message ' + message
+ '; already disconnected.');
581 handleMessage(gPeerConnection
, message
);
585 function restartHangingGet_(server
, ourId
) {
586 window
.setTimeout(function() {
587 startHangingGet_(server
, ourId
);
592 function readResponseHeader_(request
, key
) {
593 var value
= request
.getResponseHeader(key
)
594 if (value
== null || value
.length
== 0) {
595 throw failTest('Received empty value ' + value
+
596 ' for response header key ' + key
+ '.');
598 return parseInt(value
);