Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1 | <html> |
| 2 | <head> |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 3 | <script type="text/javascript" src="webrtc_test_utilities.js"></script> |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 4 | <script type="text/javascript"> |
| 5 | $ = function(id) { |
| 6 | return document.getElementById(id); |
| 7 | }; |
| 8 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 9 | var gFirstConnection = null; |
| 10 | var gSecondConnection = null; |
| 11 | var gTestWithoutMsidAndBundle = false; |
| 12 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 13 | var gLocalStream = null; |
| 14 | var gSentTones = ''; |
| 15 | |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 16 | setAllEventsOccuredHandler(function() { |
| 17 | document.title = 'OK'; |
| 18 | }); |
| 19 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 20 | // Test that we can setup call with an audio and video track. |
| 21 | function call(constraints) { |
| 22 | createConnections(null); |
| 23 | navigator.webkitGetUserMedia(constraints, |
| 24 | addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| 25 | waitForVideo('remote-view-1'); |
| 26 | waitForVideo('remote-view-2'); |
| 27 | } |
| 28 | |
| 29 | // First calls without streams on any connections, and then adds a stream |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 30 | // to peer connection 1 which gets sent to peer connection 2. We must wait |
| 31 | // for the first negotiation to complete before starting the second one, which |
| 32 | // is why we wait until the connection is stable before re-negotiating. |
| 33 | function callEmptyThenAddOneStreamAndRenegotiate(constraints) { |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 34 | createConnections(null); |
| 35 | negotiate(); |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 36 | waitForConnectionToStabilize(gFirstConnection); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 37 | navigator.webkitGetUserMedia(constraints, |
| 38 | addStreamToTheFirstConnectionAndNegotiate, printGetUserMediaError); |
| 39 | // Only the first connection is sending here. |
| 40 | waitForVideo('remote-view-2'); |
| 41 | } |
| 42 | |
| 43 | // Test that we can setup call with an audio and video track and |
| 44 | // simulate that the remote peer don't support MSID. |
| 45 | function callWithoutMsidAndBundle() { |
| 46 | createConnections(null); |
| 47 | gTestWithoutMsidAndBundle = true; |
| 48 | navigator.webkitGetUserMedia({audio:true, video:true}, |
| 49 | addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 50 | waitForVideo('remote-view-1'); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 51 | waitForVideo('remote-view-2'); |
| 52 | } |
| 53 | |
| 54 | // Test only a data channel. |
| 55 | function callWithDataOnly() { |
| 56 | createConnections({optional:[{RtpDataChannels: true}]}); |
| 57 | setupDataChannel(); |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 58 | negotiate(); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 59 | } |
| 60 | |
| 61 | // Test call with audio, video and a data channel. |
| 62 | function callWithDataAndMedia() { |
| 63 | createConnections({optional:[{RtpDataChannels: true}]}); |
| 64 | setupDataChannel(); |
| 65 | navigator.webkitGetUserMedia({audio:true, video:true}, |
| 66 | addStreamToBothConnectionsAndNegotiate, |
| 67 | printGetUserMediaError); |
| 68 | waitForVideo('remote-view-1'); |
| 69 | waitForVideo('remote-view-2'); |
| 70 | } |
| 71 | |
| 72 | // Test call with a data channel and later add audio and video. |
| 73 | function callWithDataAndLaterAddMedia() { |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 74 | createConnections({optional:[{RtpDataChannels: true}]}); |
| 75 | setupDataChannel(); |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 76 | negotiate(); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 77 | |
Torne (Richard Coles) | a93a17c | 2013-05-15 11:34:50 +0100 | [diff] [blame] | 78 | // Set an event handler for when the data channel has been closed. |
| 79 | setAllEventsOccuredHandler(function() { |
| 80 | // When the video is flowing the test is done. |
| 81 | setAllEventsOccuredHandler(function() { |
| 82 | document.title = 'OK'; |
| 83 | }); |
| 84 | navigator.webkitGetUserMedia({audio:true, video:true}, |
| 85 | addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| 86 | waitForVideo('remote-view-1'); |
| 87 | waitForVideo('remote-view-2'); |
| 88 | }); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 89 | } |
| 90 | |
| 91 | // Test that we can setup call and send DTMF. |
| 92 | function callAndSendDtmf(tones) { |
| 93 | createConnections(null); |
| 94 | navigator.webkitGetUserMedia({audio:true, video:true}, |
| 95 | addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| 96 | var onCallEstablished = function() { |
| 97 | // Send DTMF tones. |
| 98 | var localAudioTrack = gLocalStream.getAudioTracks()[0]; |
| 99 | var dtmfSender = gFirstConnection.createDTMFSender(localAudioTrack); |
| 100 | dtmfSender.ontonechange = onToneChange; |
| 101 | dtmfSender.insertDTMF(tones); |
| 102 | // Wait for the DTMF tones callback. |
| 103 | document.title = 'Waiting for dtmf...'; |
| 104 | addExpectedEvent(); |
| 105 | var waitDtmf = setInterval(function() { |
| 106 | if (gSentTones == tones) { |
| 107 | clearInterval(waitDtmf); |
| 108 | eventOccured(); |
| 109 | } |
| 110 | }, 100); |
| 111 | } |
| 112 | |
| 113 | // Do the DTMF test after we have received video. |
| 114 | detectVideoIn('remote-view-2', onCallEstablished); |
| 115 | } |
| 116 | |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 117 | // Test call with a new Video MediaStream that has been created based on a |
| 118 | // stream generated by getUserMedia. |
| 119 | function callWithNewVideoMediaStream() { |
| 120 | createConnections(null); |
| 121 | navigator.webkitGetUserMedia({audio:true, video:true}, |
| 122 | createNewVideoStreamAndAddToBothConnections, printGetUserMediaError); |
| 123 | waitForVideo('remote-view-1'); |
| 124 | waitForVideo('remote-view-2'); |
| 125 | } |
| 126 | |
| 127 | // Test call with a new Video MediaStream that has been created based on a |
| 128 | // stream generated by getUserMedia. When Video is flowing, an audio track |
| 129 | // is added to the sent stream and the video track is removed. This |
| 130 | // is to test that adding and removing of remote tracks on an existing |
| 131 | // mediastream works. |
| 132 | function callWithNewVideoMediaStreamLaterSwitchToAudio() { |
| 133 | createConnections(null); |
| 134 | navigator.webkitGetUserMedia({audio:true, video:true}, |
| 135 | createNewVideoStreamAndAddToBothConnections, printGetUserMediaError); |
| 136 | |
| 137 | waitForVideo('remote-view-1'); |
| 138 | waitForVideo('remote-view-2'); |
| 139 | |
| 140 | // Set an event handler for when video is playing. |
| 141 | setAllEventsOccuredHandler(function() { |
| 142 | // Add an audio track to the local stream and remove the video track and |
| 143 | // then renegotiate. But first - setup the expectations. |
| 144 | local_stream = gFirstConnection.getLocalStreams()[0]; |
| 145 | |
| 146 | remote_stream_1 = gFirstConnection.getRemoteStreams()[0]; |
| 147 | // Add an expected event that onaddtrack will be called on the remote |
| 148 | // mediastream received on gFirstConnection when the audio track is |
| 149 | // received. |
| 150 | addExpectedEvent(); |
| 151 | remote_stream_1.onaddtrack = function(){ |
| 152 | expectEquals(remote_stream_1.getAudioTracks()[0].id, |
| 153 | local_stream.getAudioTracks()[0].id); |
| 154 | eventOccured(); |
| 155 | } |
| 156 | |
| 157 | // Add an expectation that the received video track is removed from |
| 158 | // gFirstConnection. |
| 159 | addExpectedEvent(); |
| 160 | remote_stream_1.onremovetrack = function() { |
| 161 | eventOccured(); |
| 162 | } |
| 163 | |
| 164 | // Add an expected event that onaddtrack will be called on the remote |
| 165 | // mediastream received on gSecondConnection when the audio track is |
| 166 | // received. |
| 167 | remote_stream_2 = gSecondConnection.getRemoteStreams()[0]; |
| 168 | addExpectedEvent(); |
| 169 | remote_stream_2.onaddtrack = function() { |
| 170 | expectEquals(remote_stream_2.getAudioTracks()[0].id, |
| 171 | local_stream.getAudioTracks()[0].id); |
| 172 | eventOccured(); |
| 173 | } |
| 174 | |
| 175 | // Add an expectation that the received video track is removed from |
| 176 | // gSecondConnection. |
| 177 | addExpectedEvent(); |
| 178 | remote_stream_2.onremovetrack = function() { |
| 179 | eventOccured(); |
| 180 | } |
| 181 | // When all the above events have occurred- the test pass. |
| 182 | setAllEventsOccuredHandler(function() { document.title = 'OK'; }); |
| 183 | |
| 184 | local_stream.addTrack(gLocalStream.getAudioTracks()[0]); |
| 185 | local_stream.removeTrack(local_stream.getVideoTracks()[0]); |
| 186 | negotiate(); |
| 187 | }); // End of setAllEventsOccuredHandler. |
| 188 | } |
| 189 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 190 | // This function is used for setting up a test that: |
| 191 | // 1. Creates a data channel on |gFirstConnection| and sends data to |
| 192 | // |gSecondConnection|. |
| 193 | // 2. When data is received on |gSecondConnection| a message |
| 194 | // is sent to |gFirstConnection|. |
| 195 | // 3. When data is received on |gFirstConnection|, the data |
| 196 | // channel is closed. The test passes when the state transition completes. |
| 197 | function setupDataChannel() { |
| 198 | var sendDataString = "send some text on a data channel." |
| 199 | firstDataChannel = gFirstConnection.createDataChannel( |
| 200 | "sendDataChannel", {reliable : false}); |
| 201 | expectEquals('connecting', firstDataChannel.readyState); |
| 202 | |
| 203 | // When |firstDataChannel| transition to open state, send a text string. |
| 204 | firstDataChannel.onopen = function() { |
| 205 | expectEquals('open', firstDataChannel.readyState); |
| 206 | firstDataChannel.send(sendDataString); |
| 207 | } |
| 208 | |
| 209 | // When |firstDataChannel| receive a message, close the channel and |
| 210 | // initiate a new offer/answer exchange to complete the closure. |
| 211 | firstDataChannel.onmessage = function(event) { |
| 212 | expectEquals(event.data, sendDataString); |
| 213 | firstDataChannel.close(); |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 214 | negotiate(); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 215 | } |
| 216 | |
| 217 | // When |firstDataChannel| transition to closed state, the test pass. |
| 218 | addExpectedEvent(); |
| 219 | firstDataChannel.onclose = function() { |
| 220 | expectEquals('closed', firstDataChannel.readyState); |
| 221 | eventOccured(); |
| 222 | } |
| 223 | |
| 224 | // Event handler for when |gSecondConnection| receive a new dataChannel. |
| 225 | gSecondConnection.ondatachannel = function (event) { |
| 226 | var secondDataChannel = event.channel; |
| 227 | |
| 228 | // When |secondDataChannel| receive a message, send a message back. |
| 229 | secondDataChannel.onmessage = function(event) { |
| 230 | expectEquals(event.data, sendDataString); |
| 231 | expectEquals('open', secondDataChannel.readyState); |
| 232 | secondDataChannel.send(sendDataString); |
| 233 | } |
| 234 | } |
| 235 | } |
| 236 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 237 | // Test call with a stream that has been created by getUserMedia, clone |
| 238 | // the stream to a cloned stream, send them via the same peer connection. |
| 239 | function addTwoMediaStreamsToOneConnection() { |
| 240 | createConnections(null); |
| 241 | navigator.webkitGetUserMedia({audio:true, video:true}, |
| 242 | CloneStreamAndAddTwoStreamstoOneConnection, printGetUserMediaError); |
| 243 | } |
| 244 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 245 | function onToneChange(tone) { |
| 246 | gSentTones += tone.tone; |
| 247 | document.title = gSentTones; |
| 248 | } |
| 249 | |
| 250 | function createConnections(constraints) { |
| 251 | gFirstConnection = new webkitRTCPeerConnection(null, constraints); |
| 252 | gFirstConnection.onicecandidate = onIceCandidateToFirst; |
| 253 | gFirstConnection.onaddstream = function(event) { |
| 254 | onRemoteStream(event, 'remote-view-1'); |
| 255 | } |
| 256 | expectEquals('stable', gFirstConnection.signalingState); |
| 257 | |
| 258 | gSecondConnection = new webkitRTCPeerConnection(null, constraints); |
| 259 | gSecondConnection.onicecandidate = onIceCandidateToSecond; |
| 260 | gSecondConnection.onaddstream = function(event) { |
| 261 | onRemoteStream(event, 'remote-view-2'); |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | function displayAndRemember(localStream) { |
| 266 | var localStreamUrl = webkitURL.createObjectURL(localStream); |
| 267 | $('local-view').src = localStreamUrl; |
| 268 | |
| 269 | gLocalStream = localStream; |
| 270 | } |
| 271 | |
| 272 | // Called if getUserMedia fails. |
| 273 | function printGetUserMediaError(error) { |
| 274 | document.title = 'getUserMedia request failed with code ' + error.code; |
| 275 | } |
| 276 | |
| 277 | // Called if getUserMedia succeeds and we want to send from both connections. |
| 278 | function addStreamToBothConnectionsAndNegotiate(localStream) { |
| 279 | displayAndRemember(localStream); |
| 280 | gFirstConnection.addStream(localStream); |
| 281 | gSecondConnection.addStream(localStream); |
| 282 | negotiate(); |
| 283 | } |
| 284 | |
| 285 | // Called if getUserMedia succeeds when we want to send from one connection. |
| 286 | function addStreamToTheFirstConnectionAndNegotiate(localStream) { |
| 287 | displayAndRemember(localStream); |
| 288 | gFirstConnection.addStream(localStream); |
| 289 | negotiate(); |
| 290 | } |
| 291 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 292 | function verifyHasOneAudioAndVideoTrack(stream) { |
| 293 | expectEquals(1, stream.getAudioTracks().length); |
| 294 | expectEquals(1, stream.getVideoTracks().length); |
| 295 | } |
| 296 | |
| 297 | // Called if getUserMedia succeeds, then clone the stream, send two streams |
| 298 | // from one peer connection. |
| 299 | function CloneStreamAndAddTwoStreamstoOneConnection(localStream) { |
| 300 | displayAndRemember(localStream); |
| 301 | var clonedStream = new webkitMediaStream(); |
| 302 | clonedStream.addTrack(localStream.getVideoTracks()[0]); |
| 303 | clonedStream.addTrack(localStream.getAudioTracks()[0]); |
| 304 | gFirstConnection.addStream(localStream); |
| 305 | gFirstConnection.addStream(clonedStream); |
| 306 | |
| 307 | // Verify the local streams are correct. |
| 308 | expectEquals(2, gFirstConnection.getLocalStreams().length); |
| 309 | verifyHasOneAudioAndVideoTrack(gFirstConnection.getLocalStreams()[0]); |
| 310 | verifyHasOneAudioAndVideoTrack(gFirstConnection.getLocalStreams()[1]); |
| 311 | |
| 312 | // The remote side should receive two streams. After that, verify the |
| 313 | // remote side has the correct number of streams and tracks. |
| 314 | addExpectedEvent(); |
| 315 | addExpectedEvent(); |
| 316 | gSecondConnection.onaddstream = function(event) { |
| 317 | eventOccured(); |
| 318 | } |
| 319 | setAllEventsOccuredHandler(function() { |
| 320 | // Negotiation complete, verify remote streams on the receiving side. |
| 321 | expectEquals(2, gSecondConnection.getRemoteStreams().length); |
| 322 | verifyHasOneAudioAndVideoTrack(gSecondConnection.getRemoteStreams()[0]); |
| 323 | verifyHasOneAudioAndVideoTrack(gSecondConnection.getRemoteStreams()[1]); |
| 324 | |
| 325 | document.title = "OK"; |
| 326 | }); |
| 327 | |
| 328 | negotiate(); |
| 329 | } |
| 330 | |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 331 | // Called if getUserMedia succeeds when we want to send a modified |
| 332 | // MediaStream. A new MediaStream is created and the video track from |
| 333 | // |localStream| is added. |
| 334 | function createNewVideoStreamAndAddToBothConnections(localStream) { |
| 335 | displayAndRemember(localStream); |
| 336 | var new_stream = new webkitMediaStream(); |
| 337 | new_stream.addTrack(localStream.getVideoTracks()[0]); |
| 338 | gFirstConnection.addStream(new_stream); |
| 339 | gSecondConnection.addStream(new_stream); |
| 340 | negotiate(); |
| 341 | } |
| 342 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 343 | function negotiate() { |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 344 | // Not stable = negotiation is ongoing. The behavior of re-negotiating while |
| 345 | // a negotiation is ongoing is more or less undefined, so avoid this. |
| 346 | if (gFirstConnection.signalingState != 'stable') |
| 347 | throw 'You can only negotiate when the connection is stable!'; |
| 348 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 349 | gFirstConnection.createOffer(onOfferCreated); |
| 350 | } |
| 351 | |
| 352 | function onOfferCreated(offer) { |
| 353 | gFirstConnection.setLocalDescription(offer); |
| 354 | expectEquals('have-local-offer', gFirstConnection.signalingState); |
| 355 | receiveOffer(offer.sdp); |
| 356 | } |
| 357 | |
| 358 | function receiveOffer(offerSdp) { |
| 359 | if (gTestWithoutMsidAndBundle) { |
| 360 | offerSdp = removeMsidAndBundle(offerSdp); |
| 361 | } |
| 362 | |
| 363 | var parsedOffer = new RTCSessionDescription({ type: 'offer', |
| 364 | sdp: offerSdp }); |
| 365 | gSecondConnection.setRemoteDescription(parsedOffer); |
| 366 | gSecondConnection.createAnswer(onAnswerCreated); |
| 367 | expectEquals('have-remote-offer', gSecondConnection.signalingState); |
| 368 | } |
| 369 | |
| 370 | function removeMsidAndBundle(offerSdp) { |
| 371 | offerSdp = offerSdp.replace(/a=msid-semantic.*\r\n/g, ''); |
| 372 | offerSdp = offerSdp.replace('a=group:BUNDLE audio video\r\n', ''); |
| 373 | offerSdp = offerSdp.replace('a=mid:audio\r\n', ''); |
| 374 | offerSdp = offerSdp.replace('a=mid:video\r\n', ''); |
| 375 | offerSdp = offerSdp.replace(/a=ssrc.*\r\n/g, ''); |
| 376 | return offerSdp; |
| 377 | } |
| 378 | |
| 379 | function onAnswerCreated(answer) { |
| 380 | gSecondConnection.setLocalDescription(answer); |
| 381 | expectEquals('stable', gSecondConnection.signalingState); |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 382 | receiveAnswer(answer.sdp); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 383 | } |
| 384 | |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 385 | function receiveAnswer(answerSdp) { |
| 386 | if (gTestWithoutMsidAndBundle) { |
| 387 | answerSdp = removeMsidAndBundle(answerSdp); |
| 388 | } |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 389 | var parsedAnswer = new RTCSessionDescription({ type: 'answer', |
| 390 | sdp: answerSdp }); |
| 391 | gFirstConnection.setRemoteDescription(parsedAnswer); |
| 392 | expectEquals('stable', gFirstConnection.signalingState); |
| 393 | } |
| 394 | |
| 395 | function onIceCandidateToFirst(event) { |
| 396 | if (event.candidate) { |
| 397 | var candidate = new RTCIceCandidate(event.candidate); |
| 398 | gSecondConnection.addIceCandidate(candidate); |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | function onIceCandidateToSecond(event) { |
| 403 | if (event.candidate) { |
| 404 | var candidate = new RTCIceCandidate(event.candidate); |
| 405 | gFirstConnection.addIceCandidate(candidate); |
| 406 | } |
| 407 | } |
| 408 | |
| 409 | function onRemoteStream(e, target) { |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 410 | if (gTestWithoutMsidAndBundle && e.stream.id != "default") { |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 411 | document.title = 'a default remote stream was expected but instead ' + |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 412 | e.stream.id + ' was received.'; |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 413 | return; |
| 414 | } |
| 415 | var remoteStreamUrl = webkitURL.createObjectURL(e.stream); |
| 416 | var remoteVideo = $(target); |
| 417 | remoteVideo.src = remoteStreamUrl; |
| 418 | } |
| 419 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 420 | </script> |
| 421 | </head> |
| 422 | <body> |
| 423 | <table border="0"> |
| 424 | <tr> |
| 425 | <td>Local Preview</td> |
| 426 | <td>Remote Stream for Connection 1</td> |
| 427 | <td>Remote Stream for Connection 2</td> |
| 428 | </tr> |
| 429 | <tr> |
| 430 | <td><video width="320" height="240" id="local-view" |
| 431 | autoplay="autoplay"></video></td> |
| 432 | <td><video width="320" height="240" id="remote-view-1" |
| 433 | autoplay="autoplay"></video></td> |
| 434 | <td><video width="320" height="240" id="remote-view-2" |
| 435 | autoplay="autoplay"></video></td> |
| 436 | <!-- Canvases are named after their corresponding video elements. --> |
| 437 | <td><canvas width="320" height="240" id="remote-view-1-canvas" |
| 438 | style="display:none"></canvas></td> |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 439 | <td><canvas width="320" height="240" id="remote-view-2-canvas" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 440 | style="display:none"></canvas></td> |
| 441 | </tr> |
| 442 | </table> |
| 443 | </body> |
| 444 | </html> |