Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / content / test / data / media / peerconnection-call.html
1 <html>
2 <head>
3   <script type="text/javascript" src="webrtc_test_utilities.js"></script>
4   <script type="text/javascript" src="webrtc_test_audio.js"></script>
5   <script type="text/javascript">
6   $ = function(id) {
7     return document.getElementById(id);
8   };
9
10   var gFirstConnection = null;
11   var gSecondConnection = null;
12   var gTestWithoutMsid = false;
13   var gLocalStream = null;
14   var gSentTones = '';
15
16   var gRemoteStreams = {};
17
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; };
22   var onLocalDescriptionError = function(error) { };
23
24   // Temporary measure to be able to force iSAC 16K where needed, particularly
25   // on Android. This applies to every test which is why it's implemented like
26   // this.
27   var maybeForceIsac16K = function(sdp) { return sdp; };
28   function forceIsac16KInSdp() {
29     maybeForceIsac16K = function(sdp) {
30       // Remove all other codecs (not the video codecs though). Also leave
31       // 126 for DTMF.
32       sdp = sdp.replace(/m=audio (\d+) RTP\/SAVPF.*\r\n/g,
33                         'm=audio $1 RTP/SAVPF 103 126\r\n');
34       sdp = sdp.replace('a=fmtp:111 minptime=10', 'a=fmtp:103 minptime=10');
35       sdp = sdp.replace(
36           /a=rtpmap:(?!(103|126))\d{1,3} (?!X?VP8|red|ulpfec).*\r\n/g,
37           '');
38       return sdp;
39     };
40   }
41
42   // When using external SDES, the crypto key is chosen by javascript.
43   var EXTERNAL_SDES_LINES = {
44     'audio': 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' +
45         'inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR',
46     'video': 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' +
47         'inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj',
48     'data': 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' +
49         'inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj'
50   };
51
52   // When using GICE, the ICE credentials can be chosen by javascript.
53   var EXTERNAL_GICE_UFRAG = '1234567890123456';
54   var EXTERNAL_GICE_PWD = '123456789012345678901234';
55
56   setAllEventsOccuredHandler(function() {
57     // The C++ tests look for this 'OK' in the title.
58     document.title = 'OK';
59   });
60
61   // Test that we can setup call with an audio and video track.
62   function call(constraints) {
63     createConnections(null);
64     navigator.webkitGetUserMedia(constraints,
65       addStreamToBothConnectionsAndNegotiate, printGetUserMediaError);
66     waitForVideo('remote-view-1');
67     waitForVideo('remote-view-2');
68   }
69
70   // First calls without streams on any connections, and then adds a stream
71   // to peer connection 1 which gets sent to peer connection 2. We must wait
72   // for the first negotiation to complete before starting the second one, which
73   // is why we wait until the connection is stable before re-negotiating.
74   function callEmptyThenAddOneStreamAndRenegotiate(constraints) {
75     createConnections(null);
76     negotiate();
77     waitForConnectionToStabilize(gFirstConnection, function() {
78       navigator.webkitGetUserMedia(constraints,
79         addStreamToTheFirstConnectionAndNegotiate, printGetUserMediaError);
80       // Only the first connection is sending here.
81       waitForVideo('remote-view-2');
82     });
83   }
84
85   // First makes a call between pc1 and pc2, and then makes a call between pc3
86   // and pc4 where the remote streams from pc1 and pc2 will be used as the local
87   // streams of pc3 and pc4.
88   function callAndForwardRemoteStream(constraints) {
89     createConnections(null);
90     navigator.webkitGetUserMedia(constraints,
91                                  addStreamToBothConnectionsAndNegotiate,
92                                  printGetUserMediaError);
93     var gotRemoteStream1 = false;
94     var gotRemoteStream2 = false;
95
96     var onRemoteStream1 = function() {
97       gotRemoteStream1 = true;
98       maybeCallEstablished();
99     }
100
101     var onRemoteStream2 = function() {
102       gotRemoteStream2 = true;
103       maybeCallEstablished();
104     }
105
106     var maybeCallEstablished = function() {
107       if (gotRemoteStream1 && gotRemoteStream2) {
108         onCallEstablished();
109       }
110     }
111
112     var onCallEstablished = function() {
113       thirdConnection = createConnection(null, 'remote-view-3');
114       thirdConnection.addStream(gRemoteStreams['remote-view-1']);
115
116       fourthConnection = createConnection(null, 'remote-view-4');
117       fourthConnection.addStream(gRemoteStreams['remote-view-2']);
118
119       negotiateBetween(thirdConnection, fourthConnection);
120
121       waitForVideo('remote-view-3');
122       waitForVideo('remote-view-4');
123     }
124
125     // Do the forwarding after we have received video.
126     detectVideoPlaying('remote-view-1', onRemoteStream1);
127     detectVideoPlaying('remote-view-2', onRemoteStream2);
128   }
129
130   // Test that we can setup call with an audio and video track and
131   // simulate that the remote peer don't support MSID.
132   function callWithoutMsidAndBundle() {
133     createConnections(null);
134     transformSdp = removeBundle;
135     transformRemoteSdp = removeMsid;
136     gTestWithoutMsid = true;
137     navigator.webkitGetUserMedia({audio: true, video: true},
138         addStreamToBothConnectionsAndNegotiate, printGetUserMediaError);
139     waitForVideo('remote-view-1');
140     waitForVideo('remote-view-2');
141   }
142
143   // Test that we can't setup a call with an unsupported video codec
144   function negotiateUnsupportedVideoCodec() {
145     createConnections(null);
146     transformSdp = removeVideoCodec;
147     onLocalDescriptionError = function(error) {
148       var expectedMsg = 'Failed to set local offer sdp:' +
149           ' Session error code: ERROR_CONTENT. Session error description:' +
150               ' Failed to set video receive codecs..';
151       expectEquals(expectedMsg, error);
152
153       // Got the right message, test succeeded.
154       document.title = 'OK';
155     };
156     navigator.webkitGetUserMedia({audio: true, video: true},
157         addStreamToBothConnectionsAndNegotiate, printGetUserMediaError);
158   }
159
160   // Test that we can't setup a call if one peer does not support encryption
161   function negotiateNonCryptoCall() {
162     createConnections(null);
163     transformSdp = removeCrypto;
164     onLocalDescriptionError = function(error) {
165       var expectedMsg = 'Failed to set local offer sdp:' +
166           ' Called with a SDP without crypto enabled.';
167       expectEquals(expectedMsg, error);
168
169       // Got the right message, test succeeded.
170       document.title = 'OK';
171     };
172     navigator.webkitGetUserMedia({audio: true, video: true},
173         addStreamToBothConnectionsAndNegotiate, printGetUserMediaError);
174   }
175
176   // Test that we can negotiate a call with an SDP offer that includes a
177   // b=AS:XX line to control audio and video bandwidth
178   function negotiateOfferWithBLine() {
179     createConnections(null);
180     transformSdp = addBandwithControl;
181     navigator.webkitGetUserMedia({audio: true, video: true},
182         addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); 
183     waitForVideo('remote-view-1');
184     waitForVideo('remote-view-2');
185   }
186
187   // Test that we can setup call with legacy settings.
188   function callWithLegacySdp() {
189     transformSdp = function(sdp) {
190       return removeBundle(useGice(useExternalSdes(sdp)));
191     };
192     transformCandidate = addGiceCredsToCandidate;
193     createConnections({
194       'mandatory': {'RtpDataChannels': true, 'DtlsSrtpKeyAgreement': false}
195     });
196     setupDataChannel({reliable: false});
197     navigator.webkitGetUserMedia({audio: true, video: true},
198         addStreamToBothConnectionsAndNegotiate, printGetUserMediaError);
199     waitForVideo('remote-view-1');
200     waitForVideo('remote-view-2');
201   }
202
203   // Test only a data channel.
204   function callWithDataOnly() {
205     createConnections({optional:[{RtpDataChannels: true}]});
206     setupDataChannel({reliable: false});
207     negotiate();
208   }
209
210   function callWithSctpDataOnly() {
211     createConnections({optional: [{DtlsSrtpKeyAgreement: true}]});
212     setupSctpDataChannel({reliable: true});
213     negotiate();
214   }
215
216   // Test call with audio, video and a data channel.
217   function callWithDataAndMedia() {
218     createConnections({optional:[{RtpDataChannels: true}]});
219     setupDataChannel({reliable: false});
220     navigator.webkitGetUserMedia({audio: true, video: true},
221       addStreamToBothConnectionsAndNegotiate,
222       printGetUserMediaError);
223     waitForVideo('remote-view-1');
224     waitForVideo('remote-view-2');
225   }
226
227   function callWithSctpDataAndMedia() {
228     createConnections({optional: [{DtlsSrtpKeyAgreement: true}]});
229     setupSctpDataChannel({reliable: true});
230     navigator.webkitGetUserMedia({audio: true, video: true},
231       addStreamToBothConnectionsAndNegotiate,
232       printGetUserMediaError);
233     waitForVideo('remote-view-1');
234     waitForVideo('remote-view-2');
235   }
236
237
238   // Test call with a data channel and later add audio and video.
239   function callWithDataAndLaterAddMedia() {
240     createConnections({optional:[{RtpDataChannels: true}]});
241     setupDataChannel({reliable: false});
242     negotiate();
243
244     // Set an event handler for when the data channel has been closed.
245     setAllEventsOccuredHandler(function() {
246       // When the video is flowing the test is done.
247       setAllEventsOccuredHandler(function() {
248         document.title = 'OK';
249       });
250       navigator.webkitGetUserMedia({audio: true, video: true},
251         addStreamToBothConnectionsAndNegotiate, printGetUserMediaError);
252       waitForVideo('remote-view-1');
253       waitForVideo('remote-view-2');
254     });
255   }
256
257   // Test that we can setup call and send DTMF.
258   function callAndSendDtmf(tones) {
259     createConnections(null);
260     navigator.webkitGetUserMedia({audio: true, video: true},
261       addStreamToBothConnectionsAndNegotiate, printGetUserMediaError);
262     var onCallEstablished = function() {
263       // Send DTMF tones.
264       var localAudioTrack = gLocalStream.getAudioTracks()[0];
265       var dtmfSender = gFirstConnection.createDTMFSender(localAudioTrack);
266       dtmfSender.ontonechange = onToneChange;
267       dtmfSender.insertDTMF(tones);
268       // Wait for the DTMF tones callback.
269       document.title = 'Waiting for dtmf...';
270       addExpectedEvent();
271       var waitDtmf = setInterval(function() {
272         if (gSentTones == tones) {
273           clearInterval(waitDtmf);
274           eventOccured();
275         }
276       }, 100);
277     }
278
279     // Do the DTMF test after we have received video.
280     detectVideoPlaying('remote-view-2', onCallEstablished);
281   }
282
283   function enableRemoteVideo(peerConnection, enabled) {
284     remoteStream = peerConnection.getRemoteStreams()[0];
285     remoteVideoTrack = remoteStream.getVideoTracks()[0];
286     remoteVideoTrack.enabled = enabled;
287   }
288
289   function enableRemoteAudio(peerConnection, enabled) {
290     remoteStream = peerConnection.getRemoteStreams()[0];
291     remoteAudioTrack = remoteStream.getAudioTracks()[0];
292     remoteAudioTrack.enabled = enabled;
293   }
294
295   function callAndEnsureAudioIsPlaying() {
296     createConnections(null);
297     navigator.webkitGetUserMedia({audio: true, video: true},
298       addStreamToBothConnectionsAndNegotiate, printGetUserMediaError);
299
300     // Wait until we have gathered samples and can conclude if audio is playing.
301     addExpectedEvent();
302     var onCallEstablished = function() {
303       gatherAudioLevelSamples(gSecondConnection, 300, 100,
304                               function(samples) {
305         verifyAudioIsPlaying(samples);
306         eventOccured();
307       });
308
309       // (Also, ensure video muting doesn't affect audio).
310       enableRemoteVideo(gSecondConnection, false);
311     };
312
313     detectVideoPlaying('remote-view-2', onCallEstablished);
314   }
315
316   function callAndEnsureAudioMutingWorks() {
317     callAndEnsureAudioIsPlaying();
318     setAllEventsOccuredHandler(function() {
319       // Call is up, now mute the track and check everything goes silent (give
320       // it a small delay though, we don't expect it to happen instantly).
321       enableRemoteAudio(gSecondConnection, false);
322
323       setTimeout(function() {
324         gatherAudioLevelSamples(gSecondConnection, 200, 100, function(samples) {
325           verifyIsSilent(samples);
326           document.title = 'OK';
327         });
328       }, 500);
329     });
330   }
331
332   function callAndEnsureVideoMutingWorks() {
333     createConnections(null);
334     navigator.webkitGetUserMedia({audio: true, video: true},
335       addStreamToBothConnectionsAndNegotiate, printGetUserMediaError);
336
337     addExpectedEvent();
338     detectVideoPlaying('remote-view-2', function() {
339       // Disable the receiver's remote media stream. Video should stop.
340       // (Also, ensure muting audio doesn't affect video).
341       enableRemoteVideo(gSecondConnection, false);
342       enableRemoteAudio(gSecondConnection, false);
343
344       detectVideoStopped('remote-view-2', function() {
345         // Video has stopped: unmute and succeed if it starts playing again.
346         enableRemoteVideo(gSecondConnection, true);
347         detectVideoPlaying('remote-view-2', eventOccured);
348       })
349     });
350   }
351
352   // Test call with a new Video MediaStream that has been created based on a
353   // stream generated by getUserMedia.
354   function callWithNewVideoMediaStream() {
355     createConnections(null);
356     navigator.webkitGetUserMedia({audio: true, video: true},
357         createNewVideoStreamAndAddToBothConnections, printGetUserMediaError);
358     waitForVideo('remote-view-1');
359     waitForVideo('remote-view-2');
360   }
361
362   // Test call with a new Video MediaStream that has been created based on a
363   // stream generated by getUserMedia. When Video is flowing, an audio track
364   // is added to the sent stream and the video track is removed. This
365   // is to test that adding and removing of remote tracks on an existing
366   // mediastream works.
367   function callWithNewVideoMediaStreamLaterSwitchToAudio() {
368     createConnections(null);
369     navigator.webkitGetUserMedia({audio: true, video: true},
370         createNewVideoStreamAndAddToBothConnections, printGetUserMediaError);
371
372     waitForVideo('remote-view-1');
373     waitForVideo('remote-view-2');
374
375     // Set an event handler for when video is playing.
376     setAllEventsOccuredHandler(function() {
377       // Add an audio track to the local stream and remove the video track and
378       // then renegotiate. But first - setup the expectations.
379       local_stream = gFirstConnection.getLocalStreams()[0];
380
381       remote_stream_1 = gFirstConnection.getRemoteStreams()[0];
382       // Add an expected event that onaddtrack will be called on the remote
383       // mediastream received on gFirstConnection when the audio track is
384       // received.
385       addExpectedEvent();
386       remote_stream_1.onaddtrack = function(){
387         expectEquals(remote_stream_1.getAudioTracks()[0].id,
388                      local_stream.getAudioTracks()[0].id);
389         eventOccured();
390       }
391
392       // Add an expectation that the received video track is removed from
393       // gFirstConnection.
394       addExpectedEvent();
395       remote_stream_1.onremovetrack = function() {
396         eventOccured();
397       }
398
399       // Add an expected event that onaddtrack will be called on the remote
400       // mediastream received on gSecondConnection when the audio track is
401       // received.
402       remote_stream_2 = gSecondConnection.getRemoteStreams()[0];
403       addExpectedEvent();
404       remote_stream_2.onaddtrack = function() {
405         expectEquals(remote_stream_2.getAudioTracks()[0].id,
406                      local_stream.getAudioTracks()[0].id);
407         eventOccured();
408       }
409
410       // Add an expectation that the received video track is removed from
411       // gSecondConnection.
412       addExpectedEvent();
413       remote_stream_2.onremovetrack = function() {
414         eventOccured();
415       }
416       // When all the above events have occurred- the test pass.
417       setAllEventsOccuredHandler(function() { document.title = 'OK'; });
418
419       local_stream.addTrack(gLocalStream.getAudioTracks()[0]);
420       local_stream.removeTrack(local_stream.getVideoTracks()[0]);
421       negotiate();
422     });
423   }
424
425   // This function is used for setting up a test that:
426   // 1. Creates a data channel on |gFirstConnection| and sends data to
427   //    |gSecondConnection|.
428   // 2. When data is received on |gSecondConnection| a message
429   //    is sent to |gFirstConnection|.
430   // 3. When data is received on |gFirstConnection|, the data
431   //    channel is closed. The test passes when the state transition completes.
432   function setupDataChannel(params) {
433     var sendDataString = "send some text on a data channel."
434     firstDataChannel = gFirstConnection.createDataChannel(
435         "sendDataChannel", params);
436     expectEquals('connecting', firstDataChannel.readyState);
437
438     // When |firstDataChannel| transition to open state, send a text string.
439     firstDataChannel.onopen = function() {
440       expectEquals('open', firstDataChannel.readyState);
441       firstDataChannel.send(sendDataString);
442     }
443
444     // When |firstDataChannel| receive a message, close the channel and
445     // initiate a new offer/answer exchange to complete the closure.
446     firstDataChannel.onmessage = function(event) {
447       expectEquals(event.data, sendDataString);
448       firstDataChannel.close();
449       negotiate();
450     }
451
452     // When |firstDataChannel| transition to closed state, the test pass.
453     addExpectedEvent();
454     firstDataChannel.onclose = function() {
455       expectEquals('closed', firstDataChannel.readyState);
456       eventOccured();
457     }
458
459     // Event handler for when |gSecondConnection| receive a new dataChannel.
460     gSecondConnection.ondatachannel = function (event) {
461       var secondDataChannel = event.channel;
462
463       // When |secondDataChannel| receive a message, send a message back.
464       secondDataChannel.onmessage = function(event) {
465         expectEquals(event.data, sendDataString);
466         expectEquals('open', secondDataChannel.readyState);
467         secondDataChannel.send(sendDataString);
468       }
469     }
470   }
471
472   // SCTP data channel setup is slightly different then RTP based
473   // channels. Due to a bug in libjingle, we can't send data immediately
474   // after channel becomes open. So for that reason in SCTP,
475   // we are sending data from second channel, when ondatachannel event is
476   // received. So data flow happens 2 -> 1 -> 2.
477   function setupSctpDataChannel(params) {
478     var sendDataString = "send some text on a data channel."
479     firstDataChannel = gFirstConnection.createDataChannel(
480         "sendDataChannel", params);
481     expectEquals('connecting', firstDataChannel.readyState);
482
483     // When |firstDataChannel| transition to open state, send a text string.
484     firstDataChannel.onopen = function() {
485       expectEquals('open', firstDataChannel.readyState);
486     }
487
488     // When |firstDataChannel| receive a message, send message back.
489     // initiate a new offer/answer exchange to complete the closure.
490     firstDataChannel.onmessage = function(event) {
491       expectEquals('open', firstDataChannel.readyState);
492       expectEquals(event.data, sendDataString);
493       firstDataChannel.send(sendDataString);
494     }
495
496
497     // Event handler for when |gSecondConnection| receive a new dataChannel.
498     gSecondConnection.ondatachannel = function (event) {
499       var secondDataChannel = event.channel;
500       secondDataChannel.onopen = function() {
501         secondDataChannel.send(sendDataString);
502       }
503
504       // When |secondDataChannel| receive a message, close the channel and
505       // initiate a new offer/answer exchange to complete the closure.
506       secondDataChannel.onmessage = function(event) {
507         expectEquals(event.data, sendDataString);
508         expectEquals('open', secondDataChannel.readyState);
509         secondDataChannel.close();
510         negotiate();
511       }
512
513       // When |secondDataChannel| transition to closed state, the test pass.
514       addExpectedEvent();
515       secondDataChannel.onclose = function() {
516         expectEquals('closed', secondDataChannel.readyState);
517         eventOccured();
518       }
519     }
520   }
521
522   // Test call with a stream that has been created by getUserMedia, clone
523   // the stream to a cloned stream, send them via the same peer connection.
524   function addTwoMediaStreamsToOneConnection() {
525     createConnections(null);
526     navigator.webkitGetUserMedia({audio: true, video: true},
527         CloneStreamAndAddTwoStreamstoOneConnection, printGetUserMediaError);
528   }
529
530   function onToneChange(tone) {
531     gSentTones += tone.tone;
532     document.title = gSentTones;
533   }
534
535   function createConnections(constraints) {
536     gFirstConnection = createConnection(constraints, 'remote-view-1');
537     expectEquals('stable', gFirstConnection.signalingState);
538
539     gSecondConnection = createConnection(constraints, 'remote-view-2');
540     expectEquals('stable', gSecondConnection.signalingState);
541   }
542
543   function createConnection(constraints, remoteView) {
544     var pc = new webkitRTCPeerConnection(null, constraints);
545     pc.onaddstream = function(event) {
546       onRemoteStream(event, remoteView);
547     }
548     return pc;
549   }
550
551   function displayAndRemember(localStream) {
552     var localStreamUrl = URL.createObjectURL(localStream);
553     $('local-view').src = localStreamUrl;
554
555     gLocalStream = localStream;
556   }
557
558   // Called if getUserMedia fails.
559   function printGetUserMediaError(error) {
560     document.title = 'getUserMedia request failed:';
561     if (error.constraintName)
562       document.title += ' could not satisfy constraint ' + error.constraintName;
563     else
564       document.title += ' devices not working/user denied access.';
565     console.log(document.title);
566   }
567
568   // Called if getUserMedia succeeds and we want to send from both connections.
569   function addStreamToBothConnectionsAndNegotiate(localStream) {
570     displayAndRemember(localStream);
571     gFirstConnection.addStream(localStream);
572     gSecondConnection.addStream(localStream);
573     negotiate();
574   }
575
576   // Called if getUserMedia succeeds when we want to send from one connection.
577   function addStreamToTheFirstConnectionAndNegotiate(localStream) {
578     displayAndRemember(localStream);
579     gFirstConnection.addStream(localStream);
580     negotiate();
581   }
582
583   function verifyHasOneAudioAndVideoTrack(stream) {
584     expectEquals(1, stream.getAudioTracks().length);
585     expectEquals(1, stream.getVideoTracks().length);
586   }
587
588   // Called if getUserMedia succeeds, then clone the stream, send two streams
589   // from one peer connection.
590   function CloneStreamAndAddTwoStreamstoOneConnection(localStream) {
591     displayAndRemember(localStream);
592
593     var clonedStream = null;
594     if (typeof localStream.clone === "function") {
595       clonedStream = localStream.clone();
596     } else {
597       clonedStream = new webkitMediaStream(localStream);
598     }
599
600     gFirstConnection.addStream(localStream);
601     gFirstConnection.addStream(clonedStream);
602
603     // Verify the local streams are correct.
604     expectEquals(2, gFirstConnection.getLocalStreams().length);
605     verifyHasOneAudioAndVideoTrack(gFirstConnection.getLocalStreams()[0]);
606     verifyHasOneAudioAndVideoTrack(gFirstConnection.getLocalStreams()[1]);
607
608     // The remote side should receive two streams. After that, verify the
609     // remote side has the correct number of streams and tracks.
610     addExpectedEvent();
611     addExpectedEvent();
612     gSecondConnection.onaddstream = function(event) {
613       eventOccured();
614     }
615     setAllEventsOccuredHandler(function() {
616       // Negotiation complete, verify remote streams on the receiving side.
617       expectEquals(2, gSecondConnection.getRemoteStreams().length);
618       verifyHasOneAudioAndVideoTrack(gSecondConnection.getRemoteStreams()[0]);
619       verifyHasOneAudioAndVideoTrack(gSecondConnection.getRemoteStreams()[1]);
620
621       document.title = "OK";
622     });
623
624     negotiate();
625   }
626
627   // Called if getUserMedia succeeds when we want to send a modified
628   // MediaStream. A new MediaStream is created and the video track from
629   // |localStream| is added.
630   function createNewVideoStreamAndAddToBothConnections(localStream) {
631     displayAndRemember(localStream);
632     var new_stream = new webkitMediaStream();
633     new_stream.addTrack(localStream.getVideoTracks()[0]);
634     gFirstConnection.addStream(new_stream);
635     gSecondConnection.addStream(new_stream);
636     negotiate();
637   }
638
639   function negotiate() {
640     negotiateBetween(gFirstConnection, gSecondConnection);
641   }
642
643   function negotiateBetween(caller, callee) {
644     console.log("Negotiating call...");
645     // Not stable = negotiation is ongoing. The behavior of re-negotiating while
646     // a negotiation is ongoing is more or less undefined, so avoid this.
647     if (caller.signalingState != 'stable')
648       throw 'You can only negotiate when the connection is stable!';
649
650     connectOnIceCandidate(caller, callee);
651
652     caller.createOffer(
653         function (offer) {
654           onOfferCreated(offer, caller, callee);
655         });
656   }
657
658   function onOfferCreated(offer, caller, callee) {
659     offer.sdp = maybeForceIsac16K(transformSdp(offer.sdp));
660     caller.setLocalDescription(offer, function() {
661       expectEquals('have-local-offer', caller.signalingState);
662       receiveOffer(offer.sdp, caller, callee);
663     }, onLocalDescriptionError);
664   }
665
666   function receiveOffer(offerSdp, caller, callee) {
667     console.log("Receiving offer...");
668     offerSdp = transformRemoteSdp(offerSdp);
669
670     var parsedOffer = new RTCSessionDescription({ type: 'offer',
671                                                   sdp: offerSdp });
672     callee.setRemoteDescription(parsedOffer);
673     callee.createAnswer(function (answer) {
674                           onAnswerCreated(answer, caller, callee);
675                         });
676     expectEquals('have-remote-offer', callee.signalingState);
677   }
678
679   function removeMsid(offerSdp) {
680     offerSdp = offerSdp.replace(/a=msid-semantic.*\r\n/g, '');
681     offerSdp = offerSdp.replace('a=mid:audio\r\n', '');
682     offerSdp = offerSdp.replace('a=mid:video\r\n', '');
683     offerSdp = offerSdp.replace(/a=ssrc.*\r\n/g, '');
684     return offerSdp;
685   }
686
687   function removeVideoCodec(offerSdp) {
688     offerSdp = offerSdp.replace('a=rtpmap:100 VP8/90000\r\n',
689                                 'a=rtpmap:100 XVP8/90000\r\n');
690     return offerSdp;
691   }
692
693   function removeCrypto(offerSdp) {
694     offerSdp = offerSdp.replace(/a=crypto.*\r\n/g, 'a=Xcrypto\r\n');
695     offerSdp = offerSdp.replace(/a=fingerprint.*\r\n/g, '');
696     return offerSdp;
697   }
698
699   function addBandwithControl(offerSdp) {
700     offerSdp = offerSdp.replace('a=mid:audio\r\n', 'a=mid:audio\r\n'+
701                                 'b=AS:16\r\n');
702     offerSdp = offerSdp.replace('a=mid:video\r\n', 'a=mid:video\r\n'+
703                                 'b=AS:512\r\n');
704     return offerSdp;
705   }
706   
707   function removeBundle(sdp) {
708     return sdp.replace(/a=group:BUNDLE .*\r\n/g, '');
709   }
710
711   function useGice(sdp) {
712     sdp = sdp.replace(/t=.*\r\n/g, function(subString) {
713       return subString + 'a=ice-options:google-ice\r\n';
714     });
715     sdp = sdp.replace(/a=ice-ufrag:.*\r\n/g,
716                       'a=ice-ufrag:' + EXTERNAL_GICE_UFRAG + '\r\n');
717     sdp = sdp.replace(/a=ice-pwd:.*\r\n/g,
718                       'a=ice-pwd:' + EXTERNAL_GICE_PWD + '\r\n');
719     return sdp;
720   }
721
722   function useExternalSdes(sdp) {
723     // Remove current crypto specification.
724     sdp = sdp.replace(/a=crypto.*\r\n/g, '');
725     sdp = sdp.replace(/a=fingerprint.*\r\n/g, '');
726     // Add external crypto.  This is not compatible with |removeMsid|.
727     sdp = sdp.replace(/a=mid:(\w+)\r\n/g, function(subString, group) {
728       return subString + EXTERNAL_SDES_LINES[group] + '\r\n';
729     });
730     return sdp;
731   }
732
733   function onAnswerCreated(answer, caller, callee) {
734     answer.sdp = maybeForceIsac16K(transformSdp(answer.sdp));
735     callee.setLocalDescription(answer);
736     expectEquals('stable', callee.signalingState);
737     receiveAnswer(answer.sdp, caller);
738   }
739
740   function receiveAnswer(answerSdp, caller) {
741     console.log("Receiving answer...");
742     answerSdp = transformRemoteSdp(answerSdp);
743     var parsedAnswer = new RTCSessionDescription({ type: 'answer',
744                                                    sdp: answerSdp });
745     caller.setRemoteDescription(parsedAnswer);
746     expectEquals('stable', caller.signalingState);
747   }
748
749   function connectOnIceCandidate(caller, callee) {
750     caller.onicecandidate = function(event) { onIceCandidate(event, callee); }
751     callee.onicecandidate = function(event) { onIceCandidate(event, caller); }
752   }
753
754   function addGiceCredsToCandidate(candidate) {
755     return candidate.trimRight() +
756         ' username ' + EXTERNAL_GICE_UFRAG + ' password ' + EXTERNAL_GICE_PWD;
757   }
758
759   function onIceCandidate(event, target) {
760     if (event.candidate) {
761       var candidate = new RTCIceCandidate(event.candidate);
762       candidate.candidate = transformCandidate(candidate.candidate);
763       target.addIceCandidate(candidate);
764     }
765   }
766
767   function onRemoteStream(e, target) {
768     console.log("Receiving remote stream...");
769     if (gTestWithoutMsid && e.stream.id != "default") {
770       document.title = 'a default remote stream was expected but instead ' +
771           e.stream.id + ' was received.';
772       return;
773     }
774     gRemoteStreams[target] = e.stream;
775     var remoteStreamUrl = URL.createObjectURL(e.stream);
776     var remoteVideo = $(target);
777     remoteVideo.src = remoteStreamUrl;
778   }
779
780   </script>
781 </head>
782 <body>
783   <table border="0">
784     <tr>
785       <td>Local Preview</td>
786       <td>Remote Stream for Connection 1</td>
787       <td>Remote Stream for Connection 2</td>
788       <td>Remote Stream for Connection 3</td>
789       <td>Remote Stream for Connection 4</td>
790     </tr>
791     <tr>
792       <td><video width="320" height="240" id="local-view"
793           autoplay="autoplay"></video></td>
794       <td><video width="320" height="240" id="remote-view-1"
795           autoplay="autoplay"></video></td>
796       <td><video width="320" height="240" id="remote-view-2"
797           autoplay="autoplay"></video></td>
798       <td><video width="320" height="240" id="remote-view-3"
799           autoplay="autoplay"></video></td>
800       <td><video width="320" height="240" id="remote-view-4"
801           autoplay="autoplay"></video></td>
802       <!-- Canvases are named after their corresponding video elements. -->
803       <td><canvas width="320" height="240" id="remote-view-1-canvas"
804           style="display:none"></canvas></td>
805       <td><canvas width="320" height="240" id="remote-view-2-canvas"
806           style="display:none"></canvas></td>
807       <td><canvas width="320" height="240" id="remote-view-3-canvas"
808           style="display:none"></canvas></td>
809       <td><canvas width="320" height="240" id="remote-view-4-canvas"
810           style="display:none"></canvas></td>
811     </tr>
812   </table>
813 </body>
814 </html>