3 <script type="text/javascript" src="webrtc_test_utilities.js"></script>
4 <script type="text/javascript">
6 return document.getElementById(id);
9 var gFirstConnection = null;
10 var gSecondConnection = null;
11 var gTestWithoutMsid = false;
13 var gLocalStream = null;
16 var gRemoteStreams = {};
18 // Default transform functions, overridden by some test cases.
19 var transformSdp = function(sdp) { return sdp; };
20 var transformRemoteSdp = function(sdp) { return sdp; };
21 var transformCandidate = function(candidate) { return candidate; };
23 // When using external SDES, the crypto key is chosen by javascript.
24 var EXTERNAL_SDES_LINES = {
25 'audio': 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' +
26 'inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR',
27 'video': 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' +
28 'inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj',
29 'data': 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' +
30 'inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj'
33 // When using GICE, the ICE credentials can be chosen by javascript.
34 var EXTERNAL_GICE_UFRAG = '1234567890123456';
35 var EXTERNAL_GICE_PWD = '123456789012345678901234';
37 setAllEventsOccuredHandler(function() {
38 document.title = 'OK';
41 // Test that we can setup call with an audio and video track.
42 function call(constraints) {
43 createConnections(null);
44 navigator.webkitGetUserMedia(constraints,
45 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError);
46 waitForVideo('remote-view-1');
47 waitForVideo('remote-view-2');
50 // First calls without streams on any connections, and then adds a stream
51 // to peer connection 1 which gets sent to peer connection 2. We must wait
52 // for the first negotiation to complete before starting the second one, which
53 // is why we wait until the connection is stable before re-negotiating.
54 function callEmptyThenAddOneStreamAndRenegotiate(constraints) {
55 createConnections(null);
57 waitForConnectionToStabilize(gFirstConnection);
58 navigator.webkitGetUserMedia(constraints,
59 addStreamToTheFirstConnectionAndNegotiate, printGetUserMediaError);
60 // Only the first connection is sending here.
61 waitForVideo('remote-view-2');
64 // First makes a call between pc1 and pc2, and then makes a call between pc3
65 // and pc4 where the remote streams from pc1 and pc2 will be used as the local
66 // streams of pc3 and pc4.
67 function callAndForwardRemoteStream(constraints) {
68 createConnections(null);
69 navigator.webkitGetUserMedia(constraints,
70 addStreamToBothConnectionsAndNegotiate,
71 printGetUserMediaError);
72 var gotRemoteStream1 = false;
73 var gotRemoteStream2 = false;
75 var onRemoteStream1 = function() {
76 gotRemoteStream1 = true;
77 maybeCallEstablished();
80 var onRemoteStream2 = function() {
81 gotRemoteStream2 = true;
82 maybeCallEstablished();
85 var maybeCallEstablished = function() {
86 if (gotRemoteStream1 && gotRemoteStream2) {
91 var onCallEstablished = function() {
92 thirdConnection = createConnection(null, 'remote-view-3');
93 thirdConnection.addStream(gRemoteStreams['remote-view-1']);
95 fourthConnection = createConnection(null, 'remote-view-4');
96 fourthConnection.addStream(gRemoteStreams['remote-view-2']);
98 negotiateBetween(thirdConnection, fourthConnection);
100 waitForVideo('remote-view-3');
101 waitForVideo('remote-view-4');
104 // Do the forwarding after we have received video.
105 detectVideoIn('remote-view-1', onRemoteStream1);
106 detectVideoIn('remote-view-2', onRemoteStream2);
109 // Test that we can setup call with an audio and video track and
110 // simulate that the remote peer don't support MSID.
111 function callWithoutMsidAndBundle() {
112 createConnections(null);
113 transformSdp = removeBundle;
114 transformRemoteSdp = removeMsid;
115 gTestWithoutMsid = true;
116 navigator.webkitGetUserMedia({audio: true, video: true},
117 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError);
118 waitForVideo('remote-view-1');
119 waitForVideo('remote-view-2');
122 // Test that we can setup call with legacy settings.
123 function callWithLegacySdp() {
124 transformSdp = function(sdp) {
125 return removeBundle(useGice(useExternalSdes(sdp)));
127 transformCandidate = addGiceCredsToCandidate;
129 'mandatory': {'RtpDataChannels': true, 'DtlsSrtpKeyAgreement': false}
131 setupDataChannel({reliable: false});
132 navigator.webkitGetUserMedia({audio: true, video: true},
133 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError);
134 waitForVideo('remote-view-1');
135 waitForVideo('remote-view-2');
138 // Test only a data channel.
139 function callWithDataOnly() {
140 createConnections({optional:[{RtpDataChannels: true}]});
141 setupDataChannel({reliable: false});
145 function callWithSctpDataOnly() {
146 createConnections({optional: [{DtlsSrtpKeyAgreement: true}]});
147 setupSctpDataChannel({reliable: true});
151 // Test call with audio, video and a data channel.
152 function callWithDataAndMedia() {
153 createConnections({optional:[{RtpDataChannels: true}]});
154 setupDataChannel({reliable: false});
155 navigator.webkitGetUserMedia({audio: true, video: true},
156 addStreamToBothConnectionsAndNegotiate,
157 printGetUserMediaError);
158 waitForVideo('remote-view-1');
159 waitForVideo('remote-view-2');
162 function callWithSctpDataAndMedia() {
163 createConnections({optional: [{DtlsSrtpKeyAgreement: true}]});
164 setupSctpDataChannel({reliable: true});
165 navigator.webkitGetUserMedia({audio: true, video: true},
166 addStreamToBothConnectionsAndNegotiate,
167 printGetUserMediaError);
168 waitForVideo('remote-view-1');
169 waitForVideo('remote-view-2');
173 // Test call with a data channel and later add audio and video.
174 function callWithDataAndLaterAddMedia() {
175 createConnections({optional:[{RtpDataChannels: true}]});
176 setupDataChannel({reliable: false});
179 // Set an event handler for when the data channel has been closed.
180 setAllEventsOccuredHandler(function() {
181 // When the video is flowing the test is done.
182 setAllEventsOccuredHandler(function() {
183 document.title = 'OK';
185 navigator.webkitGetUserMedia({audio: true, video: true},
186 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError);
187 waitForVideo('remote-view-1');
188 waitForVideo('remote-view-2');
192 // Test that we can setup call and send DTMF.
193 function callAndSendDtmf(tones) {
194 createConnections(null);
195 navigator.webkitGetUserMedia({audio: true, video: true},
196 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError);
197 var onCallEstablished = function() {
199 var localAudioTrack = gLocalStream.getAudioTracks()[0];
200 var dtmfSender = gFirstConnection.createDTMFSender(localAudioTrack);
201 dtmfSender.ontonechange = onToneChange;
202 dtmfSender.insertDTMF(tones);
203 // Wait for the DTMF tones callback.
204 document.title = 'Waiting for dtmf...';
206 var waitDtmf = setInterval(function() {
207 if (gSentTones == tones) {
208 clearInterval(waitDtmf);
214 // Do the DTMF test after we have received video.
215 detectVideoIn('remote-view-2', onCallEstablished);
218 // Test call with a new Video MediaStream that has been created based on a
219 // stream generated by getUserMedia.
220 function callWithNewVideoMediaStream() {
221 createConnections(null);
222 navigator.webkitGetUserMedia({audio: true, video: true},
223 createNewVideoStreamAndAddToBothConnections, printGetUserMediaError);
224 waitForVideo('remote-view-1');
225 waitForVideo('remote-view-2');
228 // Test call with a new Video MediaStream that has been created based on a
229 // stream generated by getUserMedia. When Video is flowing, an audio track
230 // is added to the sent stream and the video track is removed. This
231 // is to test that adding and removing of remote tracks on an existing
232 // mediastream works.
233 function callWithNewVideoMediaStreamLaterSwitchToAudio() {
234 createConnections(null);
235 navigator.webkitGetUserMedia({audio: true, video: true},
236 createNewVideoStreamAndAddToBothConnections, printGetUserMediaError);
238 waitForVideo('remote-view-1');
239 waitForVideo('remote-view-2');
241 // Set an event handler for when video is playing.
242 setAllEventsOccuredHandler(function() {
243 // Add an audio track to the local stream and remove the video track and
244 // then renegotiate. But first - setup the expectations.
245 local_stream = gFirstConnection.getLocalStreams()[0];
247 remote_stream_1 = gFirstConnection.getRemoteStreams()[0];
248 // Add an expected event that onaddtrack will be called on the remote
249 // mediastream received on gFirstConnection when the audio track is
252 remote_stream_1.onaddtrack = function(){
253 expectEquals(remote_stream_1.getAudioTracks()[0].id,
254 local_stream.getAudioTracks()[0].id);
258 // Add an expectation that the received video track is removed from
261 remote_stream_1.onremovetrack = function() {
265 // Add an expected event that onaddtrack will be called on the remote
266 // mediastream received on gSecondConnection when the audio track is
268 remote_stream_2 = gSecondConnection.getRemoteStreams()[0];
270 remote_stream_2.onaddtrack = function() {
271 expectEquals(remote_stream_2.getAudioTracks()[0].id,
272 local_stream.getAudioTracks()[0].id);
276 // Add an expectation that the received video track is removed from
277 // gSecondConnection.
279 remote_stream_2.onremovetrack = function() {
282 // When all the above events have occurred- the test pass.
283 setAllEventsOccuredHandler(function() { document.title = 'OK'; });
285 local_stream.addTrack(gLocalStream.getAudioTracks()[0]);
286 local_stream.removeTrack(local_stream.getVideoTracks()[0]);
288 }); // End of setAllEventsOccuredHandler.
291 // This function is used for setting up a test that:
292 // 1. Creates a data channel on |gFirstConnection| and sends data to
293 // |gSecondConnection|.
294 // 2. When data is received on |gSecondConnection| a message
295 // is sent to |gFirstConnection|.
296 // 3. When data is received on |gFirstConnection|, the data
297 // channel is closed. The test passes when the state transition completes.
298 function setupDataChannel(params) {
299 var sendDataString = "send some text on a data channel."
300 firstDataChannel = gFirstConnection.createDataChannel(
301 "sendDataChannel", params);
302 expectEquals('connecting', firstDataChannel.readyState);
304 // When |firstDataChannel| transition to open state, send a text string.
305 firstDataChannel.onopen = function() {
306 expectEquals('open', firstDataChannel.readyState);
307 firstDataChannel.send(sendDataString);
310 // When |firstDataChannel| receive a message, close the channel and
311 // initiate a new offer/answer exchange to complete the closure.
312 firstDataChannel.onmessage = function(event) {
313 expectEquals(event.data, sendDataString);
314 firstDataChannel.close();
318 // When |firstDataChannel| transition to closed state, the test pass.
320 firstDataChannel.onclose = function() {
321 expectEquals('closed', firstDataChannel.readyState);
325 // Event handler for when |gSecondConnection| receive a new dataChannel.
326 gSecondConnection.ondatachannel = function (event) {
327 var secondDataChannel = event.channel;
329 // When |secondDataChannel| receive a message, send a message back.
330 secondDataChannel.onmessage = function(event) {
331 expectEquals(event.data, sendDataString);
332 expectEquals('open', secondDataChannel.readyState);
333 secondDataChannel.send(sendDataString);
338 // SCTP data channel setup is slightly different then RTP based
339 // channels. Due to a bug in libjingle, we can't send data immediately
340 // after channel becomes open. So for that reason in SCTP,
341 // we are sending data from second channel, when ondatachannel event is
342 // received. So data flow happens 2 -> 1 -> 2.
343 function setupSctpDataChannel(params) {
344 var sendDataString = "send some text on a data channel."
345 firstDataChannel = gFirstConnection.createDataChannel(
346 "sendDataChannel", params);
347 expectEquals('connecting', firstDataChannel.readyState);
349 // When |firstDataChannel| transition to open state, send a text string.
350 firstDataChannel.onopen = function() {
351 expectEquals('open', firstDataChannel.readyState);
354 // When |firstDataChannel| receive a message, send message back.
355 // initiate a new offer/answer exchange to complete the closure.
356 firstDataChannel.onmessage = function(event) {
357 expectEquals('open', firstDataChannel.readyState);
358 expectEquals(event.data, sendDataString);
359 firstDataChannel.send(sendDataString);
363 // Event handler for when |gSecondConnection| receive a new dataChannel.
364 gSecondConnection.ondatachannel = function (event) {
365 var secondDataChannel = event.channel;
366 secondDataChannel.send(sendDataString);
368 // When |secondDataChannel| receive a message, close the channel and
369 // initiate a new offer/answer exchange to complete the closure.
370 secondDataChannel.onmessage = function(event) {
371 expectEquals(event.data, sendDataString);
372 expectEquals('open', secondDataChannel.readyState);
373 secondDataChannel.close();
377 // When |secondDataChannel| transition to closed state, the test pass.
379 secondDataChannel.onclose = function() {
380 expectEquals('closed', secondDataChannel.readyState);
386 // Test call with a stream that has been created by getUserMedia, clone
387 // the stream to a cloned stream, send them via the same peer connection.
388 function addTwoMediaStreamsToOneConnection() {
389 createConnections(null);
390 navigator.webkitGetUserMedia({audio: true, video: true},
391 CloneStreamAndAddTwoStreamstoOneConnection, printGetUserMediaError);
394 function onToneChange(tone) {
395 gSentTones += tone.tone;
396 document.title = gSentTones;
399 function createConnections(constraints) {
400 gFirstConnection = createConnection(constraints, 'remote-view-1');
401 expectEquals('stable', gFirstConnection.signalingState);
403 gSecondConnection = createConnection(constraints, 'remote-view-2');
404 expectEquals('stable', gSecondConnection.signalingState);
407 function createConnection(constraints, remoteView) {
408 var pc = new webkitRTCPeerConnection(null, constraints);
409 pc.onaddstream = function(event) {
410 onRemoteStream(event, remoteView);
415 function displayAndRemember(localStream) {
416 var localStreamUrl = webkitURL.createObjectURL(localStream);
417 $('local-view').src = localStreamUrl;
419 gLocalStream = localStream;
422 // Called if getUserMedia fails.
423 function printGetUserMediaError(error) {
424 document.title = 'getUserMedia request failed with code ' + error.code;
427 // Called if getUserMedia succeeds and we want to send from both connections.
428 function addStreamToBothConnectionsAndNegotiate(localStream) {
429 displayAndRemember(localStream);
430 gFirstConnection.addStream(localStream);
431 gSecondConnection.addStream(localStream);
435 // Called if getUserMedia succeeds when we want to send from one connection.
436 function addStreamToTheFirstConnectionAndNegotiate(localStream) {
437 displayAndRemember(localStream);
438 gFirstConnection.addStream(localStream);
442 function verifyHasOneAudioAndVideoTrack(stream) {
443 expectEquals(1, stream.getAudioTracks().length);
444 expectEquals(1, stream.getVideoTracks().length);
447 // Called if getUserMedia succeeds, then clone the stream, send two streams
448 // from one peer connection.
449 function CloneStreamAndAddTwoStreamstoOneConnection(localStream) {
450 displayAndRemember(localStream);
451 var clonedStream = new webkitMediaStream();
452 clonedStream.addTrack(localStream.getVideoTracks()[0]);
453 clonedStream.addTrack(localStream.getAudioTracks()[0]);
454 gFirstConnection.addStream(localStream);
455 gFirstConnection.addStream(clonedStream);
457 // Verify the local streams are correct.
458 expectEquals(2, gFirstConnection.getLocalStreams().length);
459 verifyHasOneAudioAndVideoTrack(gFirstConnection.getLocalStreams()[0]);
460 verifyHasOneAudioAndVideoTrack(gFirstConnection.getLocalStreams()[1]);
462 // The remote side should receive two streams. After that, verify the
463 // remote side has the correct number of streams and tracks.
466 gSecondConnection.onaddstream = function(event) {
469 setAllEventsOccuredHandler(function() {
470 // Negotiation complete, verify remote streams on the receiving side.
471 expectEquals(2, gSecondConnection.getRemoteStreams().length);
472 verifyHasOneAudioAndVideoTrack(gSecondConnection.getRemoteStreams()[0]);
473 verifyHasOneAudioAndVideoTrack(gSecondConnection.getRemoteStreams()[1]);
475 document.title = "OK";
481 // Called if getUserMedia succeeds when we want to send a modified
482 // MediaStream. A new MediaStream is created and the video track from
483 // |localStream| is added.
484 function createNewVideoStreamAndAddToBothConnections(localStream) {
485 displayAndRemember(localStream);
486 var new_stream = new webkitMediaStream();
487 new_stream.addTrack(localStream.getVideoTracks()[0]);
488 gFirstConnection.addStream(new_stream);
489 gSecondConnection.addStream(new_stream);
493 function negotiate() {
494 negotiateBetween(gFirstConnection, gSecondConnection);
497 function negotiateBetween(caller, callee) {
498 // Not stable = negotiation is ongoing. The behavior of re-negotiating while
499 // a negotiation is ongoing is more or less undefined, so avoid this.
500 if (caller.signalingState != 'stable')
501 throw 'You can only negotiate when the connection is stable!';
503 connectOnIceCandidate(caller, callee);
507 onOfferCreated(offer, caller, callee);
511 function onOfferCreated(offer, caller, callee) {
512 offer.sdp = transformSdp(offer.sdp);
513 caller.setLocalDescription(offer);
514 expectEquals('have-local-offer', caller.signalingState);
515 receiveOffer(offer.sdp, caller, callee);
518 function receiveOffer(offerSdp, caller, callee) {
519 offerSdp = transformRemoteSdp(offerSdp);
521 var parsedOffer = new RTCSessionDescription({ type: 'offer',
523 callee.setRemoteDescription(parsedOffer);
524 callee.createAnswer(function (answer) {
525 onAnswerCreated(answer, caller, callee);
527 expectEquals('have-remote-offer', callee.signalingState);
530 function removeMsid(offerSdp) {
531 offerSdp = offerSdp.replace(/a=msid-semantic.*\r\n/g, '');
532 offerSdp = offerSdp.replace('a=mid:audio\r\n', '');
533 offerSdp = offerSdp.replace('a=mid:video\r\n', '');
534 offerSdp = offerSdp.replace(/a=ssrc.*\r\n/g, '');
538 function removeBundle(sdp) {
539 return sdp.replace(/a=group:BUNDLE .*\r\n/g, '');
542 function useGice(sdp) {
543 sdp = sdp.replace(/t=.*\r\n/g, function(subString) {
544 return subString + 'a=ice-options:google-ice\r\n';
546 sdp = sdp.replace(/a=ice-ufrag:.*\r\n/g,
547 'a=ice-ufrag:' + EXTERNAL_GICE_UFRAG + '\r\n');
548 sdp = sdp.replace(/a=ice-pwd:.*\r\n/g,
549 'a=ice-pwd:' + EXTERNAL_GICE_PWD + '\r\n');
553 function useExternalSdes(sdp) {
554 // Remove current crypto specification.
555 sdp = sdp.replace(/a=crypto.*\r\n/g, '');
556 sdp = sdp.replace(/a=fingerprint.*\r\n/g, '');
557 // Add external crypto. This is not compatible with |removeMsid|.
558 sdp = sdp.replace(/a=mid:(\w+)\r\n/g, function(subString, group) {
559 return subString + EXTERNAL_SDES_LINES[group] + '\r\n';
564 function onAnswerCreated(answer, caller, callee) {
565 answer.sdp = transformSdp(answer.sdp);
566 callee.setLocalDescription(answer);
567 expectEquals('stable', callee.signalingState);
568 receiveAnswer(answer.sdp, caller);
571 function receiveAnswer(answerSdp, caller) {
572 answerSdp = transformRemoteSdp(answerSdp);
573 var parsedAnswer = new RTCSessionDescription({ type: 'answer',
575 caller.setRemoteDescription(parsedAnswer);
576 expectEquals('stable', caller.signalingState);
579 function connectOnIceCandidate(caller, callee) {
580 caller.onicecandidate = function(event) { onIceCandidate(event, callee); }
581 callee.onicecandidate = function(event) { onIceCandidate(event, caller); }
584 function addGiceCredsToCandidate(candidate) {
585 return candidate.trimRight() +
586 ' username ' + EXTERNAL_GICE_UFRAG + ' password ' + EXTERNAL_GICE_PWD;
589 function onIceCandidate(event, target) {
590 if (event.candidate) {
591 var candidate = new RTCIceCandidate(event.candidate);
592 candidate.candidate = transformCandidate(candidate.candidate);
593 target.addIceCandidate(candidate);
597 function onRemoteStream(e, target) {
598 if (gTestWithoutMsid && e.stream.id != "default") {
599 document.title = 'a default remote stream was expected but instead ' +
600 e.stream.id + ' was received.';
603 gRemoteStreams[target] = e.stream;
604 var remoteStreamUrl = webkitURL.createObjectURL(e.stream);
605 var remoteVideo = $(target);
606 remoteVideo.src = remoteStreamUrl;
614 <td>Local Preview</td>
615 <td>Remote Stream for Connection 1</td>
616 <td>Remote Stream for Connection 2</td>
617 <td>Remote Stream for Connection 3</td>
618 <td>Remote Stream for Connection 4</td>
621 <td><video width="320" height="240" id="local-view"
622 autoplay="autoplay"></video></td>
623 <td><video width="320" height="240" id="remote-view-1"
624 autoplay="autoplay"></video></td>
625 <td><video width="320" height="240" id="remote-view-2"
626 autoplay="autoplay"></video></td>
627 <td><video width="320" height="240" id="remote-view-3"
628 autoplay="autoplay"></video></td>
629 <td><video width="320" height="240" id="remote-view-4"
630 autoplay="autoplay"></video></td>
631 <!-- Canvases are named after their corresponding video elements. -->
632 <td><canvas width="320" height="240" id="remote-view-1-canvas"
633 style="display:none"></canvas></td>
634 <td><canvas width="320" height="240" id="remote-view-2-canvas"
635 style="display:none"></canvas></td>
636 <td><canvas width="320" height="240" id="remote-view-3-canvas"
637 style="display:none"></canvas></td>
638 <td><canvas width="320" height="240" id="remote-view-4-canvas"
639 style="display:none"></canvas></td>