| <html> |
| <head> |
| <script type="text/javascript" src="webrtc_test_utilities.js"></script> |
| <script type="text/javascript" src="webrtc_test_audio.js"></script> |
| <script type="text/javascript"> |
| $ = function(id) { |
| return document.getElementById(id); |
| }; |
| |
| var gFirstConnection = null; |
| var gSecondConnection = null; |
| var gTestWithoutMsid = false; |
| var gLocalStream = null; |
| var gSentTones = ''; |
| |
| var gRemoteStreams = {}; |
| |
| // Default transform functions, overridden by some test cases. |
| var transformSdp = function(sdp) { return sdp; }; |
| var transformRemoteSdp = function(sdp) { return sdp; }; |
| var onLocalDescriptionError = function(error) { failTest(error); }; |
| var onRemoteDescriptionError = function(error) { failTest(error); }; |
| |
| // Temporary measure to be able to force iSAC 16K where needed, particularly |
| // on Android. This applies to every test which is why it's implemented like |
| // this. |
| var maybeForceIsac16K = function(sdp) { return sdp; }; |
| function forceIsac16KInSdp() { |
| maybeForceIsac16K = function(sdp) { |
| if (sdp.search('m=audio') == -1) |
| return sdp; |
| |
| sdp = sdp.replace(/m=audio (\d+) RTP\/SAVPF.*\r\n/g, |
| 'm=audio $1 RTP/SAVPF 103 126\r\n'); |
| sdp = sdp.replace('a=fmtp:111 minptime=10', 'a=fmtp:103 minptime=10'); |
| if (sdp.search('a=rtpmap:103 ISAC/16000') == -1) |
| failTest('Missing iSAC 16K codec on Android; cannot force codec.'); |
| |
| return sdp; |
| }; |
| sendValueToTest('isac-forced'); |
| } |
| |
| // When using external SDES, the crypto key is chosen by javascript. |
| var EXTERNAL_SDES_LINES = { |
| 'audio': 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' + |
| 'inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR', |
| 'video': 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' + |
| 'inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj', |
| 'data': 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' + |
| 'inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj' |
| }; |
| |
| setAllEventsOccuredHandler(reportTestSuccess); |
| |
| // Test that we can setup call with an audio and video track. |
| function call(constraints) { |
| createConnections(null); |
| navigator.webkitGetUserMedia(constraints, |
| addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| waitForVideo('remote-view-1'); |
| waitForVideo('remote-view-2'); |
| } |
| |
| // Test that we can setup a call with a video track and that the remote peer |
| // receives black frames if the local video track is disabled. |
| function callAndDisableLocalVideo(constraints) { |
| createConnections(null); |
| navigator.webkitGetUserMedia(constraints, |
| addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| detectVideoPlaying('remote-view-1', |
| function () { |
| assertEquals(gLocalStream.getVideoTracks().length, 1); |
| gLocalStream.getVideoTracks()[0].enabled = false; |
| waitForBlackVideo('remote-view-1'); |
| }); |
| } |
| |
| // Test that we can setup call with an audio and video track and check that |
| // the video resolution is as expected. |
| function callAndExpectResolution(constraints, |
| expected_width, |
| expected_height) { |
| createConnections(null); |
| navigator.webkitGetUserMedia(constraints, |
| addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| waitForVideoWithResolution('remote-view-1', |
| expected_width, |
| expected_height); |
| waitForVideoWithResolution('remote-view-2', |
| expected_width, |
| expected_height); |
| } |
| |
| |
| // First calls without streams on any connections, and then adds a stream |
| // to peer connection 1 which gets sent to peer connection 2. We must wait |
| // for the first negotiation to complete before starting the second one, which |
| // is why we wait until the connection is stable before re-negotiating. |
| function callEmptyThenAddOneStreamAndRenegotiate(constraints) { |
| createConnections(null); |
| negotiate(); |
| waitForConnectionToStabilize(gFirstConnection, function() { |
| navigator.webkitGetUserMedia(constraints, |
| addStreamToTheFirstConnectionAndNegotiate, printGetUserMediaError); |
| // Only the first connection is sending here. |
| waitForVideo('remote-view-2'); |
| }); |
| } |
| |
| // First makes a call between pc1 and pc2, and then makes a call between pc3 |
| // and pc4. The stream sent from pc3 to pc4 is the stream received on pc1. |
| // The stream sent from pc4 to pc3 is cloned from the stream received on pc2 |
| // to test that cloning of remote video tracks works as intended. |
| function callAndForwardRemoteStream(constraints) { |
| createConnections(null); |
| navigator.webkitGetUserMedia(constraints, |
| addStreamToBothConnectionsAndNegotiate, |
| printGetUserMediaError); |
| var gotRemoteStream1 = false; |
| var gotRemoteStream2 = false; |
| |
| var onRemoteStream1 = function() { |
| gotRemoteStream1 = true; |
| maybeCallEstablished(); |
| } |
| |
| var onRemoteStream2 = function() { |
| gotRemoteStream2 = true; |
| maybeCallEstablished(); |
| } |
| |
| var maybeCallEstablished = function() { |
| if (gotRemoteStream1 && gotRemoteStream2) { |
| onCallEstablished(); |
| } |
| } |
| |
| var onCallEstablished = function() { |
| thirdConnection = createConnection(null, 'remote-view-3'); |
| thirdConnection.addStream(gRemoteStreams['remote-view-1']); |
| |
| fourthConnection = createConnection(null, 'remote-view-4'); |
| fourthConnection.addStream(gRemoteStreams['remote-view-2'].clone()); |
| |
| negotiateBetween(thirdConnection, fourthConnection); |
| |
| waitForVideo('remote-view-3'); |
| waitForVideo('remote-view-4'); |
| } |
| |
| // Do the forwarding after we have received video. |
| detectVideoPlaying('remote-view-1', onRemoteStream1); |
| detectVideoPlaying('remote-view-2', onRemoteStream2); |
| } |
| |
| // First makes a call between pc1 and pc2, and then construct a new media |
| // stream using the remote audio and video tracks, connect the new media |
| // stream to a video element. These operations should not crash Chrome. |
| function ConnectChromiumSinkToRemoteAudioTrack() { |
| createConnections(null); |
| navigator.webkitGetUserMedia({audio: true, video: true}, |
| addStreamToBothConnectionsAndNegotiate, |
| printGetUserMediaError); |
| |
| detectVideoPlaying('remote-view-2', function() { |
| // Construct a new media stream with remote tracks. |
| var newStream = new webkitMediaStream(); |
| newStream.addTrack( |
| gSecondConnection.getRemoteStreams()[0].getAudioTracks()[0]); |
| newStream.addTrack( |
| gSecondConnection.getRemoteStreams()[0].getVideoTracks()[0]); |
| var videoElement = document.createElement('video'); |
| |
| // No crash for this operation. |
| videoElement.src = URL.createObjectURL(newStream); |
| waitForVideo('remote-view-2'); |
| }); |
| } |
| |
| // Test that we can setup call with an audio and video track and |
| // simulate that the remote peer don't support MSID. |
| function callWithoutMsidAndBundle() { |
| createConnections(null); |
| transformSdp = removeBundle; |
| transformRemoteSdp = removeMsid; |
| gTestWithoutMsid = true; |
| navigator.webkitGetUserMedia({audio: true, video: true}, |
| addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| waitForVideo('remote-view-1'); |
| waitForVideo('remote-view-2'); |
| } |
| |
| // Test that we can't setup a call with an unsupported video codec |
| function negotiateUnsupportedVideoCodec() { |
| createConnections(null); |
| transformSdp = removeVideoCodec; |
| |
| onLocalDescriptionError = function(error) { |
| var expectedMsg = 'Failed to set local offer sdp:' + |
| ' Session error code: ERROR_CONTENT. Session error description:' + |
| ' Failed to set video receive codecs..'; |
| assertEquals(expectedMsg, error); |
| reportTestSuccess(); |
| }; |
| navigator.webkitGetUserMedia({audio: true, video: true}, |
| addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| } |
| |
| // Test that we can't setup a call if one peer does not support encryption |
| function negotiateNonCryptoCall() { |
| createConnections(null); |
| transformSdp = removeCrypto; |
| onLocalDescriptionError = function(error) { |
| var expectedMsg = 'Failed to set local offer sdp:' + |
| ' Called with SDP without DTLS fingerprint.'; |
| |
| assertEquals(expectedMsg, error); |
| reportTestSuccess(); |
| }; |
| navigator.webkitGetUserMedia({audio: true, video: true}, |
| addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| } |
| |
| // Test that we can negotiate a call with an SDP offer that includes a |
| // b=AS:XX line to control audio and video bandwidth |
| function negotiateOfferWithBLine() { |
| createConnections(null); |
| transformSdp = addBandwithControl; |
| navigator.webkitGetUserMedia({audio: true, video: true}, |
| addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| waitForVideo('remote-view-1'); |
| waitForVideo('remote-view-2'); |
| } |
| |
| // Test that we can setup call with legacy settings. |
| function callWithLegacySdp() { |
| transformSdp = function(sdp) { |
| return removeBundle(useGice(useExternalSdes(sdp))); |
| }; |
| createConnections({ |
| 'mandatory': {'RtpDataChannels': true, 'DtlsSrtpKeyAgreement': false} |
| }); |
| setupDataChannel({reliable: false}); |
| navigator.webkitGetUserMedia({audio: true, video: true}, |
| addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| waitForVideo('remote-view-1'); |
| waitForVideo('remote-view-2'); |
| } |
| |
| // Test only a data channel. |
| function callWithDataOnly() { |
| createConnections({optional:[{RtpDataChannels: true}]}); |
| setupDataChannel({reliable: false}); |
| negotiate(); |
| } |
| |
| function callWithSctpDataOnly() { |
| createConnections({optional: [{DtlsSrtpKeyAgreement: true}]}); |
| setupSctpDataChannel({reliable: true}); |
| negotiate(); |
| } |
| |
| // Test call with audio, video and a data channel. |
| function callWithDataAndMedia() { |
| createConnections({optional:[{RtpDataChannels: true}]}); |
| setupDataChannel({reliable: false}); |
| navigator.webkitGetUserMedia({audio: true, video: true}, |
| addStreamToBothConnectionsAndNegotiate, |
| printGetUserMediaError); |
| waitForVideo('remote-view-1'); |
| waitForVideo('remote-view-2'); |
| } |
| |
| function callWithSctpDataAndMedia() { |
| createConnections({optional: [{DtlsSrtpKeyAgreement: true}]}); |
| setupSctpDataChannel({reliable: true}); |
| navigator.webkitGetUserMedia({audio: true, video: true}, |
| addStreamToBothConnectionsAndNegotiate, |
| printGetUserMediaError); |
| waitForVideo('remote-view-1'); |
| waitForVideo('remote-view-2'); |
| } |
| |
| |
| // Test call with a data channel and later add audio and video. |
| function callWithDataAndLaterAddMedia() { |
| createConnections({optional:[{RtpDataChannels: true}]}); |
| setupDataChannel({reliable: false}); |
| negotiate(); |
| |
| // Set an event handler for when the data channel has been closed. |
| setAllEventsOccuredHandler(function() { |
| // When the video is flowing the test is done. |
| setAllEventsOccuredHandler(reportTestSuccess); |
| navigator.webkitGetUserMedia({audio: true, video: true}, |
| addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| waitForVideo('remote-view-1'); |
| waitForVideo('remote-view-2'); |
| }); |
| } |
| |
| // Test that we can setup call and send DTMF. |
| function callAndSendDtmf(tones) { |
| createConnections(null); |
| navigator.webkitGetUserMedia({audio: true, video: true}, |
| addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| var onCallEstablished = function() { |
| // Send DTMF tones. |
| var localAudioTrack = gLocalStream.getAudioTracks()[0]; |
| var dtmfSender = gFirstConnection.createDTMFSender(localAudioTrack); |
| dtmfSender.ontonechange = onToneChange; |
| dtmfSender.insertDTMF(tones); |
| // Wait for the DTMF tones callback. |
| addExpectedEvent(); |
| var waitDtmf = setInterval(function() { |
| if (gSentTones == tones) { |
| clearInterval(waitDtmf); |
| eventOccured(); |
| } |
| }, 100); |
| } |
| |
| // Do the DTMF test after we have received video. |
| detectVideoPlaying('remote-view-2', onCallEstablished); |
| } |
| |
| function testCreateOfferOptions() { |
| createConnections(null); |
| var offerOptions = { |
| 'offerToReceiveAudio': false, |
| 'offerToReceiveVideo': true |
| }; |
| |
| gFirstConnection.createOffer( |
| function(offer) { |
| assertEquals(-1, offer.sdp.search('m=audio')); |
| assertNotEquals(-1, offer.sdp.search('m=video')); |
| |
| reportTestSuccess(); |
| }, |
| function(error) { failTest(error); }, |
| offerOptions); |
| } |
| |
| function callAndEnsureAudioIsPlaying(beLenient, constraints) { |
| createConnections(null); |
| |
| // Add the local stream to gFirstConnection to play one-way audio. |
| navigator.webkitGetUserMedia(constraints, |
| addStreamToTheFirstConnectionAndNegotiate, printGetUserMediaError); |
| |
| var onCallEstablished = function() { |
| ensureAudioPlaying(gSecondConnection, beLenient); |
| }; |
| |
| waitForConnectionToStabilize(gFirstConnection, onCallEstablished); |
| } |
| |
| function enableRemoteVideo(peerConnection, enabled) { |
| remoteStream = peerConnection.getRemoteStreams()[0]; |
| remoteStream.getVideoTracks()[0].enabled = enabled; |
| } |
| |
| function enableRemoteAudio(peerConnection, enabled) { |
| remoteStream = peerConnection.getRemoteStreams()[0]; |
| remoteStream.getAudioTracks()[0].enabled = enabled; |
| } |
| |
| function enableLocalVideo(peerConnection, enabled) { |
| localStream = peerConnection.getLocalStreams()[0]; |
| localStream.getVideoTracks()[0].enabled = enabled; |
| } |
| |
| function enableLocalAudio(peerConnection, enabled) { |
| localStream = peerConnection.getLocalStreams()[0]; |
| localStream.getAudioTracks()[0].enabled = enabled; |
| } |
| |
| function callAndEnsureRemoteAudioTrackMutingWorks(beLenient) { |
| callAndEnsureAudioIsPlaying(beLenient, {audio: true, video: true}); |
| setAllEventsOccuredHandler(function() { |
| setAllEventsOccuredHandler(reportTestSuccess); |
| |
| // Call is up, now mute the remote track and check we stop playing out |
| // audio (after a small delay, we don't expect it to happen instantly). |
| enableRemoteAudio(gSecondConnection, false); |
| ensureSilence(gSecondConnection); |
| }); |
| } |
| |
| function callAndEnsureLocalAudioTrackMutingWorks(beLenient) { |
| callAndEnsureAudioIsPlaying(beLenient, {audio: true, video: true}); |
| setAllEventsOccuredHandler(function() { |
| setAllEventsOccuredHandler(reportTestSuccess); |
| |
| // Call is up, now mute the local track of the sending side and ensure |
| // the receiving side stops receiving audio. |
| enableLocalAudio(gFirstConnection, false); |
| ensureSilence(gSecondConnection); |
| }); |
| } |
| |
| function callAndEnsureAudioTrackUnmutingWorks(beLenient) { |
| callAndEnsureAudioIsPlaying(beLenient, {audio: true, video: true}); |
| setAllEventsOccuredHandler(function() { |
| setAllEventsOccuredHandler(reportTestSuccess); |
| |
| // Mute, wait a while, unmute, verify audio gets back up. |
| // (Also, ensure video muting doesn't affect audio). |
| enableRemoteAudio(gSecondConnection, false); |
| enableRemoteVideo(gSecondConnection, false); |
| |
| setTimeout(function() { |
| enableRemoteAudio(gSecondConnection, true); |
| }, 500); |
| |
| setTimeout(function() { |
| ensureAudioPlaying(gSecondConnection, beLenient); |
| }, 1500); |
| }); |
| } |
| |
| function callAndEnsureLocalVideoMutingDoesntMuteAudio(beLenient) { |
| callAndEnsureAudioIsPlaying(beLenient, {audio: true, video: true}); |
| setAllEventsOccuredHandler(function() { |
| setAllEventsOccuredHandler(reportTestSuccess); |
| enableLocalVideo(gFirstConnection, false); |
| ensureAudioPlaying(gSecondConnection, beLenient); |
| }); |
| } |
| |
| function callAndEnsureRemoteVideoMutingDoesntMuteAudio(beLenient) { |
| callAndEnsureAudioIsPlaying(beLenient, {audio: true, video: true}); |
| setAllEventsOccuredHandler(function() { |
| setAllEventsOccuredHandler(reportTestSuccess); |
| enableRemoteVideo(gSecondConnection, false); |
| ensureAudioPlaying(gSecondConnection, beLenient); |
| }); |
| } |
| |
| function callAndEnsureVideoTrackMutingWorks() { |
| createConnections(null); |
| navigator.webkitGetUserMedia({audio: true, video: true}, |
| addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| |
| addExpectedEvent(); |
| detectVideoPlaying('remote-view-2', function() { |
| // Disable the receiver's remote media stream. Video should stop. |
| // (Also, ensure muting audio doesn't affect video). |
| enableRemoteVideo(gSecondConnection, false); |
| enableRemoteAudio(gSecondConnection, false); |
| |
| detectVideoStopped('remote-view-2', function() { |
| // Video has stopped: unmute and succeed if it starts playing again. |
| enableRemoteVideo(gSecondConnection, true); |
| detectVideoPlaying('remote-view-2', eventOccured); |
| }) |
| }); |
| } |
| |
| // Test call with a new Video MediaStream that has been created based on a |
| // stream generated by getUserMedia. |
| function callWithNewVideoMediaStream() { |
| createConnections(null); |
| navigator.webkitGetUserMedia({audio: true, video: true}, |
| createNewVideoStreamAndAddToBothConnections, printGetUserMediaError); |
| waitForVideo('remote-view-1'); |
| waitForVideo('remote-view-2'); |
| } |
| |
| // Test call with a new Video MediaStream that has been created based on a |
| // stream generated by getUserMedia. When Video is flowing, an audio track |
| // is added to the sent stream and the video track is removed. This |
| // is to test that adding and removing of remote tracks on an existing |
| // mediastream works. |
| function callWithNewVideoMediaStreamLaterSwitchToAudio() { |
| createConnections(null); |
| navigator.webkitGetUserMedia({audio: true, video: true}, |
| createNewVideoStreamAndAddToBothConnections, printGetUserMediaError); |
| |
| waitForVideo('remote-view-1'); |
| waitForVideo('remote-view-2'); |
| |
| // Set an event handler for when video is playing. |
| setAllEventsOccuredHandler(function() { |
| // Add an audio track to the local stream and remove the video track and |
| // then renegotiate. But first - setup the expectations. |
| local_stream = gFirstConnection.getLocalStreams()[0]; |
| |
| remote_stream_1 = gFirstConnection.getRemoteStreams()[0]; |
| // Add an expected event that onaddtrack will be called on the remote |
| // mediastream received on gFirstConnection when the audio track is |
| // received. |
| addExpectedEvent(); |
| remote_stream_1.onaddtrack = function(){ |
| assertEquals(remote_stream_1.getAudioTracks()[0].id, |
| local_stream.getAudioTracks()[0].id); |
| eventOccured(); |
| } |
| |
| // Add an expectation that the received video track is removed from |
| // gFirstConnection. |
| addExpectedEvent(); |
| remote_stream_1.onremovetrack = function() { |
| eventOccured(); |
| } |
| |
| // Add an expected event that onaddtrack will be called on the remote |
| // mediastream received on gSecondConnection when the audio track is |
| // received. |
| remote_stream_2 = gSecondConnection.getRemoteStreams()[0]; |
| addExpectedEvent(); |
| remote_stream_2.onaddtrack = function() { |
| assertEquals(remote_stream_2.getAudioTracks()[0].id, |
| local_stream.getAudioTracks()[0].id); |
| eventOccured(); |
| } |
| |
| // Add an expectation that the received video track is removed from |
| // gSecondConnection. |
| addExpectedEvent(); |
| remote_stream_2.onremovetrack = function() { |
| eventOccured(); |
| } |
| // When all the above events have occurred- the test pass. |
| setAllEventsOccuredHandler(reportTestSuccess); |
| |
| local_stream.addTrack(gLocalStream.getAudioTracks()[0]); |
| local_stream.removeTrack(local_stream.getVideoTracks()[0]); |
| negotiate(); |
| }); |
| } |
| |
| // This function is used for setting up a test that: |
| // 1. Creates a data channel on |gFirstConnection| and sends data to |
| // |gSecondConnection|. |
| // 2. When data is received on |gSecondConnection| a message |
| // is sent to |gFirstConnection|. |
| // 3. When data is received on |gFirstConnection|, the data |
| // channel is closed. The test passes when the state transition completes. |
| function setupDataChannel(params) { |
| var sendDataString = "send some text on a data channel." |
| firstDataChannel = gFirstConnection.createDataChannel( |
| "sendDataChannel", params); |
| assertEquals('connecting', firstDataChannel.readyState); |
| |
| // When |firstDataChannel| transition to open state, send a text string. |
| firstDataChannel.onopen = function() { |
| assertEquals('open', firstDataChannel.readyState); |
| if (firstDataChannel.reliable) { |
| firstDataChannel.send(sendDataString); |
| } else { |
| sendDataRepeatedlyUntilClosed(firstDataChannel); |
| } |
| } |
| |
| // When |firstDataChannel| receive a message, close the channel and |
| // initiate a new offer/answer exchange to complete the closure. |
| firstDataChannel.onmessage = function(event) { |
| assertEquals(event.data, sendDataString); |
| firstDataChannel.close(); |
| negotiate(); |
| } |
| |
| // When |firstDataChannel| transition to closed state, the test pass. |
| addExpectedEvent(); |
| firstDataChannel.onclose = function() { |
| assertEquals('closed', firstDataChannel.readyState); |
| eventOccured(); |
| } |
| |
| // Event handler for when |gSecondConnection| receive a new dataChannel. |
| gSecondConnection.ondatachannel = function (event) { |
| var secondDataChannel = event.channel; |
| |
| // When |secondDataChannel| receive a message, send a message back. |
| secondDataChannel.onmessage = function(event) { |
| assertEquals(event.data, sendDataString); |
| console.log("gSecondConnection received data"); |
| if (secondDataChannel.reliable) { |
| // If we're reliable we will just send one message over the channel, |
| // and therefore channel one's message handler cannot have shut us |
| // down already. |
| assertEquals('open', secondDataChannel.readyState); |
| secondDataChannel.send(sendDataString); |
| } else { |
| // If unreliable, this could be one in a series of messages and it |
| // is possible we already replied (which will close our channel). |
| sendDataRepeatedlyUntilClosed(secondDataChannel); |
| } |
| } |
| } |
| |
| // Sends |sendDataString| on |dataChannel| every 200ms as long as |
| // |dataChannel| is open. |
| function sendDataRepeatedlyUntilClosed(dataChannel) { |
| var sendTimer = setInterval(function() { |
| if (dataChannel.readyState == 'open') |
| dataChannel.send(sendDataString); |
| else |
| clearInterval(sendTimer); |
| }, 200); |
| } |
| } |
| |
| // SCTP data channel setup is slightly different then RTP based |
| // channels. Due to a bug in libjingle, we can't send data immediately |
| // after channel becomes open. So for that reason in SCTP, |
| // we are sending data from second channel, when ondatachannel event is |
| // received. So data flow happens 2 -> 1 -> 2. |
| function setupSctpDataChannel(params) { |
| var sendDataString = "send some text on a data channel." |
| firstDataChannel = gFirstConnection.createDataChannel( |
| "sendDataChannel", params); |
| assertEquals('connecting', firstDataChannel.readyState); |
| |
| // When |firstDataChannel| transition to open state, send a text string. |
| firstDataChannel.onopen = function() { |
| assertEquals('open', firstDataChannel.readyState); |
| } |
| |
| // When |firstDataChannel| receive a message, send message back. |
| // initiate a new offer/answer exchange to complete the closure. |
| firstDataChannel.onmessage = function(event) { |
| assertEquals('open', firstDataChannel.readyState); |
| assertEquals(event.data, sendDataString); |
| firstDataChannel.send(sendDataString); |
| } |
| |
| |
| // Event handler for when |gSecondConnection| receive a new dataChannel. |
| gSecondConnection.ondatachannel = function (event) { |
| var secondDataChannel = event.channel; |
| secondDataChannel.onopen = function() { |
| secondDataChannel.send(sendDataString); |
| } |
| |
| // When |secondDataChannel| receive a message, close the channel and |
| // initiate a new offer/answer exchange to complete the closure. |
| secondDataChannel.onmessage = function(event) { |
| assertEquals(event.data, sendDataString); |
| assertEquals('open', secondDataChannel.readyState); |
| secondDataChannel.close(); |
| negotiate(); |
| } |
| |
| // When |secondDataChannel| transition to closed state, the test pass. |
| addExpectedEvent(); |
| secondDataChannel.onclose = function() { |
| assertEquals('closed', secondDataChannel.readyState); |
| eventOccured(); |
| } |
| } |
| } |
| |
| // Test call with a stream that has been created by getUserMedia, clone |
| // the stream to a cloned stream, send them via the same peer connection. |
| function addTwoMediaStreamsToOneConnection() { |
| createConnections(null); |
| navigator.webkitGetUserMedia({audio: true, video: true}, |
| CloneStreamAndAddTwoStreamstoOneConnection, printGetUserMediaError); |
| } |
| |
| function onToneChange(tone) { |
| gSentTones += tone.tone; |
| } |
| |
| function createConnections(constraints) { |
| gFirstConnection = createConnection(constraints, 'remote-view-1'); |
| assertEquals('stable', gFirstConnection.signalingState); |
| |
| gSecondConnection = createConnection(constraints, 'remote-view-2'); |
| assertEquals('stable', gSecondConnection.signalingState); |
| } |
| |
| function createConnection(constraints, remoteView) { |
| var pc = new webkitRTCPeerConnection(null, constraints); |
| pc.onaddstream = function(event) { |
| onRemoteStream(event, remoteView); |
| } |
| return pc; |
| } |
| |
| function displayAndRemember(localStream) { |
| var localStreamUrl = URL.createObjectURL(localStream); |
| $('local-view').src = localStreamUrl; |
| |
| gLocalStream = localStream; |
| } |
| |
| // Called if getUserMedia fails. |
| function printGetUserMediaError(error) { |
| var message = 'getUserMedia request unexpectedly failed:'; |
| if (error.constraintName) |
| message += ' could not satisfy constraint ' + error.constraintName; |
| else |
| message += ' devices not working/user denied access.'; |
| failTest(message); |
| } |
| |
| // Called if getUserMedia succeeds and we want to send from both connections. |
| function addStreamToBothConnectionsAndNegotiate(localStream) { |
| displayAndRemember(localStream); |
| gFirstConnection.addStream(localStream); |
| gSecondConnection.addStream(localStream); |
| negotiate(); |
| } |
| |
| // Called if getUserMedia succeeds when we want to send from one connection. |
| function addStreamToTheFirstConnectionAndNegotiate(localStream) { |
| displayAndRemember(localStream); |
| gFirstConnection.addStream(localStream); |
| negotiate(); |
| } |
| |
| function verifyHasOneAudioAndVideoTrack(stream) { |
| assertEquals(1, stream.getAudioTracks().length); |
| assertEquals(1, stream.getVideoTracks().length); |
| } |
| |
| // Called if getUserMedia succeeds, then clone the stream, send two streams |
| // from one peer connection. |
| function CloneStreamAndAddTwoStreamstoOneConnection(localStream) { |
| displayAndRemember(localStream); |
| |
| var clonedStream = null; |
| if (typeof localStream.clone === "function") { |
| clonedStream = localStream.clone(); |
| } else { |
| clonedStream = new webkitMediaStream(localStream); |
| } |
| |
| gFirstConnection.addStream(localStream); |
| gFirstConnection.addStream(clonedStream); |
| |
| // Verify the local streams are correct. |
| assertEquals(2, gFirstConnection.getLocalStreams().length); |
| verifyHasOneAudioAndVideoTrack(gFirstConnection.getLocalStreams()[0]); |
| verifyHasOneAudioAndVideoTrack(gFirstConnection.getLocalStreams()[1]); |
| |
| // The remote side should receive two streams. After that, verify the |
| // remote side has the correct number of streams and tracks. |
| addExpectedEvent(); |
| addExpectedEvent(); |
| gSecondConnection.onaddstream = function(event) { |
| eventOccured(); |
| } |
| setAllEventsOccuredHandler(function() { |
| // Negotiation complete, verify remote streams on the receiving side. |
| assertEquals(2, gSecondConnection.getRemoteStreams().length); |
| verifyHasOneAudioAndVideoTrack(gSecondConnection.getRemoteStreams()[0]); |
| verifyHasOneAudioAndVideoTrack(gSecondConnection.getRemoteStreams()[1]); |
| |
| reportTestSuccess(); |
| }); |
| |
| negotiate(); |
| } |
| |
| // Called if getUserMedia succeeds when we want to send a modified |
| // MediaStream. A new MediaStream is created and the video track from |
| // |localStream| is added. |
| function createNewVideoStreamAndAddToBothConnections(localStream) { |
| displayAndRemember(localStream); |
| var new_stream = new webkitMediaStream(); |
| new_stream.addTrack(localStream.getVideoTracks()[0]); |
| gFirstConnection.addStream(new_stream); |
| gSecondConnection.addStream(new_stream); |
| negotiate(); |
| } |
| |
| function negotiate() { |
| negotiateBetween(gFirstConnection, gSecondConnection); |
| } |
| |
| function negotiateBetween(caller, callee) { |
| console.log("Negotiating call..."); |
| // Not stable = negotiation is ongoing. The behavior of re-negotiating while |
| // a negotiation is ongoing is more or less undefined, so avoid this. |
| if (caller.signalingState != 'stable' || callee.signalingState != 'stable') |
| throw 'You can only negotiate when the connection is stable!'; |
| |
| connectOnIceCandidate(caller, callee); |
| |
| caller.createOffer( |
| function (offer) { |
| onOfferCreated(offer, caller, callee); |
| }); |
| } |
| |
| function onOfferCreated(offer, caller, callee) { |
| offer.sdp = maybeForceIsac16K(transformSdp(offer.sdp)); |
| caller.setLocalDescription(offer, function() { |
| assertEquals('have-local-offer', caller.signalingState); |
| receiveOffer(offer.sdp, caller, callee); |
| }, onLocalDescriptionError); |
| } |
| |
| function receiveOffer(offerSdp, caller, callee) { |
| console.log("Receiving offer..."); |
| offerSdp = transformRemoteSdp(offerSdp); |
| |
| var parsedOffer = new RTCSessionDescription({ type: 'offer', |
| sdp: offerSdp }); |
| callee.setRemoteDescription(parsedOffer, function() {}, |
| onRemoteDescriptionError); |
| callee.createAnswer(function (answer) { |
| onAnswerCreated(answer, caller, callee); |
| }); |
| assertEquals('have-remote-offer', callee.signalingState); |
| } |
| |
| function removeMsid(offerSdp) { |
| offerSdp = offerSdp.replace(/a=msid-semantic.*\r\n/g, ''); |
| offerSdp = offerSdp.replace('a=mid:audio\r\n', ''); |
| offerSdp = offerSdp.replace('a=mid:video\r\n', ''); |
| offerSdp = offerSdp.replace(/a=ssrc.*\r\n/g, ''); |
| return offerSdp; |
| } |
| |
| function removeVideoCodec(offerSdp) { |
| offerSdp = offerSdp.replace('a=rtpmap:100 VP8/90000\r\n', |
| 'a=rtpmap:100 XVP8/90000\r\n'); |
| return offerSdp; |
| } |
| |
| function removeCrypto(offerSdp) { |
| offerSdp = offerSdp.replace(/a=crypto.*\r\n/g, 'a=Xcrypto\r\n'); |
| offerSdp = offerSdp.replace(/a=fingerprint.*\r\n/g, ''); |
| return offerSdp; |
| } |
| |
| function addBandwithControl(offerSdp) { |
| offerSdp = offerSdp.replace('a=mid:audio\r\n', 'a=mid:audio\r\n'+ |
| 'b=AS:16\r\n'); |
| offerSdp = offerSdp.replace('a=mid:video\r\n', 'a=mid:video\r\n'+ |
| 'b=AS:512\r\n'); |
| return offerSdp; |
| } |
| |
| function removeBundle(sdp) { |
| return sdp.replace(/a=group:BUNDLE .*\r\n/g, ''); |
| } |
| |
| function useGice(sdp) { |
| sdp = sdp.replace(/t=.*\r\n/g, function(subString) { |
| return subString + 'a=ice-options:google-ice\r\n'; |
| }); |
| return sdp; |
| } |
| |
| function useExternalSdes(sdp) { |
| // Remove current crypto specification. |
| sdp = sdp.replace(/a=crypto.*\r\n/g, ''); |
| sdp = sdp.replace(/a=fingerprint.*\r\n/g, ''); |
| // Add external crypto. This is not compatible with |removeMsid|. |
| sdp = sdp.replace(/a=mid:(\w+)\r\n/g, function(subString, group) { |
| return subString + EXTERNAL_SDES_LINES[group] + '\r\n'; |
| }); |
| return sdp; |
| } |
| |
| function onAnswerCreated(answer, caller, callee) { |
| answer.sdp = maybeForceIsac16K(transformSdp(answer.sdp)); |
| callee.setLocalDescription(answer, |
| function () { |
| assertEquals('stable', callee.signalingState); |
| }, |
| onLocalDescriptionError); |
| receiveAnswer(answer.sdp, caller); |
| } |
| |
| function receiveAnswer(answerSdp, caller) { |
| console.log("Receiving answer..."); |
| answerSdp = transformRemoteSdp(answerSdp); |
| var parsedAnswer = new RTCSessionDescription({ type: 'answer', |
| sdp: answerSdp }); |
| caller.setRemoteDescription(parsedAnswer, |
| function() { |
| assertEquals('stable', caller.signalingState); |
| }, |
| onRemoteDescriptionError); |
| } |
| |
| function connectOnIceCandidate(caller, callee) { |
| caller.onicecandidate = function(event) { onIceCandidate(event, callee); } |
| callee.onicecandidate = function(event) { onIceCandidate(event, caller); } |
| } |
| |
| function onIceCandidate(event, target) { |
| if (event.candidate) { |
| var candidate = new RTCIceCandidate(event.candidate); |
| target.addIceCandidate(candidate); |
| } |
| } |
| |
| function onRemoteStream(e, target) { |
| console.log("Receiving remote stream..."); |
| if (gTestWithoutMsid && e.stream.id != "default") { |
| failTest('a default remote stream was expected but instead ' + |
| e.stream.id + ' was received.'); |
| } |
| gRemoteStreams[target] = e.stream; |
| var remoteStreamUrl = URL.createObjectURL(e.stream); |
| var remoteVideo = $(target); |
| remoteVideo.src = remoteStreamUrl; |
| } |
| |
| </script> |
| </head> |
| <body> |
| <table border="0"> |
| <tr> |
| <td><video width="320" height="240" id="local-view" style="display:none" |
| autoplay muted></video></td> |
| <td><video width="320" height="240" id="remote-view-1" |
| style="display:none" autoplay></video></td> |
| <td><video width="320" height="240" id="remote-view-2" |
| style="display:none" autoplay></video></td> |
| <td><video width="320" height="240" id="remote-view-3" |
| style="display:none" autoplay></video></td> |
| <td><video width="320" height="240" id="remote-view-4" |
| style="display:none" autoplay></video></td> |
| <!-- Canvases are named after their corresponding video elements. --> |
| <td><canvas width="320" height="240" id="remote-view-1-canvas" |
| style="display:none"></canvas></td> |
| <td><canvas width="320" height="240" id="remote-view-2-canvas" |
| style="display:none"></canvas></td> |
| <td><canvas width="320" height="240" id="remote-view-3-canvas" |
| style="display:none"></canvas></td> |
| <td><canvas width="320" height="240" id="remote-view-4-canvas" |
| style="display:none"></canvas></td> |
| </tr> |
| </table> |
| </body> |
| </html> |