| OLD | NEW |
| 1 <html> | 1 <html> |
| 2 <head> | 2 <head> |
| 3 <script type="text/javascript" src="webrtc_test_utilities.js"></script> | 3 <script type="text/javascript" src="webrtc_test_utilities.js"></script> |
| 4 <script type="text/javascript" src="webrtc_test_audio.js"></script> | 4 <script type="text/javascript" src="webrtc_test_audio.js"></script> |
| 5 <script type="text/javascript"> | 5 <script type="text/javascript"> |
| 6 $ = function(id) { | 6 $ = function(id) { |
| 7 return document.getElementById(id); | 7 return document.getElementById(id); |
| 8 }; | 8 }; |
| 9 | 9 |
| 10 var gFirstConnection = null; | 10 var gFirstConnection = null; |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 46 'video': 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' + | 46 'video': 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' + |
| 47 'inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj', | 47 'inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj', |
| 48 'data': 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' + | 48 'data': 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' + |
| 49 'inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj' | 49 'inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj' |
| 50 }; | 50 }; |
| 51 | 51 |
| 52 // When using GICE, the ICE credentials can be chosen by javascript. | 52 // When using GICE, the ICE credentials can be chosen by javascript. |
| 53 var EXTERNAL_GICE_UFRAG = '1234567890123456'; | 53 var EXTERNAL_GICE_UFRAG = '1234567890123456'; |
| 54 var EXTERNAL_GICE_PWD = '123456789012345678901234'; | 54 var EXTERNAL_GICE_PWD = '123456789012345678901234'; |
| 55 | 55 |
| 56 setAllEventsOccuredHandler(function() { | 56 setAllEventsOccuredHandler(reportTestSuccess); |
| 57 // The C++ tests look for this 'OK' in the title. | |
| 58 document.title = 'OK'; | |
| 59 }); | |
| 60 | 57 |
| 61 // Test that we can setup call with an audio and video track. | 58 // Test that we can setup call with an audio and video track. |
| 62 function call(constraints) { | 59 function call(constraints) { |
| 63 createConnections(null); | 60 createConnections(null); |
| 64 navigator.webkitGetUserMedia(constraints, | 61 navigator.webkitGetUserMedia(constraints, |
| 65 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); | 62 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| 66 waitForVideo('remote-view-1'); | 63 waitForVideo('remote-view-1'); |
| 67 waitForVideo('remote-view-2'); | 64 waitForVideo('remote-view-2'); |
| 68 } | 65 } |
| 69 | 66 |
| (...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 137 navigator.webkitGetUserMedia({audio: true, video: true}, | 134 navigator.webkitGetUserMedia({audio: true, video: true}, |
| 138 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); | 135 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| 139 waitForVideo('remote-view-1'); | 136 waitForVideo('remote-view-1'); |
| 140 waitForVideo('remote-view-2'); | 137 waitForVideo('remote-view-2'); |
| 141 } | 138 } |
| 142 | 139 |
| 143 // Test that we can't setup a call with an unsupported video codec | 140 // Test that we can't setup a call with an unsupported video codec |
| 144 function negotiateUnsupportedVideoCodec() { | 141 function negotiateUnsupportedVideoCodec() { |
| 145 createConnections(null); | 142 createConnections(null); |
| 146 transformSdp = removeVideoCodec; | 143 transformSdp = removeVideoCodec; |
| 144 |
| 147 onLocalDescriptionError = function(error) { | 145 onLocalDescriptionError = function(error) { |
| 148 var expectedMsg = 'Failed to set local offer sdp:' + | 146 var expectedMsg = 'Failed to set local offer sdp:' + |
| 149 ' Session error code: ERROR_CONTENT. Session error description:' + | 147 ' Session error code: ERROR_CONTENT. Session error description:' + |
| 150 ' Failed to set video receive codecs..'; | 148 ' Failed to set video receive codecs..'; |
| 151 expectEquals(expectedMsg, error); | 149 assertEquals(expectedMsg, error); |
| 152 | 150 reportTestSuccess(); |
| 153 // Got the right message, test succeeded. | |
| 154 document.title = 'OK'; | |
| 155 }; | 151 }; |
| 156 navigator.webkitGetUserMedia({audio: true, video: true}, | 152 navigator.webkitGetUserMedia({audio: true, video: true}, |
| 157 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); | 153 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| 158 } | 154 } |
| 159 | 155 |
| 160 // Test that we can't setup a call if one peer does not support encryption | 156 // Test that we can't setup a call if one peer does not support encryption |
| 161 function negotiateNonCryptoCall() { | 157 function negotiateNonCryptoCall() { |
| 162 createConnections(null); | 158 createConnections(null); |
| 163 transformSdp = removeCrypto; | 159 transformSdp = removeCrypto; |
| 164 onLocalDescriptionError = function(error) { | 160 onLocalDescriptionError = function(error) { |
| 165 var expectedMsg = 'Failed to set local offer sdp:' + | 161 var expectedMsg = 'Failed to set local offer sdp:' + |
| 166 ' Called with SDP without DTLS fingerprint.'; | 162 ' Called with SDP without DTLS fingerprint.'; |
| 167 expectEquals(expectedMsg, error); | |
| 168 | 163 |
| 169 // Got the right message, test succeeded. | 164 assertEquals(expectedMsg, error); |
| 170 document.title = 'OK'; | 165 reportTestSuccess(); |
| 171 }; | 166 }; |
| 172 navigator.webkitGetUserMedia({audio: true, video: true}, | 167 navigator.webkitGetUserMedia({audio: true, video: true}, |
| 173 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); | 168 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| 174 } | 169 } |
| 175 | 170 |
| 176 // Test that we can negotiate a call with an SDP offer that includes a | 171 // 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 | 172 // b=AS:XX line to control audio and video bandwidth |
| 178 function negotiateOfferWithBLine() { | 173 function negotiateOfferWithBLine() { |
| 179 createConnections(null); | 174 createConnections(null); |
| 180 transformSdp = addBandwithControl; | 175 transformSdp = addBandwithControl; |
| 181 navigator.webkitGetUserMedia({audio: true, video: true}, | 176 navigator.webkitGetUserMedia({audio: true, video: true}, |
| 182 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); | 177 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| 183 waitForVideo('remote-view-1'); | 178 waitForVideo('remote-view-1'); |
| 184 waitForVideo('remote-view-2'); | 179 waitForVideo('remote-view-2'); |
| 185 } | 180 } |
| 186 | 181 |
| 187 // Test that we can setup call with legacy settings. | 182 // Test that we can setup call with legacy settings. |
| 188 function callWithLegacySdp() { | 183 function callWithLegacySdp() { |
| 189 transformSdp = function(sdp) { | 184 transformSdp = function(sdp) { |
| 190 return removeBundle(useGice(useExternalSdes(sdp))); | 185 return removeBundle(useGice(useExternalSdes(sdp))); |
| 191 }; | 186 }; |
| 192 transformCandidate = addGiceCredsToCandidate; | 187 transformCandidate = addGiceCredsToCandidate; |
| (...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 237 | 232 |
| 238 // Test call with a data channel and later add audio and video. | 233 // Test call with a data channel and later add audio and video. |
| 239 function callWithDataAndLaterAddMedia() { | 234 function callWithDataAndLaterAddMedia() { |
| 240 createConnections({optional:[{RtpDataChannels: true}]}); | 235 createConnections({optional:[{RtpDataChannels: true}]}); |
| 241 setupDataChannel({reliable: false}); | 236 setupDataChannel({reliable: false}); |
| 242 negotiate(); | 237 negotiate(); |
| 243 | 238 |
| 244 // Set an event handler for when the data channel has been closed. | 239 // Set an event handler for when the data channel has been closed. |
| 245 setAllEventsOccuredHandler(function() { | 240 setAllEventsOccuredHandler(function() { |
| 246 // When the video is flowing the test is done. | 241 // When the video is flowing the test is done. |
| 247 setAllEventsOccuredHandler(function() { | 242 setAllEventsOccuredHandler(reportTestSuccess); |
| 248 document.title = 'OK'; | |
| 249 }); | |
| 250 navigator.webkitGetUserMedia({audio: true, video: true}, | 243 navigator.webkitGetUserMedia({audio: true, video: true}, |
| 251 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); | 244 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| 252 waitForVideo('remote-view-1'); | 245 waitForVideo('remote-view-1'); |
| 253 waitForVideo('remote-view-2'); | 246 waitForVideo('remote-view-2'); |
| 254 }); | 247 }); |
| 255 } | 248 } |
| 256 | 249 |
| 257 // Test that we can setup call and send DTMF. | 250 // Test that we can setup call and send DTMF. |
| 258 function callAndSendDtmf(tones) { | 251 function callAndSendDtmf(tones) { |
| 259 createConnections(null); | 252 createConnections(null); |
| 260 navigator.webkitGetUserMedia({audio: true, video: true}, | 253 navigator.webkitGetUserMedia({audio: true, video: true}, |
| 261 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); | 254 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| 262 var onCallEstablished = function() { | 255 var onCallEstablished = function() { |
| 263 // Send DTMF tones. | 256 // Send DTMF tones. |
| 264 var localAudioTrack = gLocalStream.getAudioTracks()[0]; | 257 var localAudioTrack = gLocalStream.getAudioTracks()[0]; |
| 265 var dtmfSender = gFirstConnection.createDTMFSender(localAudioTrack); | 258 var dtmfSender = gFirstConnection.createDTMFSender(localAudioTrack); |
| 266 dtmfSender.ontonechange = onToneChange; | 259 dtmfSender.ontonechange = onToneChange; |
| 267 dtmfSender.insertDTMF(tones); | 260 dtmfSender.insertDTMF(tones); |
| 268 // Wait for the DTMF tones callback. | 261 // Wait for the DTMF tones callback. |
| 269 document.title = 'Waiting for dtmf...'; | |
| 270 addExpectedEvent(); | 262 addExpectedEvent(); |
| 271 var waitDtmf = setInterval(function() { | 263 var waitDtmf = setInterval(function() { |
| 272 if (gSentTones == tones) { | 264 if (gSentTones == tones) { |
| 273 clearInterval(waitDtmf); | 265 clearInterval(waitDtmf); |
| 274 eventOccured(); | 266 eventOccured(); |
| 275 } | 267 } |
| 276 }, 100); | 268 }, 100); |
| 277 } | 269 } |
| 278 | 270 |
| 279 // Do the DTMF test after we have received video. | 271 // Do the DTMF test after we have received video. |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 316 function callAndEnsureAudioMutingWorks() { | 308 function callAndEnsureAudioMutingWorks() { |
| 317 callAndEnsureAudioIsPlaying(); | 309 callAndEnsureAudioIsPlaying(); |
| 318 setAllEventsOccuredHandler(function() { | 310 setAllEventsOccuredHandler(function() { |
| 319 // Call is up, now mute the track and check everything goes silent (give | 311 // 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). | 312 // it a small delay though, we don't expect it to happen instantly). |
| 321 enableRemoteAudio(gSecondConnection, false); | 313 enableRemoteAudio(gSecondConnection, false); |
| 322 | 314 |
| 323 setTimeout(function() { | 315 setTimeout(function() { |
| 324 gatherAudioLevelSamples(gSecondConnection, 200, 100, function(samples) { | 316 gatherAudioLevelSamples(gSecondConnection, 200, 100, function(samples) { |
| 325 verifyIsSilent(samples); | 317 verifyIsSilent(samples); |
| 326 document.title = 'OK'; | 318 reportTestSuccess(); |
| 327 }); | 319 }); |
| 328 }, 500); | 320 }, 500); |
| 329 }); | 321 }); |
| 330 } | 322 } |
| 331 | 323 |
| 332 function callAndEnsureVideoMutingWorks() { | 324 function callAndEnsureVideoMutingWorks() { |
| 333 createConnections(null); | 325 createConnections(null); |
| 334 navigator.webkitGetUserMedia({audio: true, video: true}, | 326 navigator.webkitGetUserMedia({audio: true, video: true}, |
| 335 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); | 327 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); |
| 336 | 328 |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 377 // Add an audio track to the local stream and remove the video track and | 369 // Add an audio track to the local stream and remove the video track and |
| 378 // then renegotiate. But first - setup the expectations. | 370 // then renegotiate. But first - setup the expectations. |
| 379 local_stream = gFirstConnection.getLocalStreams()[0]; | 371 local_stream = gFirstConnection.getLocalStreams()[0]; |
| 380 | 372 |
| 381 remote_stream_1 = gFirstConnection.getRemoteStreams()[0]; | 373 remote_stream_1 = gFirstConnection.getRemoteStreams()[0]; |
| 382 // Add an expected event that onaddtrack will be called on the remote | 374 // Add an expected event that onaddtrack will be called on the remote |
| 383 // mediastream received on gFirstConnection when the audio track is | 375 // mediastream received on gFirstConnection when the audio track is |
| 384 // received. | 376 // received. |
| 385 addExpectedEvent(); | 377 addExpectedEvent(); |
| 386 remote_stream_1.onaddtrack = function(){ | 378 remote_stream_1.onaddtrack = function(){ |
| 387 expectEquals(remote_stream_1.getAudioTracks()[0].id, | 379 assertEquals(remote_stream_1.getAudioTracks()[0].id, |
| 388 local_stream.getAudioTracks()[0].id); | 380 local_stream.getAudioTracks()[0].id); |
| 389 eventOccured(); | 381 eventOccured(); |
| 390 } | 382 } |
| 391 | 383 |
| 392 // Add an expectation that the received video track is removed from | 384 // Add an expectation that the received video track is removed from |
| 393 // gFirstConnection. | 385 // gFirstConnection. |
| 394 addExpectedEvent(); | 386 addExpectedEvent(); |
| 395 remote_stream_1.onremovetrack = function() { | 387 remote_stream_1.onremovetrack = function() { |
| 396 eventOccured(); | 388 eventOccured(); |
| 397 } | 389 } |
| 398 | 390 |
| 399 // Add an expected event that onaddtrack will be called on the remote | 391 // Add an expected event that onaddtrack will be called on the remote |
| 400 // mediastream received on gSecondConnection when the audio track is | 392 // mediastream received on gSecondConnection when the audio track is |
| 401 // received. | 393 // received. |
| 402 remote_stream_2 = gSecondConnection.getRemoteStreams()[0]; | 394 remote_stream_2 = gSecondConnection.getRemoteStreams()[0]; |
| 403 addExpectedEvent(); | 395 addExpectedEvent(); |
| 404 remote_stream_2.onaddtrack = function() { | 396 remote_stream_2.onaddtrack = function() { |
| 405 expectEquals(remote_stream_2.getAudioTracks()[0].id, | 397 assertEquals(remote_stream_2.getAudioTracks()[0].id, |
| 406 local_stream.getAudioTracks()[0].id); | 398 local_stream.getAudioTracks()[0].id); |
| 407 eventOccured(); | 399 eventOccured(); |
| 408 } | 400 } |
| 409 | 401 |
| 410 // Add an expectation that the received video track is removed from | 402 // Add an expectation that the received video track is removed from |
| 411 // gSecondConnection. | 403 // gSecondConnection. |
| 412 addExpectedEvent(); | 404 addExpectedEvent(); |
| 413 remote_stream_2.onremovetrack = function() { | 405 remote_stream_2.onremovetrack = function() { |
| 414 eventOccured(); | 406 eventOccured(); |
| 415 } | 407 } |
| 416 // When all the above events have occurred- the test pass. | 408 // When all the above events have occurred- the test pass. |
| 417 setAllEventsOccuredHandler(function() { document.title = 'OK'; }); | 409 setAllEventsOccuredHandler(reportTestSuccess); |
| 418 | 410 |
| 419 local_stream.addTrack(gLocalStream.getAudioTracks()[0]); | 411 local_stream.addTrack(gLocalStream.getAudioTracks()[0]); |
| 420 local_stream.removeTrack(local_stream.getVideoTracks()[0]); | 412 local_stream.removeTrack(local_stream.getVideoTracks()[0]); |
| 421 negotiate(); | 413 negotiate(); |
| 422 }); | 414 }); |
| 423 } | 415 } |
| 424 | 416 |
| 425 // This function is used for setting up a test that: | 417 // This function is used for setting up a test that: |
| 426 // 1. Creates a data channel on |gFirstConnection| and sends data to | 418 // 1. Creates a data channel on |gFirstConnection| and sends data to |
| 427 // |gSecondConnection|. | 419 // |gSecondConnection|. |
| 428 // 2. When data is received on |gSecondConnection| a message | 420 // 2. When data is received on |gSecondConnection| a message |
| 429 // is sent to |gFirstConnection|. | 421 // is sent to |gFirstConnection|. |
| 430 // 3. When data is received on |gFirstConnection|, the data | 422 // 3. When data is received on |gFirstConnection|, the data |
| 431 // channel is closed. The test passes when the state transition completes. | 423 // channel is closed. The test passes when the state transition completes. |
| 432 function setupDataChannel(params) { | 424 function setupDataChannel(params) { |
| 433 var sendDataString = "send some text on a data channel." | 425 var sendDataString = "send some text on a data channel." |
| 434 firstDataChannel = gFirstConnection.createDataChannel( | 426 firstDataChannel = gFirstConnection.createDataChannel( |
| 435 "sendDataChannel", params); | 427 "sendDataChannel", params); |
| 436 expectEquals('connecting', firstDataChannel.readyState); | 428 assertEquals('connecting', firstDataChannel.readyState); |
| 437 | 429 |
| 438 // When |firstDataChannel| transition to open state, send a text string. | 430 // When |firstDataChannel| transition to open state, send a text string. |
| 439 firstDataChannel.onopen = function() { | 431 firstDataChannel.onopen = function() { |
| 440 expectEquals('open', firstDataChannel.readyState); | 432 assertEquals('open', firstDataChannel.readyState); |
| 441 firstDataChannel.send(sendDataString); | 433 firstDataChannel.send(sendDataString); |
| 442 } | 434 } |
| 443 | 435 |
| 444 // When |firstDataChannel| receive a message, close the channel and | 436 // When |firstDataChannel| receive a message, close the channel and |
| 445 // initiate a new offer/answer exchange to complete the closure. | 437 // initiate a new offer/answer exchange to complete the closure. |
| 446 firstDataChannel.onmessage = function(event) { | 438 firstDataChannel.onmessage = function(event) { |
| 447 expectEquals(event.data, sendDataString); | 439 assertEquals(event.data, sendDataString); |
| 448 firstDataChannel.close(); | 440 firstDataChannel.close(); |
| 449 negotiate(); | 441 negotiate(); |
| 450 } | 442 } |
| 451 | 443 |
| 452 // When |firstDataChannel| transition to closed state, the test pass. | 444 // When |firstDataChannel| transition to closed state, the test pass. |
| 445 // TODO(phoglund): This is flaky, at least in the setupLegacyCall case. |
| 446 // See http://crbug.com/350388. |
| 453 addExpectedEvent(); | 447 addExpectedEvent(); |
| 454 firstDataChannel.onclose = function() { | 448 firstDataChannel.onclose = function() { |
| 455 expectEquals('closed', firstDataChannel.readyState); | 449 assertEquals('closed', firstDataChannel.readyState); |
| 456 eventOccured(); | 450 eventOccured(); |
| 457 } | 451 } |
| 458 | 452 |
| 459 // Event handler for when |gSecondConnection| receive a new dataChannel. | 453 // Event handler for when |gSecondConnection| receive a new dataChannel. |
| 460 gSecondConnection.ondatachannel = function (event) { | 454 gSecondConnection.ondatachannel = function (event) { |
| 461 var secondDataChannel = event.channel; | 455 var secondDataChannel = event.channel; |
| 462 | 456 |
| 463 // When |secondDataChannel| receive a message, send a message back. | 457 // When |secondDataChannel| receive a message, send a message back. |
| 464 secondDataChannel.onmessage = function(event) { | 458 secondDataChannel.onmessage = function(event) { |
| 465 expectEquals(event.data, sendDataString); | 459 assertEquals(event.data, sendDataString); |
| 466 expectEquals('open', secondDataChannel.readyState); | 460 assertEquals('open', secondDataChannel.readyState); |
| 467 secondDataChannel.send(sendDataString); | 461 secondDataChannel.send(sendDataString); |
| 468 } | 462 } |
| 469 } | 463 } |
| 470 } | 464 } |
| 471 | 465 |
| 472 // SCTP data channel setup is slightly different then RTP based | 466 // SCTP data channel setup is slightly different then RTP based |
| 473 // channels. Due to a bug in libjingle, we can't send data immediately | 467 // channels. Due to a bug in libjingle, we can't send data immediately |
| 474 // after channel becomes open. So for that reason in SCTP, | 468 // after channel becomes open. So for that reason in SCTP, |
| 475 // we are sending data from second channel, when ondatachannel event is | 469 // we are sending data from second channel, when ondatachannel event is |
| 476 // received. So data flow happens 2 -> 1 -> 2. | 470 // received. So data flow happens 2 -> 1 -> 2. |
| 477 function setupSctpDataChannel(params) { | 471 function setupSctpDataChannel(params) { |
| 478 var sendDataString = "send some text on a data channel." | 472 var sendDataString = "send some text on a data channel." |
| 479 firstDataChannel = gFirstConnection.createDataChannel( | 473 firstDataChannel = gFirstConnection.createDataChannel( |
| 480 "sendDataChannel", params); | 474 "sendDataChannel", params); |
| 481 expectEquals('connecting', firstDataChannel.readyState); | 475 assertEquals('connecting', firstDataChannel.readyState); |
| 482 | 476 |
| 483 // When |firstDataChannel| transition to open state, send a text string. | 477 // When |firstDataChannel| transition to open state, send a text string. |
| 484 firstDataChannel.onopen = function() { | 478 firstDataChannel.onopen = function() { |
| 485 expectEquals('open', firstDataChannel.readyState); | 479 assertEquals('open', firstDataChannel.readyState); |
| 486 } | 480 } |
| 487 | 481 |
| 488 // When |firstDataChannel| receive a message, send message back. | 482 // When |firstDataChannel| receive a message, send message back. |
| 489 // initiate a new offer/answer exchange to complete the closure. | 483 // initiate a new offer/answer exchange to complete the closure. |
| 490 firstDataChannel.onmessage = function(event) { | 484 firstDataChannel.onmessage = function(event) { |
| 491 expectEquals('open', firstDataChannel.readyState); | 485 assertEquals('open', firstDataChannel.readyState); |
| 492 expectEquals(event.data, sendDataString); | 486 assertEquals(event.data, sendDataString); |
| 493 firstDataChannel.send(sendDataString); | 487 firstDataChannel.send(sendDataString); |
| 494 } | 488 } |
| 495 | 489 |
| 496 | 490 |
| 497 // Event handler for when |gSecondConnection| receive a new dataChannel. | 491 // Event handler for when |gSecondConnection| receive a new dataChannel. |
| 498 gSecondConnection.ondatachannel = function (event) { | 492 gSecondConnection.ondatachannel = function (event) { |
| 499 var secondDataChannel = event.channel; | 493 var secondDataChannel = event.channel; |
| 500 secondDataChannel.onopen = function() { | 494 secondDataChannel.onopen = function() { |
| 501 secondDataChannel.send(sendDataString); | 495 secondDataChannel.send(sendDataString); |
| 502 } | 496 } |
| 503 | 497 |
| 504 // When |secondDataChannel| receive a message, close the channel and | 498 // When |secondDataChannel| receive a message, close the channel and |
| 505 // initiate a new offer/answer exchange to complete the closure. | 499 // initiate a new offer/answer exchange to complete the closure. |
| 506 secondDataChannel.onmessage = function(event) { | 500 secondDataChannel.onmessage = function(event) { |
| 507 expectEquals(event.data, sendDataString); | 501 assertEquals(event.data, sendDataString); |
| 508 expectEquals('open', secondDataChannel.readyState); | 502 assertEquals('open', secondDataChannel.readyState); |
| 509 secondDataChannel.close(); | 503 secondDataChannel.close(); |
| 510 negotiate(); | 504 negotiate(); |
| 511 } | 505 } |
| 512 | 506 |
| 513 // When |secondDataChannel| transition to closed state, the test pass. | 507 // When |secondDataChannel| transition to closed state, the test pass. |
| 514 addExpectedEvent(); | 508 addExpectedEvent(); |
| 515 secondDataChannel.onclose = function() { | 509 secondDataChannel.onclose = function() { |
| 516 expectEquals('closed', secondDataChannel.readyState); | 510 assertEquals('closed', secondDataChannel.readyState); |
| 517 eventOccured(); | 511 eventOccured(); |
| 518 } | 512 } |
| 519 } | 513 } |
| 520 } | 514 } |
| 521 | 515 |
| 522 // Test call with a stream that has been created by getUserMedia, clone | 516 // 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. | 517 // the stream to a cloned stream, send them via the same peer connection. |
| 524 function addTwoMediaStreamsToOneConnection() { | 518 function addTwoMediaStreamsToOneConnection() { |
| 525 createConnections(null); | 519 createConnections(null); |
| 526 navigator.webkitGetUserMedia({audio: true, video: true}, | 520 navigator.webkitGetUserMedia({audio: true, video: true}, |
| 527 CloneStreamAndAddTwoStreamstoOneConnection, printGetUserMediaError); | 521 CloneStreamAndAddTwoStreamstoOneConnection, printGetUserMediaError); |
| 528 } | 522 } |
| 529 | 523 |
| 530 function onToneChange(tone) { | 524 function onToneChange(tone) { |
| 531 gSentTones += tone.tone; | 525 gSentTones += tone.tone; |
| 532 document.title = gSentTones; | |
| 533 } | 526 } |
| 534 | 527 |
| 535 function createConnections(constraints) { | 528 function createConnections(constraints) { |
| 536 gFirstConnection = createConnection(constraints, 'remote-view-1'); | 529 gFirstConnection = createConnection(constraints, 'remote-view-1'); |
| 537 expectEquals('stable', gFirstConnection.signalingState); | 530 assertEquals('stable', gFirstConnection.signalingState); |
| 538 | 531 |
| 539 gSecondConnection = createConnection(constraints, 'remote-view-2'); | 532 gSecondConnection = createConnection(constraints, 'remote-view-2'); |
| 540 expectEquals('stable', gSecondConnection.signalingState); | 533 assertEquals('stable', gSecondConnection.signalingState); |
| 541 } | 534 } |
| 542 | 535 |
| 543 function createConnection(constraints, remoteView) { | 536 function createConnection(constraints, remoteView) { |
| 544 var pc = new webkitRTCPeerConnection(null, constraints); | 537 var pc = new webkitRTCPeerConnection(null, constraints); |
| 545 pc.onaddstream = function(event) { | 538 pc.onaddstream = function(event) { |
| 546 onRemoteStream(event, remoteView); | 539 onRemoteStream(event, remoteView); |
| 547 } | 540 } |
| 548 return pc; | 541 return pc; |
| 549 } | 542 } |
| 550 | 543 |
| 551 function displayAndRemember(localStream) { | 544 function displayAndRemember(localStream) { |
| 552 var localStreamUrl = URL.createObjectURL(localStream); | 545 var localStreamUrl = URL.createObjectURL(localStream); |
| 553 $('local-view').src = localStreamUrl; | 546 $('local-view').src = localStreamUrl; |
| 554 | 547 |
| 555 gLocalStream = localStream; | 548 gLocalStream = localStream; |
| 556 } | 549 } |
| 557 | 550 |
| 558 // Called if getUserMedia fails. | 551 // Called if getUserMedia fails. |
| 559 function printGetUserMediaError(error) { | 552 function printGetUserMediaError(error) { |
| 560 document.title = 'getUserMedia request failed:'; | 553 var message = 'getUserMedia request unexpectedly failed:'; |
| 561 if (error.constraintName) | 554 if (error.constraintName) |
| 562 document.title += ' could not satisfy constraint ' + error.constraintName; | 555 message += ' could not satisfy constraint ' + error.constraintName; |
| 563 else | 556 else |
| 564 document.title += ' devices not working/user denied access.'; | 557 message += ' devices not working/user denied access.'; |
| 565 console.log(document.title); | 558 failTest(message); |
| 566 } | 559 } |
| 567 | 560 |
| 568 // Called if getUserMedia succeeds and we want to send from both connections. | 561 // Called if getUserMedia succeeds and we want to send from both connections. |
| 569 function addStreamToBothConnectionsAndNegotiate(localStream) { | 562 function addStreamToBothConnectionsAndNegotiate(localStream) { |
| 570 displayAndRemember(localStream); | 563 displayAndRemember(localStream); |
| 571 gFirstConnection.addStream(localStream); | 564 gFirstConnection.addStream(localStream); |
| 572 gSecondConnection.addStream(localStream); | 565 gSecondConnection.addStream(localStream); |
| 573 negotiate(); | 566 negotiate(); |
| 574 } | 567 } |
| 575 | 568 |
| 576 // Called if getUserMedia succeeds when we want to send from one connection. | 569 // Called if getUserMedia succeeds when we want to send from one connection. |
| 577 function addStreamToTheFirstConnectionAndNegotiate(localStream) { | 570 function addStreamToTheFirstConnectionAndNegotiate(localStream) { |
| 578 displayAndRemember(localStream); | 571 displayAndRemember(localStream); |
| 579 gFirstConnection.addStream(localStream); | 572 gFirstConnection.addStream(localStream); |
| 580 negotiate(); | 573 negotiate(); |
| 581 } | 574 } |
| 582 | 575 |
| 583 function verifyHasOneAudioAndVideoTrack(stream) { | 576 function verifyHasOneAudioAndVideoTrack(stream) { |
| 584 expectEquals(1, stream.getAudioTracks().length); | 577 assertEquals(1, stream.getAudioTracks().length); |
| 585 expectEquals(1, stream.getVideoTracks().length); | 578 assertEquals(1, stream.getVideoTracks().length); |
| 586 } | 579 } |
| 587 | 580 |
| 588 // Called if getUserMedia succeeds, then clone the stream, send two streams | 581 // Called if getUserMedia succeeds, then clone the stream, send two streams |
| 589 // from one peer connection. | 582 // from one peer connection. |
| 590 function CloneStreamAndAddTwoStreamstoOneConnection(localStream) { | 583 function CloneStreamAndAddTwoStreamstoOneConnection(localStream) { |
| 591 displayAndRemember(localStream); | 584 displayAndRemember(localStream); |
| 592 | 585 |
| 593 var clonedStream = null; | 586 var clonedStream = null; |
| 594 if (typeof localStream.clone === "function") { | 587 if (typeof localStream.clone === "function") { |
| 595 clonedStream = localStream.clone(); | 588 clonedStream = localStream.clone(); |
| 596 } else { | 589 } else { |
| 597 clonedStream = new webkitMediaStream(localStream); | 590 clonedStream = new webkitMediaStream(localStream); |
| 598 } | 591 } |
| 599 | 592 |
| 600 gFirstConnection.addStream(localStream); | 593 gFirstConnection.addStream(localStream); |
| 601 gFirstConnection.addStream(clonedStream); | 594 gFirstConnection.addStream(clonedStream); |
| 602 | 595 |
| 603 // Verify the local streams are correct. | 596 // Verify the local streams are correct. |
| 604 expectEquals(2, gFirstConnection.getLocalStreams().length); | 597 assertEquals(2, gFirstConnection.getLocalStreams().length); |
| 605 verifyHasOneAudioAndVideoTrack(gFirstConnection.getLocalStreams()[0]); | 598 verifyHasOneAudioAndVideoTrack(gFirstConnection.getLocalStreams()[0]); |
| 606 verifyHasOneAudioAndVideoTrack(gFirstConnection.getLocalStreams()[1]); | 599 verifyHasOneAudioAndVideoTrack(gFirstConnection.getLocalStreams()[1]); |
| 607 | 600 |
| 608 // The remote side should receive two streams. After that, verify the | 601 // The remote side should receive two streams. After that, verify the |
| 609 // remote side has the correct number of streams and tracks. | 602 // remote side has the correct number of streams and tracks. |
| 610 addExpectedEvent(); | 603 addExpectedEvent(); |
| 611 addExpectedEvent(); | 604 addExpectedEvent(); |
| 612 gSecondConnection.onaddstream = function(event) { | 605 gSecondConnection.onaddstream = function(event) { |
| 613 eventOccured(); | 606 eventOccured(); |
| 614 } | 607 } |
| 615 setAllEventsOccuredHandler(function() { | 608 setAllEventsOccuredHandler(function() { |
| 616 // Negotiation complete, verify remote streams on the receiving side. | 609 // Negotiation complete, verify remote streams on the receiving side. |
| 617 expectEquals(2, gSecondConnection.getRemoteStreams().length); | 610 assertEquals(2, gSecondConnection.getRemoteStreams().length); |
| 618 verifyHasOneAudioAndVideoTrack(gSecondConnection.getRemoteStreams()[0]); | 611 verifyHasOneAudioAndVideoTrack(gSecondConnection.getRemoteStreams()[0]); |
| 619 verifyHasOneAudioAndVideoTrack(gSecondConnection.getRemoteStreams()[1]); | 612 verifyHasOneAudioAndVideoTrack(gSecondConnection.getRemoteStreams()[1]); |
| 620 | 613 |
| 621 document.title = "OK"; | 614 reportTestSuccess(); |
| 622 }); | 615 }); |
| 623 | 616 |
| 624 negotiate(); | 617 negotiate(); |
| 625 } | 618 } |
| 626 | 619 |
| 627 // Called if getUserMedia succeeds when we want to send a modified | 620 // Called if getUserMedia succeeds when we want to send a modified |
| 628 // MediaStream. A new MediaStream is created and the video track from | 621 // MediaStream. A new MediaStream is created and the video track from |
| 629 // |localStream| is added. | 622 // |localStream| is added. |
| 630 function createNewVideoStreamAndAddToBothConnections(localStream) { | 623 function createNewVideoStreamAndAddToBothConnections(localStream) { |
| 631 displayAndRemember(localStream); | 624 displayAndRemember(localStream); |
| (...skipping 19 matching lines...) Expand all Loading... |
| 651 | 644 |
| 652 caller.createOffer( | 645 caller.createOffer( |
| 653 function (offer) { | 646 function (offer) { |
| 654 onOfferCreated(offer, caller, callee); | 647 onOfferCreated(offer, caller, callee); |
| 655 }); | 648 }); |
| 656 } | 649 } |
| 657 | 650 |
| 658 function onOfferCreated(offer, caller, callee) { | 651 function onOfferCreated(offer, caller, callee) { |
| 659 offer.sdp = maybeForceIsac16K(transformSdp(offer.sdp)); | 652 offer.sdp = maybeForceIsac16K(transformSdp(offer.sdp)); |
| 660 caller.setLocalDescription(offer, function() { | 653 caller.setLocalDescription(offer, function() { |
| 661 expectEquals('have-local-offer', caller.signalingState); | 654 assertEquals('have-local-offer', caller.signalingState); |
| 662 receiveOffer(offer.sdp, caller, callee); | 655 receiveOffer(offer.sdp, caller, callee); |
| 663 }, onLocalDescriptionError); | 656 }, onLocalDescriptionError); |
| 664 } | 657 } |
| 665 | 658 |
| 666 function receiveOffer(offerSdp, caller, callee) { | 659 function receiveOffer(offerSdp, caller, callee) { |
| 667 console.log("Receiving offer..."); | 660 console.log("Receiving offer..."); |
| 668 offerSdp = transformRemoteSdp(offerSdp); | 661 offerSdp = transformRemoteSdp(offerSdp); |
| 669 | 662 |
| 670 var parsedOffer = new RTCSessionDescription({ type: 'offer', | 663 var parsedOffer = new RTCSessionDescription({ type: 'offer', |
| 671 sdp: offerSdp }); | 664 sdp: offerSdp }); |
| 672 callee.setRemoteDescription(parsedOffer); | 665 callee.setRemoteDescription(parsedOffer); |
| 673 callee.createAnswer(function (answer) { | 666 callee.createAnswer(function (answer) { |
| 674 onAnswerCreated(answer, caller, callee); | 667 onAnswerCreated(answer, caller, callee); |
| 675 }); | 668 }); |
| 676 expectEquals('have-remote-offer', callee.signalingState); | 669 assertEquals('have-remote-offer', callee.signalingState); |
| 677 } | 670 } |
| 678 | 671 |
| 679 function removeMsid(offerSdp) { | 672 function removeMsid(offerSdp) { |
| 680 offerSdp = offerSdp.replace(/a=msid-semantic.*\r\n/g, ''); | 673 offerSdp = offerSdp.replace(/a=msid-semantic.*\r\n/g, ''); |
| 681 offerSdp = offerSdp.replace('a=mid:audio\r\n', ''); | 674 offerSdp = offerSdp.replace('a=mid:audio\r\n', ''); |
| 682 offerSdp = offerSdp.replace('a=mid:video\r\n', ''); | 675 offerSdp = offerSdp.replace('a=mid:video\r\n', ''); |
| 683 offerSdp = offerSdp.replace(/a=ssrc.*\r\n/g, ''); | 676 offerSdp = offerSdp.replace(/a=ssrc.*\r\n/g, ''); |
| 684 return offerSdp; | 677 return offerSdp; |
| 685 } | 678 } |
| 686 | 679 |
| 687 function removeVideoCodec(offerSdp) { | 680 function removeVideoCodec(offerSdp) { |
| 688 offerSdp = offerSdp.replace('a=rtpmap:100 VP8/90000\r\n', | 681 offerSdp = offerSdp.replace('a=rtpmap:100 VP8/90000\r\n', |
| 689 'a=rtpmap:100 XVP8/90000\r\n'); | 682 'a=rtpmap:100 XVP8/90000\r\n'); |
| 690 return offerSdp; | 683 return offerSdp; |
| 691 } | 684 } |
| 692 | 685 |
| 693 function removeCrypto(offerSdp) { | 686 function removeCrypto(offerSdp) { |
| 694 offerSdp = offerSdp.replace(/a=crypto.*\r\n/g, 'a=Xcrypto\r\n'); | 687 offerSdp = offerSdp.replace(/a=crypto.*\r\n/g, 'a=Xcrypto\r\n'); |
| 695 offerSdp = offerSdp.replace(/a=fingerprint.*\r\n/g, ''); | 688 offerSdp = offerSdp.replace(/a=fingerprint.*\r\n/g, ''); |
| 696 return offerSdp; | 689 return offerSdp; |
| 697 } | 690 } |
| 698 | 691 |
| 699 function addBandwithControl(offerSdp) { | 692 function addBandwithControl(offerSdp) { |
| 700 offerSdp = offerSdp.replace('a=mid:audio\r\n', 'a=mid:audio\r\n'+ | 693 offerSdp = offerSdp.replace('a=mid:audio\r\n', 'a=mid:audio\r\n'+ |
| 701 'b=AS:16\r\n'); | 694 'b=AS:16\r\n'); |
| 702 offerSdp = offerSdp.replace('a=mid:video\r\n', 'a=mid:video\r\n'+ | 695 offerSdp = offerSdp.replace('a=mid:video\r\n', 'a=mid:video\r\n'+ |
| 703 'b=AS:512\r\n'); | 696 'b=AS:512\r\n'); |
| 704 return offerSdp; | 697 return offerSdp; |
| 705 } | 698 } |
| 706 | 699 |
| 707 function removeBundle(sdp) { | 700 function removeBundle(sdp) { |
| 708 return sdp.replace(/a=group:BUNDLE .*\r\n/g, ''); | 701 return sdp.replace(/a=group:BUNDLE .*\r\n/g, ''); |
| 709 } | 702 } |
| 710 | 703 |
| 711 function useGice(sdp) { | 704 function useGice(sdp) { |
| 712 sdp = sdp.replace(/t=.*\r\n/g, function(subString) { | 705 sdp = sdp.replace(/t=.*\r\n/g, function(subString) { |
| 713 return subString + 'a=ice-options:google-ice\r\n'; | 706 return subString + 'a=ice-options:google-ice\r\n'; |
| 714 }); | 707 }); |
| 715 sdp = sdp.replace(/a=ice-ufrag:.*\r\n/g, | 708 sdp = sdp.replace(/a=ice-ufrag:.*\r\n/g, |
| 716 'a=ice-ufrag:' + EXTERNAL_GICE_UFRAG + '\r\n'); | 709 'a=ice-ufrag:' + EXTERNAL_GICE_UFRAG + '\r\n'); |
| 717 sdp = sdp.replace(/a=ice-pwd:.*\r\n/g, | 710 sdp = sdp.replace(/a=ice-pwd:.*\r\n/g, |
| 718 'a=ice-pwd:' + EXTERNAL_GICE_PWD + '\r\n'); | 711 'a=ice-pwd:' + EXTERNAL_GICE_PWD + '\r\n'); |
| 719 return sdp; | 712 return sdp; |
| 720 } | 713 } |
| 721 | 714 |
| 722 function useExternalSdes(sdp) { | 715 function useExternalSdes(sdp) { |
| 723 // Remove current crypto specification. | 716 // Remove current crypto specification. |
| 724 sdp = sdp.replace(/a=crypto.*\r\n/g, ''); | 717 sdp = sdp.replace(/a=crypto.*\r\n/g, ''); |
| 725 sdp = sdp.replace(/a=fingerprint.*\r\n/g, ''); | 718 sdp = sdp.replace(/a=fingerprint.*\r\n/g, ''); |
| 726 // Add external crypto. This is not compatible with |removeMsid|. | 719 // Add external crypto. This is not compatible with |removeMsid|. |
| 727 sdp = sdp.replace(/a=mid:(\w+)\r\n/g, function(subString, group) { | 720 sdp = sdp.replace(/a=mid:(\w+)\r\n/g, function(subString, group) { |
| 728 return subString + EXTERNAL_SDES_LINES[group] + '\r\n'; | 721 return subString + EXTERNAL_SDES_LINES[group] + '\r\n'; |
| 729 }); | 722 }); |
| 730 return sdp; | 723 return sdp; |
| 731 } | 724 } |
| 732 | 725 |
| 733 function onAnswerCreated(answer, caller, callee) { | 726 function onAnswerCreated(answer, caller, callee) { |
| 734 answer.sdp = maybeForceIsac16K(transformSdp(answer.sdp)); | 727 answer.sdp = maybeForceIsac16K(transformSdp(answer.sdp)); |
| 735 callee.setLocalDescription(answer); | 728 callee.setLocalDescription(answer); |
| 736 expectEquals('stable', callee.signalingState); | 729 assertEquals('stable', callee.signalingState); |
| 737 receiveAnswer(answer.sdp, caller); | 730 receiveAnswer(answer.sdp, caller); |
| 738 } | 731 } |
| 739 | 732 |
| 740 function receiveAnswer(answerSdp, caller) { | 733 function receiveAnswer(answerSdp, caller) { |
| 741 console.log("Receiving answer..."); | 734 console.log("Receiving answer..."); |
| 742 answerSdp = transformRemoteSdp(answerSdp); | 735 answerSdp = transformRemoteSdp(answerSdp); |
| 743 var parsedAnswer = new RTCSessionDescription({ type: 'answer', | 736 var parsedAnswer = new RTCSessionDescription({ type: 'answer', |
| 744 sdp: answerSdp }); | 737 sdp: answerSdp }); |
| 745 caller.setRemoteDescription(parsedAnswer); | 738 caller.setRemoteDescription(parsedAnswer); |
| 746 expectEquals('stable', caller.signalingState); | 739 assertEquals('stable', caller.signalingState); |
| 747 } | 740 } |
| 748 | 741 |
| 749 function connectOnIceCandidate(caller, callee) { | 742 function connectOnIceCandidate(caller, callee) { |
| 750 caller.onicecandidate = function(event) { onIceCandidate(event, callee); } | 743 caller.onicecandidate = function(event) { onIceCandidate(event, callee); } |
| 751 callee.onicecandidate = function(event) { onIceCandidate(event, caller); } | 744 callee.onicecandidate = function(event) { onIceCandidate(event, caller); } |
| 752 } | 745 } |
| 753 | 746 |
| 754 function addGiceCredsToCandidate(candidate) { | 747 function addGiceCredsToCandidate(candidate) { |
| 755 return candidate.trimRight() + | 748 return candidate.trimRight() + |
| 756 ' username ' + EXTERNAL_GICE_UFRAG + ' password ' + EXTERNAL_GICE_PWD; | 749 ' username ' + EXTERNAL_GICE_UFRAG + ' password ' + EXTERNAL_GICE_PWD; |
| 757 } | 750 } |
| 758 | 751 |
| 759 function onIceCandidate(event, target) { | 752 function onIceCandidate(event, target) { |
| 760 if (event.candidate) { | 753 if (event.candidate) { |
| 761 console.log("onIceCandidate, candidate is:" + event.candidate.candidate); | 754 console.log("onIceCandidate, candidate is:" + event.candidate.candidate); |
| 762 var candidate = new RTCIceCandidate(event.candidate); | 755 var candidate = new RTCIceCandidate(event.candidate); |
| 763 candidate.candidate = transformCandidate(candidate.candidate); | 756 candidate.candidate = transformCandidate(candidate.candidate); |
| 764 target.addIceCandidate(candidate); | 757 target.addIceCandidate(candidate); |
| 765 } | 758 } |
| 766 } | 759 } |
| 767 | 760 |
| 768 function onRemoteStream(e, target) { | 761 function onRemoteStream(e, target) { |
| 769 console.log("Receiving remote stream..."); | 762 console.log("Receiving remote stream..."); |
| 770 if (gTestWithoutMsid && e.stream.id != "default") { | 763 if (gTestWithoutMsid && e.stream.id != "default") { |
| 771 document.title = 'a default remote stream was expected but instead ' + | 764 failTest('a default remote stream was expected but instead ' + |
| 772 e.stream.id + ' was received.'; | 765 e.stream.id + ' was received.'); |
| 773 return; | |
| 774 } | 766 } |
| 775 gRemoteStreams[target] = e.stream; | 767 gRemoteStreams[target] = e.stream; |
| 776 var remoteStreamUrl = URL.createObjectURL(e.stream); | 768 var remoteStreamUrl = URL.createObjectURL(e.stream); |
| 777 var remoteVideo = $(target); | 769 var remoteVideo = $(target); |
| 778 remoteVideo.src = remoteStreamUrl; | 770 remoteVideo.src = remoteStreamUrl; |
| 779 } | 771 } |
| 780 | 772 |
| 781 </script> | 773 </script> |
| 782 </head> | 774 </head> |
| 783 <body> | 775 <body> |
| (...skipping 22 matching lines...) Expand all Loading... |
| 806 <td><canvas width="320" height="240" id="remote-view-2-canvas" | 798 <td><canvas width="320" height="240" id="remote-view-2-canvas" |
| 807 style="display:none"></canvas></td> | 799 style="display:none"></canvas></td> |
| 808 <td><canvas width="320" height="240" id="remote-view-3-canvas" | 800 <td><canvas width="320" height="240" id="remote-view-3-canvas" |
| 809 style="display:none"></canvas></td> | 801 style="display:none"></canvas></td> |
| 810 <td><canvas width="320" height="240" id="remote-view-4-canvas" | 802 <td><canvas width="320" height="240" id="remote-view-4-canvas" |
| 811 style="display:none"></canvas></td> | 803 style="display:none"></canvas></td> |
| 812 </tr> | 804 </tr> |
| 813 </table> | 805 </table> |
| 814 </body> | 806 </body> |
| 815 </html> | 807 </html> |
| OLD | NEW |