1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/media/tests/mochitest/pc.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1782 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 + 1.11 +/** 1.12 + * This class mimics a state machine and handles a list of commands by 1.13 + * executing them synchronously. 1.14 + * 1.15 + * @constructor 1.16 + * @param {object} framework 1.17 + * A back reference to the framework which makes use of the class. It's 1.18 + * getting passed in as parameter to each command callback. 1.19 + * @param {Array[]} [commandList=[]] 1.20 + * Default commands to set during initialization 1.21 + */ 1.22 +function CommandChain(framework, commandList) { 1.23 + this._framework = framework; 1.24 + 1.25 + this._commands = commandList || [ ]; 1.26 + this._current = 0; 1.27 + 1.28 + this.onFinished = null; 1.29 +} 1.30 + 1.31 +CommandChain.prototype = { 1.32 + 1.33 + /** 1.34 + * Returns the index of the current command of the chain 1.35 + * 1.36 + * @returns {number} Index of the current command 1.37 + */ 1.38 + get current() { 1.39 + return this._current; 1.40 + }, 1.41 + 1.42 + /** 1.43 + * Checks if the chain has already processed all the commands 1.44 + * 1.45 + * @returns {boolean} True, if all commands have been processed 1.46 + */ 1.47 + get finished() { 1.48 + return this._current === this._commands.length; 1.49 + }, 1.50 + 1.51 + /** 1.52 + * Returns the assigned commands of the chain. 1.53 + * 1.54 + * @returns {Array[]} Commands of the chain 1.55 + */ 1.56 + get commands() { 1.57 + return this._commands; 1.58 + }, 1.59 + 1.60 + /** 1.61 + * Sets new commands for the chain. All existing commands will be replaced. 1.62 + * 1.63 + * @param {Array[]} commands 1.64 + * List of commands 1.65 + */ 1.66 + set commands(commands) { 1.67 + this._commands = commands; 1.68 + }, 1.69 + 1.70 + /** 1.71 + * Execute the next command in the chain. 1.72 + */ 1.73 + executeNext : function () { 1.74 + var self = this; 1.75 + 1.76 + function _executeNext() { 1.77 + if (!self.finished) { 1.78 + var step = self._commands[self._current]; 1.79 + self._current++; 1.80 + 1.81 + self.currentStepLabel = step[0]; 1.82 + info("Run step: " + self.currentStepLabel); 1.83 + step[1](self._framework); // Execute step 1.84 + } 1.85 + else if (typeof(self.onFinished) === 'function') { 1.86 + self.onFinished(); 1.87 + } 1.88 + } 1.89 + 1.90 + // To prevent building up the stack we have to execute the next 1.91 + // step asynchronously 1.92 + window.setTimeout(_executeNext, 0); 1.93 + }, 1.94 + 1.95 + /** 1.96 + * Add new commands to the end of the chain 1.97 + * 1.98 + * @param {Array[]} commands 1.99 + * List of commands 1.100 + */ 1.101 + append: function (commands) { 1.102 + this._commands = this._commands.concat(commands); 1.103 + }, 1.104 + 1.105 + /** 1.106 + * Returns the index of the specified command in the chain. 1.107 + * 1.108 + * @param {string} id 1.109 + * Identifier of the command 1.110 + * @returns {number} Index of the command 1.111 + */ 1.112 + indexOf: function (id) { 1.113 + for (var i = 0; i < this._commands.length; i++) { 1.114 + if (this._commands[i][0] === id) { 1.115 + return i; 1.116 + } 1.117 + } 1.118 + 1.119 + return -1; 1.120 + }, 1.121 + 1.122 + /** 1.123 + * Inserts the new commands after the specified command. 1.124 + * 1.125 + * @param {string} id 1.126 + * Identifier of the command 1.127 + * @param {Array[]} commands 1.128 + * List of commands 1.129 + */ 1.130 + insertAfter: function (id, commands) { 1.131 + var index = this.indexOf(id); 1.132 + 1.133 + if (index > -1) { 1.134 + var tail = this.removeAfter(id); 1.135 + 1.136 + this.append(commands); 1.137 + this.append(tail); 1.138 + } 1.139 + }, 1.140 + 1.141 + /** 1.142 + * Inserts the new commands before the specified command. 1.143 + * 1.144 + * @param {string} id 1.145 + * Identifier of the command 1.146 + * @param {Array[]} commands 1.147 + * List of commands 1.148 + */ 1.149 + insertBefore: function (id, commands) { 1.150 + var index = this.indexOf(id); 1.151 + 1.152 + if (index > -1) { 1.153 + var tail = this.removeAfter(id); 1.154 + var object = this.remove(id); 1.155 + 1.156 + this.append(commands); 1.157 + this.append(object); 1.158 + this.append(tail); 1.159 + } 1.160 + }, 1.161 + 1.162 + /** 1.163 + * Removes the specified command 1.164 + * 1.165 + * @param {string} id 1.166 + * Identifier of the command 1.167 + * @returns {object[]} Removed command 1.168 + */ 1.169 + remove : function (id) { 1.170 + return this._commands.splice(this.indexOf(id), 1); 1.171 + }, 1.172 + 1.173 + /** 1.174 + * Removes all commands after the specified one. 1.175 + * 1.176 + * @param {string} id 1.177 + * Identifier of the command 1.178 + * @returns {object[]} Removed commands 1.179 + */ 1.180 + removeAfter : function (id) { 1.181 + var index = this.indexOf(id); 1.182 + 1.183 + if (index > -1) { 1.184 + return this._commands.splice(index + 1); 1.185 + } 1.186 + 1.187 + return null; 1.188 + }, 1.189 + 1.190 + /** 1.191 + * Removes all commands before the specified one. 1.192 + * 1.193 + * @param {string} id 1.194 + * Identifier of the command 1.195 + * @returns {object[]} Removed commands 1.196 + */ 1.197 + removeBefore : function (id) { 1.198 + var index = this.indexOf(id); 1.199 + 1.200 + if (index > -1) { 1.201 + return this._commands.splice(0, index); 1.202 + } 1.203 + 1.204 + return null; 1.205 + }, 1.206 + 1.207 + /** 1.208 + * Replaces all commands after the specified one. 1.209 + * 1.210 + * @param {string} id 1.211 + * Identifier of the command 1.212 + * @returns {object[]} Removed commands 1.213 + */ 1.214 + replaceAfter : function (id, commands) { 1.215 + var oldCommands = this.removeAfter(id); 1.216 + this.append(commands); 1.217 + 1.218 + return oldCommands; 1.219 + }, 1.220 + 1.221 + /** 1.222 + * Replaces all commands before the specified one. 1.223 + * 1.224 + * @param {string} id 1.225 + * Identifier of the command 1.226 + * @returns {object[]} Removed commands 1.227 + */ 1.228 + replaceBefore : function (id, commands) { 1.229 + var oldCommands = this.removeBefore(id); 1.230 + this.insertBefore(id, commands); 1.231 + 1.232 + return oldCommands; 1.233 + }, 1.234 + 1.235 + /** 1.236 + * Remove all commands whose identifiers match the specified regex. 1.237 + * 1.238 + * @param {regex} id_match 1.239 + * Regular expression to match command identifiers. 1.240 + */ 1.241 + filterOut : function (id_match) { 1.242 + for (var i = this._commands.length - 1; i >= 0; i--) { 1.243 + if (id_match.test(this._commands[i][0])) { 1.244 + this._commands.splice(i, 1); 1.245 + } 1.246 + } 1.247 + } 1.248 +}; 1.249 + 1.250 +/** 1.251 + * This class provides a state checker for media elements which store 1.252 + * a media stream to check for media attribute state and events fired. 1.253 + * When constructed by a caller, an object instance is created with 1.254 + * a media element, event state checkers for canplaythrough, timeupdate, and 1.255 + * time changing on the media element and stream. 1.256 + * 1.257 + * @param {HTMLMediaElement} element the media element being analyzed 1.258 + */ 1.259 +function MediaElementChecker(element) { 1.260 + this.element = element; 1.261 + this.canPlayThroughFired = false; 1.262 + this.timeUpdateFired = false; 1.263 + this.timePassed = false; 1.264 + 1.265 + var self = this; 1.266 + var elementId = self.element.getAttribute('id'); 1.267 + 1.268 + // When canplaythrough fires, we track that it's fired and remove the 1.269 + // event listener. 1.270 + var canPlayThroughCallback = function() { 1.271 + info('canplaythrough fired for media element ' + elementId); 1.272 + self.canPlayThroughFired = true; 1.273 + self.element.removeEventListener('canplaythrough', canPlayThroughCallback, 1.274 + false); 1.275 + }; 1.276 + 1.277 + // When timeupdate fires, we track that it's fired and check if time 1.278 + // has passed on the media stream and media element. 1.279 + var timeUpdateCallback = function() { 1.280 + self.timeUpdateFired = true; 1.281 + info('timeupdate fired for media element ' + elementId); 1.282 + 1.283 + // If time has passed, then track that and remove the timeupdate event 1.284 + // listener. 1.285 + if(element.mozSrcObject && element.mozSrcObject.currentTime > 0 && 1.286 + element.currentTime > 0) { 1.287 + info('time passed for media element ' + elementId); 1.288 + self.timePassed = true; 1.289 + self.element.removeEventListener('timeupdate', timeUpdateCallback, 1.290 + false); 1.291 + } 1.292 + }; 1.293 + 1.294 + element.addEventListener('canplaythrough', canPlayThroughCallback, false); 1.295 + element.addEventListener('timeupdate', timeUpdateCallback, false); 1.296 +} 1.297 + 1.298 +MediaElementChecker.prototype = { 1.299 + 1.300 + /** 1.301 + * Waits until the canplaythrough & timeupdate events to fire along with 1.302 + * ensuring time has passed on the stream and media element. 1.303 + * 1.304 + * @param {Function} onSuccess the success callback when media flow is 1.305 + * established 1.306 + */ 1.307 + waitForMediaFlow : function MEC_WaitForMediaFlow(onSuccess) { 1.308 + var self = this; 1.309 + var elementId = self.element.getAttribute('id'); 1.310 + info('Analyzing element: ' + elementId); 1.311 + 1.312 + if(self.canPlayThroughFired && self.timeUpdateFired && self.timePassed) { 1.313 + ok(true, 'Media flowing for ' + elementId); 1.314 + onSuccess(); 1.315 + } else { 1.316 + setTimeout(function() { 1.317 + self.waitForMediaFlow(onSuccess); 1.318 + }, 100); 1.319 + } 1.320 + }, 1.321 + 1.322 + /** 1.323 + * Checks if there is no media flow present by checking that the ready 1.324 + * state of the media element is HAVE_METADATA. 1.325 + */ 1.326 + checkForNoMediaFlow : function MEC_CheckForNoMediaFlow() { 1.327 + ok(this.element.readyState === HTMLMediaElement.HAVE_METADATA, 1.328 + 'Media element has a ready state of HAVE_METADATA'); 1.329 + } 1.330 +}; 1.331 + 1.332 +/** 1.333 + * Query function for determining if any IP address is available for 1.334 + * generating SDP. 1.335 + * 1.336 + * @return false if required additional network setup. 1.337 + */ 1.338 +function isNetworkReady() { 1.339 + // for gonk platform 1.340 + if ("nsINetworkInterfaceListService" in SpecialPowers.Ci) { 1.341 + var listService = SpecialPowers.Cc["@mozilla.org/network/interface-list-service;1"] 1.342 + .getService(SpecialPowers.Ci.nsINetworkInterfaceListService); 1.343 + var itfList = listService.getDataInterfaceList( 1.344 + SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_MMS_INTERFACES | 1.345 + SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_SUPL_INTERFACES | 1.346 + SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_IMS_INTERFACES | 1.347 + SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_DUN_INTERFACES); 1.348 + var num = itfList.getNumberOfInterface(); 1.349 + for (var i = 0; i < num; i++) { 1.350 + var ips = {}; 1.351 + var prefixLengths = {}; 1.352 + var length = itfList.getInterface(i).getAddresses(ips, prefixLengths); 1.353 + 1.354 + for (var j = 0; j < length; j++) { 1.355 + var ip = ips.value[j]; 1.356 + // skip IPv6 address 1.357 + if (ip.indexOf(":") < 0) { 1.358 + info("Network interface is ready with address: " + ip); 1.359 + return true; 1.360 + } 1.361 + } 1.362 + } 1.363 + // ip address is not available 1.364 + info("Network interface is not ready, required additional network setup"); 1.365 + return false; 1.366 + } 1.367 + info("Network setup is not required"); 1.368 + return true; 1.369 +} 1.370 + 1.371 +/** 1.372 + * Network setup utils for Gonk 1.373 + * 1.374 + * @return {object} providing functions for setup/teardown data connection 1.375 + */ 1.376 +function getNetworkUtils() { 1.377 + var url = SimpleTest.getTestFileURL("NetworkPreparationChromeScript.js"); 1.378 + var script = SpecialPowers.loadChromeScript(url); 1.379 + 1.380 + var utils = { 1.381 + /** 1.382 + * Utility for setting up data connection. 1.383 + * 1.384 + * @param aCallback callback after data connection is ready. 1.385 + */ 1.386 + prepareNetwork: function(aCallback) { 1.387 + script.addMessageListener('network-ready', function (message) { 1.388 + info("Network interface is ready"); 1.389 + aCallback(); 1.390 + }); 1.391 + info("Setup network interface"); 1.392 + script.sendAsyncMessage("prepare-network", true); 1.393 + }, 1.394 + /** 1.395 + * Utility for tearing down data connection. 1.396 + * 1.397 + * @param aCallback callback after data connection is closed. 1.398 + */ 1.399 + tearDownNetwork: function(aCallback) { 1.400 + script.addMessageListener('network-disabled', function (message) { 1.401 + ok(true, 'network-disabled'); 1.402 + script.destroy(); 1.403 + aCallback(); 1.404 + }); 1.405 + script.sendAsyncMessage("network-cleanup", true); 1.406 + } 1.407 + }; 1.408 + 1.409 + return utils; 1.410 +} 1.411 + 1.412 +/** 1.413 + * This class handles tests for peer connections. 1.414 + * 1.415 + * @constructor 1.416 + * @param {object} [options={}] 1.417 + * Optional options for the peer connection test 1.418 + * @param {object} [options.commands=commandsPeerConnection] 1.419 + * Commands to run for the test 1.420 + * @param {bool} [options.is_local=true] 1.421 + * true if this test should run the tests for the "local" side. 1.422 + * @param {bool} [options.is_remote=true] 1.423 + * true if this test should run the tests for the "remote" side. 1.424 + * @param {object} [options.config_pc1=undefined] 1.425 + * Configuration for the local peer connection instance 1.426 + * @param {object} [options.config_pc2=undefined] 1.427 + * Configuration for the remote peer connection instance. If not defined 1.428 + * the configuration from the local instance will be used 1.429 + */ 1.430 +function PeerConnectionTest(options) { 1.431 + // If no options are specified make it an empty object 1.432 + options = options || { }; 1.433 + options.commands = options.commands || commandsPeerConnection; 1.434 + options.is_local = "is_local" in options ? options.is_local : true; 1.435 + options.is_remote = "is_remote" in options ? options.is_remote : true; 1.436 + 1.437 + var netTeardownCommand = null; 1.438 + if (!isNetworkReady()) { 1.439 + var utils = getNetworkUtils(); 1.440 + // Trigger network setup to obtain IP address before creating any PeerConnection. 1.441 + utils.prepareNetwork(function() { 1.442 + ok(isNetworkReady(),'setup network connection successfully'); 1.443 + }); 1.444 + 1.445 + netTeardownCommand = [ 1.446 + [ 1.447 + 'TEARDOWN_NETWORK', 1.448 + function(test) { 1.449 + utils.tearDownNetwork(function() { 1.450 + info('teardown network connection'); 1.451 + test.next(); 1.452 + }); 1.453 + } 1.454 + ] 1.455 + ]; 1.456 + } 1.457 + 1.458 + if (options.is_local) 1.459 + this.pcLocal = new PeerConnectionWrapper('pcLocal', options.config_pc1); 1.460 + else 1.461 + this.pcLocal = null; 1.462 + 1.463 + if (options.is_remote) 1.464 + this.pcRemote = new PeerConnectionWrapper('pcRemote', options.config_pc2 || options.config_pc1); 1.465 + else 1.466 + this.pcRemote = null; 1.467 + 1.468 + this.connected = false; 1.469 + 1.470 + // Create command chain instance and assign default commands 1.471 + this.chain = new CommandChain(this, options.commands); 1.472 + if (!options.is_local) { 1.473 + this.chain.filterOut(/^PC_LOCAL/); 1.474 + } 1.475 + if (!options.is_remote) { 1.476 + this.chain.filterOut(/^PC_REMOTE/); 1.477 + } 1.478 + 1.479 + // Insert network teardown after testcase execution. 1.480 + if (netTeardownCommand) { 1.481 + this.chain.append(netTeardownCommand); 1.482 + } 1.483 + 1.484 + var self = this; 1.485 + this.chain.onFinished = function () { 1.486 + self.teardown(); 1.487 + }; 1.488 +} 1.489 + 1.490 +/** 1.491 + * Closes the peer connection if it is active 1.492 + * 1.493 + * @param {Function} onSuccess 1.494 + * Callback to execute when the peer connection has been closed successfully 1.495 + */ 1.496 +PeerConnectionTest.prototype.close = function PCT_close(onSuccess) { 1.497 + info("Closing peer connections. Connection state=" + this.connected); 1.498 + 1.499 + function signalingstatechangeClose(state) { 1.500 + info("'onsignalingstatechange' event '" + state + "' received"); 1.501 + is(state, "closed", "onsignalingstatechange event is closed"); 1.502 + } 1.503 + 1.504 + // There is no onclose event for the remote peer existent yet. So close it 1.505 + // side-by-side with the local peer. 1.506 + if (this.pcLocal) { 1.507 + this.pcLocal.onsignalingstatechange = signalingstatechangeClose; 1.508 + this.pcLocal.close(); 1.509 + } 1.510 + if (this.pcRemote) { 1.511 + this.pcRemote.onsignalingstatechange = signalingstatechangeClose; 1.512 + this.pcRemote.close(); 1.513 + } 1.514 + this.connected = false; 1.515 + 1.516 + onSuccess(); 1.517 +}; 1.518 + 1.519 +/** 1.520 + * Executes the next command. 1.521 + */ 1.522 +PeerConnectionTest.prototype.next = function PCT_next() { 1.523 + if (this._stepTimeout) { 1.524 + clearTimeout(this._stepTimeout); 1.525 + this._stepTimeout = null; 1.526 + } 1.527 + this.chain.executeNext(); 1.528 +}; 1.529 + 1.530 +/** 1.531 + * Set a timeout for the current step. 1.532 + * @param {long] ms the number of milliseconds to allow for this step 1.533 + */ 1.534 +PeerConnectionTest.prototype.setStepTimeout = function(ms) { 1.535 + this._stepTimeout = setTimeout(function() { 1.536 + ok(false, "Step timed out: " + this.chain.currentStepLabel); 1.537 + this.next(); 1.538 + }.bind(this), ms); 1.539 +}; 1.540 + 1.541 +/** 1.542 + * Creates an answer for the specified peer connection instance 1.543 + * and automatically handles the failure case. 1.544 + * 1.545 + * @param {PeerConnectionWrapper} peer 1.546 + * The peer connection wrapper to run the command on 1.547 + * @param {function} onSuccess 1.548 + * Callback to execute if the offer was created successfully 1.549 + */ 1.550 +PeerConnectionTest.prototype.createAnswer = 1.551 +function PCT_createAnswer(peer, onSuccess) { 1.552 + peer.createAnswer(function (answer) { 1.553 + onSuccess(answer); 1.554 + }); 1.555 +}; 1.556 + 1.557 +/** 1.558 + * Creates an offer for the specified peer connection instance 1.559 + * and automatically handles the failure case. 1.560 + * 1.561 + * @param {PeerConnectionWrapper} peer 1.562 + * The peer connection wrapper to run the command on 1.563 + * @param {function} onSuccess 1.564 + * Callback to execute if the offer was created successfully 1.565 + */ 1.566 +PeerConnectionTest.prototype.createOffer = 1.567 +function PCT_createOffer(peer, onSuccess) { 1.568 + peer.createOffer(function (offer) { 1.569 + onSuccess(offer); 1.570 + }); 1.571 +}; 1.572 + 1.573 +PeerConnectionTest.prototype.setIdentityProvider = 1.574 +function(peer, provider, protocol, identity) { 1.575 + peer.setIdentityProvider(provider, protocol, identity); 1.576 +}; 1.577 + 1.578 +/** 1.579 + * Sets the local description for the specified peer connection instance 1.580 + * and automatically handles the failure case. 1.581 + * 1.582 + * @param {PeerConnectionWrapper} peer 1.583 + The peer connection wrapper to run the command on 1.584 + * @param {mozRTCSessionDescription} desc 1.585 + * Session description for the local description request 1.586 + * @param {function} onSuccess 1.587 + * Callback to execute if the local description was set successfully 1.588 + */ 1.589 +PeerConnectionTest.prototype.setLocalDescription = 1.590 +function PCT_setLocalDescription(peer, desc, stateExpected, onSuccess) { 1.591 + var eventFired = false; 1.592 + var stateChanged = false; 1.593 + 1.594 + function check_next_test() { 1.595 + if (eventFired && stateChanged) { 1.596 + onSuccess(); 1.597 + } 1.598 + } 1.599 + 1.600 + peer.onsignalingstatechange = function (state) { 1.601 + //info(peer + ": 'onsignalingstatechange' event registered, signalingState: " + peer.signalingState); 1.602 + info(peer + ": 'onsignalingstatechange' event '" + state + "' received"); 1.603 + if(stateExpected === state && eventFired == false) { 1.604 + eventFired = true; 1.605 + check_next_test(); 1.606 + } else { 1.607 + ok(false, "This event has either already fired or there has been a " + 1.608 + "mismatch between event received " + state + 1.609 + " and event expected " + stateExpected); 1.610 + } 1.611 + }; 1.612 + 1.613 + peer.setLocalDescription(desc, function () { 1.614 + stateChanged = true; 1.615 + check_next_test(); 1.616 + }); 1.617 +}; 1.618 + 1.619 +/** 1.620 + * Sets the media constraints for both peer connection instances. 1.621 + * 1.622 + * @param {object} constraintsLocal 1.623 + * Media constrains for the local peer connection instance 1.624 + * @param constraintsRemote 1.625 + */ 1.626 +PeerConnectionTest.prototype.setMediaConstraints = 1.627 +function PCT_setMediaConstraints(constraintsLocal, constraintsRemote) { 1.628 + if (this.pcLocal) 1.629 + this.pcLocal.constraints = constraintsLocal; 1.630 + if (this.pcRemote) 1.631 + this.pcRemote.constraints = constraintsRemote; 1.632 +}; 1.633 + 1.634 +/** 1.635 + * Sets the media constraints used on a createOffer call in the test. 1.636 + * 1.637 + * @param {object} constraints the media constraints to use on createOffer 1.638 + */ 1.639 +PeerConnectionTest.prototype.setOfferConstraints = 1.640 +function PCT_setOfferConstraints(constraints) { 1.641 + if (this.pcLocal) 1.642 + this.pcLocal.offerConstraints = constraints; 1.643 +}; 1.644 + 1.645 +/** 1.646 + * Sets the remote description for the specified peer connection instance 1.647 + * and automatically handles the failure case. 1.648 + * 1.649 + * @param {PeerConnectionWrapper} peer 1.650 + The peer connection wrapper to run the command on 1.651 + * @param {mozRTCSessionDescription} desc 1.652 + * Session description for the remote description request 1.653 + * @param {function} onSuccess 1.654 + * Callback to execute if the local description was set successfully 1.655 + */ 1.656 +PeerConnectionTest.prototype.setRemoteDescription = 1.657 +function PCT_setRemoteDescription(peer, desc, stateExpected, onSuccess) { 1.658 + var eventFired = false; 1.659 + var stateChanged = false; 1.660 + 1.661 + function check_next_test() { 1.662 + if (eventFired && stateChanged) { 1.663 + onSuccess(); 1.664 + } 1.665 + } 1.666 + 1.667 + peer.onsignalingstatechange = function (state) { 1.668 + info(peer + ": 'onsignalingstatechange' event '" + state + "' received"); 1.669 + if(stateExpected === state && eventFired == false) { 1.670 + eventFired = true; 1.671 + check_next_test(); 1.672 + } else { 1.673 + ok(false, "This event has either already fired or there has been a " + 1.674 + "mismatch between event received " + state + 1.675 + " and event expected " + stateExpected); 1.676 + } 1.677 + }; 1.678 + 1.679 + peer.setRemoteDescription(desc, function () { 1.680 + stateChanged = true; 1.681 + check_next_test(); 1.682 + }); 1.683 +}; 1.684 + 1.685 +/** 1.686 + * Start running the tests as assigned to the command chain. 1.687 + */ 1.688 +PeerConnectionTest.prototype.run = function PCT_run() { 1.689 + this.next(); 1.690 +}; 1.691 + 1.692 +/** 1.693 + * Clean up the objects used by the test 1.694 + */ 1.695 +PeerConnectionTest.prototype.teardown = function PCT_teardown() { 1.696 + this.close(function () { 1.697 + info("Test finished"); 1.698 + if (window.SimpleTest) 1.699 + SimpleTest.finish(); 1.700 + else 1.701 + finish(); 1.702 + }); 1.703 +}; 1.704 + 1.705 +/** 1.706 + * This class handles tests for data channels. 1.707 + * 1.708 + * @constructor 1.709 + * @param {object} [options={}] 1.710 + * Optional options for the peer connection test 1.711 + * @param {object} [options.commands=commandsDataChannel] 1.712 + * Commands to run for the test 1.713 + * @param {object} [options.config_pc1=undefined] 1.714 + * Configuration for the local peer connection instance 1.715 + * @param {object} [options.config_pc2=undefined] 1.716 + * Configuration for the remote peer connection instance. If not defined 1.717 + * the configuration from the local instance will be used 1.718 + */ 1.719 +function DataChannelTest(options) { 1.720 + options = options || { }; 1.721 + options.commands = options.commands || commandsDataChannel; 1.722 + 1.723 + PeerConnectionTest.call(this, options); 1.724 +} 1.725 + 1.726 +DataChannelTest.prototype = Object.create(PeerConnectionTest.prototype, { 1.727 + close : { 1.728 + /** 1.729 + * Close the open data channels, followed by the underlying peer connection 1.730 + * 1.731 + * @param {Function} onSuccess 1.732 + * Callback to execute when the connection has been closed 1.733 + */ 1.734 + value : function DCT_close(onSuccess) { 1.735 + var self = this; 1.736 + 1.737 + function _closeChannels() { 1.738 + var length = self.pcLocal.dataChannels.length; 1.739 + 1.740 + if (length > 0) { 1.741 + self.closeDataChannel(length - 1, function () { 1.742 + _closeChannels(); 1.743 + }); 1.744 + } 1.745 + else { 1.746 + PeerConnectionTest.prototype.close.call(self, onSuccess); 1.747 + } 1.748 + } 1.749 + 1.750 + _closeChannels(); 1.751 + } 1.752 + }, 1.753 + 1.754 + closeDataChannel : { 1.755 + /** 1.756 + * Close the specified data channel 1.757 + * 1.758 + * @param {Number} index 1.759 + * Index of the data channel to close on both sides 1.760 + * @param {Function} onSuccess 1.761 + * Callback to execute when the data channel has been closed 1.762 + */ 1.763 + value : function DCT_closeDataChannel(index, onSuccess) { 1.764 + var localChannel = this.pcLocal.dataChannels[index]; 1.765 + var remoteChannel = this.pcRemote.dataChannels[index]; 1.766 + 1.767 + var self = this; 1.768 + 1.769 + // Register handler for remote channel, cause we have to wait until 1.770 + // the current close operation has been finished. 1.771 + remoteChannel.onclose = function () { 1.772 + self.pcRemote.dataChannels.splice(index, 1); 1.773 + 1.774 + onSuccess(remoteChannel); 1.775 + }; 1.776 + 1.777 + localChannel.close(); 1.778 + this.pcLocal.dataChannels.splice(index, 1); 1.779 + } 1.780 + }, 1.781 + 1.782 + createDataChannel : { 1.783 + /** 1.784 + * Create a data channel 1.785 + * 1.786 + * @param {Dict} options 1.787 + * Options for the data channel (see nsIPeerConnection) 1.788 + * @param {Function} onSuccess 1.789 + * Callback when the creation was successful 1.790 + */ 1.791 + value : function DCT_createDataChannel(options, onSuccess) { 1.792 + var localChannel = null; 1.793 + var remoteChannel = null; 1.794 + var self = this; 1.795 + 1.796 + // Method to synchronize all asynchronous events. 1.797 + function check_next_test() { 1.798 + if (self.connected && localChannel && remoteChannel) { 1.799 + onSuccess(localChannel, remoteChannel); 1.800 + } 1.801 + } 1.802 + 1.803 + if (!options.negotiated) { 1.804 + // Register handlers for the remote peer 1.805 + this.pcRemote.registerDataChannelOpenEvents(function (channel) { 1.806 + remoteChannel = channel; 1.807 + check_next_test(); 1.808 + }); 1.809 + } 1.810 + 1.811 + // Create the datachannel and handle the local 'onopen' event 1.812 + this.pcLocal.createDataChannel(options, function (channel) { 1.813 + localChannel = channel; 1.814 + 1.815 + if (options.negotiated) { 1.816 + // externally negotiated - we need to open from both ends 1.817 + options.id = options.id || channel.id; // allow for no id to let the impl choose 1.818 + self.pcRemote.createDataChannel(options, function (channel) { 1.819 + remoteChannel = channel; 1.820 + check_next_test(); 1.821 + }); 1.822 + } else { 1.823 + check_next_test(); 1.824 + } 1.825 + }); 1.826 + } 1.827 + }, 1.828 + 1.829 + send : { 1.830 + /** 1.831 + * Send data (message or blob) to the other peer 1.832 + * 1.833 + * @param {String|Blob} data 1.834 + * Data to send to the other peer. For Blobs the MIME type will be lost. 1.835 + * @param {Function} onSuccess 1.836 + * Callback to execute when data has been sent 1.837 + * @param {Object} [options={ }] 1.838 + * Options to specify the data channels to be used 1.839 + * @param {DataChannelWrapper} [options.sourceChannel=pcLocal.dataChannels[length - 1]] 1.840 + * Data channel to use for sending the message 1.841 + * @param {DataChannelWrapper} [options.targetChannel=pcRemote.dataChannels[length - 1]] 1.842 + * Data channel to use for receiving the message 1.843 + */ 1.844 + value : function DCT_send(data, onSuccess, options) { 1.845 + options = options || { }; 1.846 + var source = options.sourceChannel || 1.847 + this.pcLocal.dataChannels[this.pcLocal.dataChannels.length - 1]; 1.848 + var target = options.targetChannel || 1.849 + this.pcRemote.dataChannels[this.pcRemote.dataChannels.length - 1]; 1.850 + 1.851 + // Register event handler for the target channel 1.852 + target.onmessage = function (recv_data) { 1.853 + onSuccess(target, recv_data); 1.854 + }; 1.855 + 1.856 + source.send(data); 1.857 + } 1.858 + }, 1.859 + 1.860 + setLocalDescription : { 1.861 + /** 1.862 + * Sets the local description for the specified peer connection instance 1.863 + * and automatically handles the failure case. In case for the final call 1.864 + * it will setup the requested datachannel. 1.865 + * 1.866 + * @param {PeerConnectionWrapper} peer 1.867 + The peer connection wrapper to run the command on 1.868 + * @param {mozRTCSessionDescription} desc 1.869 + * Session description for the local description request 1.870 + * @param {function} onSuccess 1.871 + * Callback to execute if the local description was set successfully 1.872 + */ 1.873 + value : function DCT_setLocalDescription(peer, desc, state, onSuccess) { 1.874 + // If the peer has a remote offer we are in the final call, and have 1.875 + // to wait for the datachannel connection to be open. It will also set 1.876 + // the local description internally. 1.877 + if (peer.signalingState === 'have-remote-offer') { 1.878 + this.waitForInitialDataChannel(peer, desc, state, onSuccess); 1.879 + } 1.880 + else { 1.881 + PeerConnectionTest.prototype.setLocalDescription.call(this, peer, 1.882 + desc, state, onSuccess); 1.883 + } 1.884 + 1.885 + } 1.886 + }, 1.887 + 1.888 + waitForInitialDataChannel : { 1.889 + /** 1.890 + * Create an initial data channel before the peer connection has been connected 1.891 + * 1.892 + * @param {PeerConnectionWrapper} peer 1.893 + The peer connection wrapper to run the command on 1.894 + * @param {mozRTCSessionDescription} desc 1.895 + * Session description for the local description request 1.896 + * @param {Function} onSuccess 1.897 + * Callback when the creation was successful 1.898 + */ 1.899 + value : function DCT_waitForInitialDataChannel(peer, desc, state, onSuccess) { 1.900 + var self = this; 1.901 + 1.902 + var targetPeer = peer; 1.903 + var targetChannel = null; 1.904 + 1.905 + var sourcePeer = (peer == this.pcLocal) ? this.pcRemote : this.pcLocal; 1.906 + var sourceChannel = null; 1.907 + 1.908 + // Method to synchronize all asynchronous events which current happen 1.909 + // due to a non-predictable flow. With bug 875346 fixed we will be able 1.910 + // to simplify this code. 1.911 + function check_next_test() { 1.912 + if (self.connected && sourceChannel && targetChannel) { 1.913 + onSuccess(sourceChannel, targetChannel); 1.914 + } 1.915 + } 1.916 + 1.917 + // Register 'onopen' handler for the first local data channel 1.918 + sourcePeer.dataChannels[0].onopen = function (channel) { 1.919 + sourceChannel = channel; 1.920 + check_next_test(); 1.921 + }; 1.922 + 1.923 + // Register handlers for the target peer 1.924 + targetPeer.registerDataChannelOpenEvents(function (channel) { 1.925 + targetChannel = channel; 1.926 + check_next_test(); 1.927 + }); 1.928 + 1.929 + PeerConnectionTest.prototype.setLocalDescription.call(this, targetPeer, desc, 1.930 + state, 1.931 + function () { 1.932 + self.connected = true; 1.933 + check_next_test(); 1.934 + } 1.935 + ); 1.936 + } 1.937 + } 1.938 +}); 1.939 + 1.940 +/** 1.941 + * This class acts as a wrapper around a DataChannel instance. 1.942 + * 1.943 + * @param dataChannel 1.944 + * @param peerConnectionWrapper 1.945 + * @constructor 1.946 + */ 1.947 +function DataChannelWrapper(dataChannel, peerConnectionWrapper) { 1.948 + this._channel = dataChannel; 1.949 + this._pc = peerConnectionWrapper; 1.950 + 1.951 + info("Creating " + this); 1.952 + 1.953 + /** 1.954 + * Setup appropriate callbacks 1.955 + */ 1.956 + 1.957 + this.onclose = unexpectedEventAndFinish(this, 'onclose'); 1.958 + this.onerror = unexpectedEventAndFinish(this, 'onerror'); 1.959 + this.onmessage = unexpectedEventAndFinish(this, 'onmessage'); 1.960 + this.onopen = unexpectedEventAndFinish(this, 'onopen'); 1.961 + 1.962 + var self = this; 1.963 + 1.964 + /** 1.965 + * Callback for native data channel 'onclose' events. If no custom handler 1.966 + * has been specified via 'this.onclose', a failure will be raised if an 1.967 + * event of this type gets caught. 1.968 + */ 1.969 + this._channel.onclose = function () { 1.970 + info(self + ": 'onclose' event fired"); 1.971 + 1.972 + self.onclose(self); 1.973 + self.onclose = unexpectedEventAndFinish(self, 'onclose'); 1.974 + }; 1.975 + 1.976 + /** 1.977 + * Callback for native data channel 'onmessage' events. If no custom handler 1.978 + * has been specified via 'this.onmessage', a failure will be raised if an 1.979 + * event of this type gets caught. 1.980 + * 1.981 + * @param {Object} event 1.982 + * Event data which includes the sent message 1.983 + */ 1.984 + this._channel.onmessage = function (event) { 1.985 + info(self + ": 'onmessage' event fired for '" + event.data + "'"); 1.986 + 1.987 + self.onmessage(event.data); 1.988 + self.onmessage = unexpectedEventAndFinish(self, 'onmessage'); 1.989 + }; 1.990 + 1.991 + /** 1.992 + * Callback for native data channel 'onopen' events. If no custom handler 1.993 + * has been specified via 'this.onopen', a failure will be raised if an 1.994 + * event of this type gets caught. 1.995 + */ 1.996 + this._channel.onopen = function () { 1.997 + info(self + ": 'onopen' event fired"); 1.998 + 1.999 + self.onopen(self); 1.1000 + self.onopen = unexpectedEventAndFinish(self, 'onopen'); 1.1001 + }; 1.1002 +} 1.1003 + 1.1004 +DataChannelWrapper.prototype = { 1.1005 + /** 1.1006 + * Returns the binary type of the channel 1.1007 + * 1.1008 + * @returns {String} The binary type 1.1009 + */ 1.1010 + get binaryType() { 1.1011 + return this._channel.binaryType; 1.1012 + }, 1.1013 + 1.1014 + /** 1.1015 + * Sets the binary type of the channel 1.1016 + * 1.1017 + * @param {String} type 1.1018 + * The new binary type of the channel 1.1019 + */ 1.1020 + set binaryType(type) { 1.1021 + this._channel.binaryType = type; 1.1022 + }, 1.1023 + 1.1024 + /** 1.1025 + * Returns the label of the underlying data channel 1.1026 + * 1.1027 + * @returns {String} The label 1.1028 + */ 1.1029 + get label() { 1.1030 + return this._channel.label; 1.1031 + }, 1.1032 + 1.1033 + /** 1.1034 + * Returns the protocol of the underlying data channel 1.1035 + * 1.1036 + * @returns {String} The protocol 1.1037 + */ 1.1038 + get protocol() { 1.1039 + return this._channel.protocol; 1.1040 + }, 1.1041 + 1.1042 + /** 1.1043 + * Returns the id of the underlying data channel 1.1044 + * 1.1045 + * @returns {number} The stream id 1.1046 + */ 1.1047 + get id() { 1.1048 + return this._channel.id; 1.1049 + }, 1.1050 + 1.1051 + /** 1.1052 + * Returns the reliable state of the underlying data channel 1.1053 + * 1.1054 + * @returns {bool} The stream's reliable state 1.1055 + */ 1.1056 + get reliable() { 1.1057 + return this._channel.reliable; 1.1058 + }, 1.1059 + 1.1060 + // ordered, maxRetransmits and maxRetransmitTime not exposed yet 1.1061 + 1.1062 + /** 1.1063 + * Returns the readyState bit of the data channel 1.1064 + * 1.1065 + * @returns {String} The state of the channel 1.1066 + */ 1.1067 + get readyState() { 1.1068 + return this._channel.readyState; 1.1069 + }, 1.1070 + 1.1071 + /** 1.1072 + * Close the data channel 1.1073 + */ 1.1074 + close : function () { 1.1075 + info(this + ": Closing channel"); 1.1076 + this._channel.close(); 1.1077 + }, 1.1078 + 1.1079 + /** 1.1080 + * Send data through the data channel 1.1081 + * 1.1082 + * @param {String|Object} data 1.1083 + * Data which has to be sent through the data channel 1.1084 + */ 1.1085 + send: function DCW_send(data) { 1.1086 + info(this + ": Sending data '" + data + "'"); 1.1087 + this._channel.send(data); 1.1088 + }, 1.1089 + 1.1090 + /** 1.1091 + * Returns the string representation of the class 1.1092 + * 1.1093 + * @returns {String} The string representation 1.1094 + */ 1.1095 + toString: function DCW_toString() { 1.1096 + return "DataChannelWrapper (" + this._pc.label + '_' + this._channel.label + ")"; 1.1097 + } 1.1098 +}; 1.1099 + 1.1100 + 1.1101 +/** 1.1102 + * This class acts as a wrapper around a PeerConnection instance. 1.1103 + * 1.1104 + * @constructor 1.1105 + * @param {string} label 1.1106 + * Description for the peer connection instance 1.1107 + * @param {object} configuration 1.1108 + * Configuration for the peer connection instance 1.1109 + */ 1.1110 +function PeerConnectionWrapper(label, configuration) { 1.1111 + this.configuration = configuration; 1.1112 + this.label = label; 1.1113 + this.whenCreated = Date.now(); 1.1114 + 1.1115 + this.constraints = [ ]; 1.1116 + this.offerConstraints = {}; 1.1117 + this.streams = [ ]; 1.1118 + this.mediaCheckers = [ ]; 1.1119 + 1.1120 + this.dataChannels = [ ]; 1.1121 + 1.1122 + info("Creating " + this); 1.1123 + this._pc = new mozRTCPeerConnection(this.configuration); 1.1124 + is(this._pc.iceConnectionState, "new", "iceConnectionState starts at 'new'"); 1.1125 + 1.1126 + /** 1.1127 + * Setup callback handlers 1.1128 + */ 1.1129 + var self = this; 1.1130 + // This enables tests to validate that the next ice state is the one they expect to happen 1.1131 + this.next_ice_state = ""; // in most cases, the next state will be "checking", but in some tests "closed" 1.1132 + // This allows test to register their own callbacks for ICE connection state changes 1.1133 + this.ice_connection_callbacks = {}; 1.1134 + 1.1135 + this._pc.oniceconnectionstatechange = function() { 1.1136 + ok(self._pc.iceConnectionState !== undefined, "iceConnectionState should not be undefined"); 1.1137 + info(self + ": oniceconnectionstatechange fired, new state is: " + self._pc.iceConnectionState); 1.1138 + Object.keys(self.ice_connection_callbacks).forEach(function(name) { 1.1139 + self.ice_connection_callbacks[name](); 1.1140 + }); 1.1141 + if (self.next_ice_state !== "") { 1.1142 + is(self._pc.iceConnectionState, self.next_ice_state, "iceConnectionState changed to '" + 1.1143 + self.next_ice_state + "'"); 1.1144 + self.next_ice_state = ""; 1.1145 + } 1.1146 + }; 1.1147 + this.ondatachannel = unexpectedEventAndFinish(this, 'ondatachannel'); 1.1148 + this.onsignalingstatechange = unexpectedEventAndFinish(this, 'onsignalingstatechange'); 1.1149 + 1.1150 + /** 1.1151 + * Callback for native peer connection 'onaddstream' events. 1.1152 + * 1.1153 + * @param {Object} event 1.1154 + * Event data which includes the stream to be added 1.1155 + */ 1.1156 + this._pc.onaddstream = function (event) { 1.1157 + info(self + ": 'onaddstream' event fired for " + JSON.stringify(event.stream)); 1.1158 + 1.1159 + var type = ''; 1.1160 + if (event.stream.getAudioTracks().length > 0) { 1.1161 + type = 'audio'; 1.1162 + } 1.1163 + if (event.stream.getVideoTracks().length > 0) { 1.1164 + type += 'video'; 1.1165 + } 1.1166 + self.attachMedia(event.stream, type, 'remote'); 1.1167 + }; 1.1168 + 1.1169 + /** 1.1170 + * Callback for native peer connection 'ondatachannel' events. If no custom handler 1.1171 + * has been specified via 'this.ondatachannel', a failure will be raised if an 1.1172 + * event of this type gets caught. 1.1173 + * 1.1174 + * @param {Object} event 1.1175 + * Event data which includes the newly created data channel 1.1176 + */ 1.1177 + this._pc.ondatachannel = function (event) { 1.1178 + info(self + ": 'ondatachannel' event fired for " + event.channel.label); 1.1179 + 1.1180 + self.ondatachannel(new DataChannelWrapper(event.channel, self)); 1.1181 + self.ondatachannel = unexpectedEventAndFinish(self, 'ondatachannel'); 1.1182 + }; 1.1183 + 1.1184 + /** 1.1185 + * Callback for native peer connection 'onsignalingstatechange' events. If no 1.1186 + * custom handler has been specified via 'this.onsignalingstatechange', a 1.1187 + * failure will be raised if an event of this type is caught. 1.1188 + * 1.1189 + * @param {Object} aEvent 1.1190 + * Event data which includes the newly created data channel 1.1191 + */ 1.1192 + this._pc.onsignalingstatechange = function (aEvent) { 1.1193 + info(self + ": 'onsignalingstatechange' event fired"); 1.1194 + 1.1195 + // this calls the eventhandler only once and then overwrites it with the 1.1196 + // default unexpectedEvent handler 1.1197 + self.onsignalingstatechange(aEvent); 1.1198 + self.onsignalingstatechange = unexpectedEventAndFinish(self, 'onsignalingstatechange'); 1.1199 + }; 1.1200 +} 1.1201 + 1.1202 +PeerConnectionWrapper.prototype = { 1.1203 + 1.1204 + /** 1.1205 + * Returns the local description. 1.1206 + * 1.1207 + * @returns {object} The local description 1.1208 + */ 1.1209 + get localDescription() { 1.1210 + return this._pc.localDescription; 1.1211 + }, 1.1212 + 1.1213 + /** 1.1214 + * Sets the local description. 1.1215 + * 1.1216 + * @param {object} desc 1.1217 + * The new local description 1.1218 + */ 1.1219 + set localDescription(desc) { 1.1220 + this._pc.localDescription = desc; 1.1221 + }, 1.1222 + 1.1223 + /** 1.1224 + * Returns the readyState. 1.1225 + * 1.1226 + * @returns {string} 1.1227 + */ 1.1228 + get readyState() { 1.1229 + return this._pc.readyState; 1.1230 + }, 1.1231 + 1.1232 + /** 1.1233 + * Returns the remote description. 1.1234 + * 1.1235 + * @returns {object} The remote description 1.1236 + */ 1.1237 + get remoteDescription() { 1.1238 + return this._pc.remoteDescription; 1.1239 + }, 1.1240 + 1.1241 + /** 1.1242 + * Sets the remote description. 1.1243 + * 1.1244 + * @param {object} desc 1.1245 + * The new remote description 1.1246 + */ 1.1247 + set remoteDescription(desc) { 1.1248 + this._pc.remoteDescription = desc; 1.1249 + }, 1.1250 + 1.1251 + /** 1.1252 + * Returns the signaling state. 1.1253 + * 1.1254 + * @returns {object} The local description 1.1255 + */ 1.1256 + get signalingState() { 1.1257 + return this._pc.signalingState; 1.1258 + }, 1.1259 + /** 1.1260 + * Returns the ICE connection state. 1.1261 + * 1.1262 + * @returns {object} The local description 1.1263 + */ 1.1264 + get iceConnectionState() { 1.1265 + return this._pc.iceConnectionState; 1.1266 + }, 1.1267 + 1.1268 + setIdentityProvider: function(provider, protocol, identity) { 1.1269 + this._pc.setIdentityProvider(provider, protocol, identity); 1.1270 + }, 1.1271 + 1.1272 + /** 1.1273 + * Callback when we get media from either side. Also an appropriate 1.1274 + * HTML media element will be created. 1.1275 + * 1.1276 + * @param {MediaStream} stream 1.1277 + * Media stream to handle 1.1278 + * @param {string} type 1.1279 + * The type of media stream ('audio' or 'video') 1.1280 + * @param {string} side 1.1281 + * The location the stream is coming from ('local' or 'remote') 1.1282 + */ 1.1283 + attachMedia : function PCW_attachMedia(stream, type, side) { 1.1284 + info("Got media stream: " + type + " (" + side + ")"); 1.1285 + this.streams.push(stream); 1.1286 + 1.1287 + if (side === 'local') { 1.1288 + this._pc.addStream(stream); 1.1289 + } 1.1290 + 1.1291 + var element = createMediaElement(type, this.label + '_' + side); 1.1292 + this.mediaCheckers.push(new MediaElementChecker(element)); 1.1293 + element.mozSrcObject = stream; 1.1294 + element.play(); 1.1295 + }, 1.1296 + 1.1297 + /** 1.1298 + * Requests all the media streams as specified in the constrains property. 1.1299 + * 1.1300 + * @param {function} onSuccess 1.1301 + * Callback to execute if all media has been requested successfully 1.1302 + */ 1.1303 + getAllUserMedia : function PCW_GetAllUserMedia(onSuccess) { 1.1304 + var self = this; 1.1305 + 1.1306 + function _getAllUserMedia(constraintsList, index) { 1.1307 + if (index < constraintsList.length) { 1.1308 + var constraints = constraintsList[index]; 1.1309 + 1.1310 + getUserMedia(constraints, function (stream) { 1.1311 + var type = ''; 1.1312 + 1.1313 + if (constraints.audio) { 1.1314 + type = 'audio'; 1.1315 + } 1.1316 + 1.1317 + if (constraints.video) { 1.1318 + type += 'video'; 1.1319 + } 1.1320 + 1.1321 + self.attachMedia(stream, type, 'local'); 1.1322 + 1.1323 + _getAllUserMedia(constraintsList, index + 1); 1.1324 + }, generateErrorCallback()); 1.1325 + } else { 1.1326 + onSuccess(); 1.1327 + } 1.1328 + } 1.1329 + 1.1330 + info("Get " + this.constraints.length + " local streams"); 1.1331 + _getAllUserMedia(this.constraints, 0); 1.1332 + }, 1.1333 + 1.1334 + /** 1.1335 + * Create a new data channel instance 1.1336 + * 1.1337 + * @param {Object} options 1.1338 + * Options which get forwarded to nsIPeerConnection.createDataChannel 1.1339 + * @param {function} [onCreation=undefined] 1.1340 + * Callback to execute when the local data channel has been created 1.1341 + * @returns {DataChannelWrapper} The created data channel 1.1342 + */ 1.1343 + createDataChannel : function PCW_createDataChannel(options, onCreation) { 1.1344 + var label = 'channel_' + this.dataChannels.length; 1.1345 + info(this + ": Create data channel '" + label); 1.1346 + 1.1347 + var channel = this._pc.createDataChannel(label, options); 1.1348 + var wrapper = new DataChannelWrapper(channel, this); 1.1349 + 1.1350 + if (onCreation) { 1.1351 + wrapper.onopen = function () { 1.1352 + onCreation(wrapper); 1.1353 + }; 1.1354 + } 1.1355 + 1.1356 + this.dataChannels.push(wrapper); 1.1357 + return wrapper; 1.1358 + }, 1.1359 + 1.1360 + /** 1.1361 + * Creates an offer and automatically handles the failure case. 1.1362 + * 1.1363 + * @param {function} onSuccess 1.1364 + * Callback to execute if the offer was created successfully 1.1365 + */ 1.1366 + createOffer : function PCW_createOffer(onSuccess) { 1.1367 + var self = this; 1.1368 + 1.1369 + this._pc.createOffer(function (offer) { 1.1370 + info("Got offer: " + JSON.stringify(offer)); 1.1371 + self._last_offer = offer; 1.1372 + onSuccess(offer); 1.1373 + }, generateErrorCallback(), this.offerConstraints); 1.1374 + }, 1.1375 + 1.1376 + /** 1.1377 + * Creates an answer and automatically handles the failure case. 1.1378 + * 1.1379 + * @param {function} onSuccess 1.1380 + * Callback to execute if the answer was created successfully 1.1381 + */ 1.1382 + createAnswer : function PCW_createAnswer(onSuccess) { 1.1383 + var self = this; 1.1384 + 1.1385 + this._pc.createAnswer(function (answer) { 1.1386 + info(self + ": Got answer: " + JSON.stringify(answer)); 1.1387 + self._last_answer = answer; 1.1388 + onSuccess(answer); 1.1389 + }, generateErrorCallback()); 1.1390 + }, 1.1391 + 1.1392 + /** 1.1393 + * Sets the local description and automatically handles the failure case. 1.1394 + * 1.1395 + * @param {object} desc 1.1396 + * mozRTCSessionDescription for the local description request 1.1397 + * @param {function} onSuccess 1.1398 + * Callback to execute if the local description was set successfully 1.1399 + */ 1.1400 + setLocalDescription : function PCW_setLocalDescription(desc, onSuccess) { 1.1401 + var self = this; 1.1402 + this._pc.setLocalDescription(desc, function () { 1.1403 + info(self + ": Successfully set the local description"); 1.1404 + onSuccess(); 1.1405 + }, generateErrorCallback()); 1.1406 + }, 1.1407 + 1.1408 + /** 1.1409 + * Tries to set the local description and expect failure. Automatically 1.1410 + * causes the test case to fail if the call succeeds. 1.1411 + * 1.1412 + * @param {object} desc 1.1413 + * mozRTCSessionDescription for the local description request 1.1414 + * @param {function} onFailure 1.1415 + * Callback to execute if the call fails. 1.1416 + */ 1.1417 + setLocalDescriptionAndFail : function PCW_setLocalDescriptionAndFail(desc, onFailure) { 1.1418 + var self = this; 1.1419 + this._pc.setLocalDescription(desc, 1.1420 + generateErrorCallback("setLocalDescription should have failed."), 1.1421 + function (err) { 1.1422 + info(self + ": As expected, failed to set the local description"); 1.1423 + onFailure(err); 1.1424 + }); 1.1425 + }, 1.1426 + 1.1427 + /** 1.1428 + * Sets the remote description and automatically handles the failure case. 1.1429 + * 1.1430 + * @param {object} desc 1.1431 + * mozRTCSessionDescription for the remote description request 1.1432 + * @param {function} onSuccess 1.1433 + * Callback to execute if the remote description was set successfully 1.1434 + */ 1.1435 + setRemoteDescription : function PCW_setRemoteDescription(desc, onSuccess) { 1.1436 + var self = this; 1.1437 + this._pc.setRemoteDescription(desc, function () { 1.1438 + info(self + ": Successfully set remote description"); 1.1439 + onSuccess(); 1.1440 + }, generateErrorCallback()); 1.1441 + }, 1.1442 + 1.1443 + /** 1.1444 + * Tries to set the remote description and expect failure. Automatically 1.1445 + * causes the test case to fail if the call succeeds. 1.1446 + * 1.1447 + * @param {object} desc 1.1448 + * mozRTCSessionDescription for the remote description request 1.1449 + * @param {function} onFailure 1.1450 + * Callback to execute if the call fails. 1.1451 + */ 1.1452 + setRemoteDescriptionAndFail : function PCW_setRemoteDescriptionAndFail(desc, onFailure) { 1.1453 + var self = this; 1.1454 + this._pc.setRemoteDescription(desc, 1.1455 + generateErrorCallback("setRemoteDescription should have failed."), 1.1456 + function (err) { 1.1457 + info(self + ": As expected, failed to set the remote description"); 1.1458 + onFailure(err); 1.1459 + }); 1.1460 + }, 1.1461 + 1.1462 + /** 1.1463 + * Adds an ICE candidate and automatically handles the failure case. 1.1464 + * 1.1465 + * @param {object} candidate 1.1466 + * SDP candidate 1.1467 + * @param {function} onSuccess 1.1468 + * Callback to execute if the local description was set successfully 1.1469 + */ 1.1470 + addIceCandidate : function PCW_addIceCandidate(candidate, onSuccess) { 1.1471 + var self = this; 1.1472 + 1.1473 + this._pc.addIceCandidate(candidate, function () { 1.1474 + info(self + ": Successfully added an ICE candidate"); 1.1475 + onSuccess(); 1.1476 + }, generateErrorCallback()); 1.1477 + }, 1.1478 + 1.1479 + /** 1.1480 + * Tries to add an ICE candidate and expects failure. Automatically 1.1481 + * causes the test case to fail if the call succeeds. 1.1482 + * 1.1483 + * @param {object} candidate 1.1484 + * SDP candidate 1.1485 + * @param {function} onFailure 1.1486 + * Callback to execute if the call fails. 1.1487 + */ 1.1488 + addIceCandidateAndFail : function PCW_addIceCandidateAndFail(candidate, onFailure) { 1.1489 + var self = this; 1.1490 + 1.1491 + this._pc.addIceCandidate(candidate, 1.1492 + generateErrorCallback("addIceCandidate should have failed."), 1.1493 + function (err) { 1.1494 + info(self + ": As expected, failed to add an ICE candidate"); 1.1495 + onFailure(err); 1.1496 + }) ; 1.1497 + }, 1.1498 + 1.1499 + /** 1.1500 + * Returns if the ICE the connection state is "connected". 1.1501 + * 1.1502 + * @returns {boolean} True if the connection state is "connected", otherwise false. 1.1503 + */ 1.1504 + isIceConnected : function PCW_isIceConnected() { 1.1505 + info("iceConnectionState: " + this.iceConnectionState); 1.1506 + return this.iceConnectionState === "connected"; 1.1507 + }, 1.1508 + 1.1509 + /** 1.1510 + * Returns if the ICE the connection state is "checking". 1.1511 + * 1.1512 + * @returns {boolean} True if the connection state is "checking", otherwise false. 1.1513 + */ 1.1514 + isIceChecking : function PCW_isIceChecking() { 1.1515 + return this.iceConnectionState === "checking"; 1.1516 + }, 1.1517 + 1.1518 + /** 1.1519 + * Returns if the ICE the connection state is "new". 1.1520 + * 1.1521 + * @returns {boolean} True if the connection state is "new", otherwise false. 1.1522 + */ 1.1523 + isIceNew : function PCW_isIceNew() { 1.1524 + return this.iceConnectionState === "new"; 1.1525 + }, 1.1526 + 1.1527 + /** 1.1528 + * Checks if the ICE connection state still waits for a connection to get 1.1529 + * established. 1.1530 + * 1.1531 + * @returns {boolean} True if the connection state is "checking" or "new", 1.1532 + * otherwise false. 1.1533 + */ 1.1534 + isIceConnectionPending : function PCW_isIceConnectionPending() { 1.1535 + return (this.isIceChecking() || this.isIceNew()); 1.1536 + }, 1.1537 + 1.1538 + /** 1.1539 + * Registers a callback for the ICE connection state change and 1.1540 + * reports success (=connected) or failure via the callbacks. 1.1541 + * States "new" and "checking" are ignored. 1.1542 + * 1.1543 + * @param {function} onSuccess 1.1544 + * Callback if ICE connection status is "connected". 1.1545 + * @param {function} onFailure 1.1546 + * Callback if ICE connection reaches a different state than 1.1547 + * "new", "checking" or "connected". 1.1548 + */ 1.1549 + waitForIceConnected : function PCW_waitForIceConnected(onSuccess, onFailure) { 1.1550 + var self = this; 1.1551 + var mySuccess = onSuccess; 1.1552 + var myFailure = onFailure; 1.1553 + 1.1554 + function iceConnectedChanged () { 1.1555 + if (self.isIceConnected()) { 1.1556 + delete self.ice_connection_callbacks.waitForIceConnected; 1.1557 + mySuccess(); 1.1558 + } else if (! self.isIceConnectionPending()) { 1.1559 + delete self.ice_connection_callbacks.waitForIceConnected; 1.1560 + myFailure(); 1.1561 + } 1.1562 + } 1.1563 + 1.1564 + self.ice_connection_callbacks.waitForIceConnected = iceConnectedChanged; 1.1565 + }, 1.1566 + 1.1567 + /** 1.1568 + * Checks that we are getting the media streams we expect. 1.1569 + * 1.1570 + * @param {object} constraintsRemote 1.1571 + * The media constraints of the remote peer connection object 1.1572 + */ 1.1573 + checkMediaStreams : function PCW_checkMediaStreams(constraintsRemote) { 1.1574 + is(this._pc.getLocalStreams().length, this.constraints.length, 1.1575 + this + ' has ' + this.constraints.length + ' local streams'); 1.1576 + 1.1577 + // TODO: change this when multiple incoming streams are supported (bug 834835) 1.1578 + is(this._pc.getRemoteStreams().length, 1, 1.1579 + this + ' has ' + 1 + ' remote streams'); 1.1580 + }, 1.1581 + 1.1582 + /** 1.1583 + * Check that media flow is present on all media elements involved in this 1.1584 + * test by waiting for confirmation that media flow is present. 1.1585 + * 1.1586 + * @param {Function} onSuccess the success callback when media flow 1.1587 + * is confirmed on all media elements 1.1588 + */ 1.1589 + checkMediaFlowPresent : function PCW_checkMediaFlowPresent(onSuccess) { 1.1590 + var self = this; 1.1591 + 1.1592 + function _checkMediaFlowPresent(index, onSuccess) { 1.1593 + if(index >= self.mediaCheckers.length) { 1.1594 + onSuccess(); 1.1595 + } else { 1.1596 + var mediaChecker = self.mediaCheckers[index]; 1.1597 + mediaChecker.waitForMediaFlow(function() { 1.1598 + _checkMediaFlowPresent(index + 1, onSuccess); 1.1599 + }); 1.1600 + } 1.1601 + } 1.1602 + 1.1603 + _checkMediaFlowPresent(0, onSuccess); 1.1604 + }, 1.1605 + 1.1606 + /** 1.1607 + * Check that stats are present by checking for known stats. 1.1608 + * 1.1609 + * @param {Function} onSuccess the success callback to return stats to 1.1610 + */ 1.1611 + getStats : function PCW_getStats(selector, onSuccess) { 1.1612 + var self = this; 1.1613 + 1.1614 + this._pc.getStats(selector, function(stats) { 1.1615 + info(self + ": Got stats: " + JSON.stringify(stats)); 1.1616 + self._last_stats = stats; 1.1617 + onSuccess(stats); 1.1618 + }, generateErrorCallback()); 1.1619 + }, 1.1620 + 1.1621 + /** 1.1622 + * Checks that we are getting the media streams we expect. 1.1623 + * 1.1624 + * @param {object} stats 1.1625 + * The stats to check from this PeerConnectionWrapper 1.1626 + */ 1.1627 + checkStats : function PCW_checkStats(stats) { 1.1628 + function toNum(obj) { 1.1629 + return obj? obj : 0; 1.1630 + } 1.1631 + function numTracks(streams) { 1.1632 + var n = 0; 1.1633 + streams.forEach(function(stream) { 1.1634 + n += stream.getAudioTracks().length + stream.getVideoTracks().length; 1.1635 + }); 1.1636 + return n; 1.1637 + } 1.1638 + 1.1639 + const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1; 1.1640 + 1.1641 + // Use spec way of enumerating stats 1.1642 + var counters = {}; 1.1643 + for (var key in stats) { 1.1644 + if (stats.hasOwnProperty(key)) { 1.1645 + var res = stats[key]; 1.1646 + // validate stats 1.1647 + ok(res.id == key, "Coherent stats id"); 1.1648 + var nowish = Date.now() + 1000; // TODO: clock drift observed 1.1649 + var minimum = this.whenCreated - 1000; // on Windows XP (Bug 979649) 1.1650 + if (isWinXP) { 1.1651 + todo(false, "Can't reliably test rtcp timestamps on WinXP (Bug 979649)"); 1.1652 + } else { 1.1653 + ok(res.timestamp >= minimum, 1.1654 + "Valid " + (res.isRemote? "rtcp" : "rtp") + " timestamp " + 1.1655 + res.timestamp + " >= " + minimum + " (" + 1.1656 + (res.timestamp - minimum) + " ms)"); 1.1657 + ok(res.timestamp <= nowish, 1.1658 + "Valid " + (res.isRemote? "rtcp" : "rtp") + " timestamp " + 1.1659 + res.timestamp + " <= " + nowish + " (" + 1.1660 + (res.timestamp - nowish) + " ms)"); 1.1661 + } 1.1662 + if (!res.isRemote) { 1.1663 + counters[res.type] = toNum(counters[res.type]) + 1; 1.1664 + 1.1665 + switch (res.type) { 1.1666 + case "inboundrtp": 1.1667 + case "outboundrtp": { 1.1668 + // ssrc is a 32 bit number returned as a string by spec 1.1669 + ok(res.ssrc.length > 0, "Ssrc has length"); 1.1670 + ok(res.ssrc.length < 11, "Ssrc not lengthy"); 1.1671 + ok(!/[^0-9]/.test(res.ssrc), "Ssrc numeric"); 1.1672 + ok(parseInt(res.ssrc) < Math.pow(2,32), "Ssrc within limits"); 1.1673 + 1.1674 + if (res.type == "outboundrtp") { 1.1675 + ok(res.packetsSent !== undefined, "Rtp packetsSent"); 1.1676 + // minimum fragment is 8 (from RFC 791) 1.1677 + ok(res.bytesSent >= res.packetsSent * 8, "Rtp bytesSent"); 1.1678 + } else { 1.1679 + ok(res.packetsReceived !== undefined, "Rtp packetsReceived"); 1.1680 + ok(res.bytesReceived >= res.packetsReceived * 8, "Rtp bytesReceived"); 1.1681 + } 1.1682 + if (res.remoteId) { 1.1683 + var rem = stats[res.remoteId]; 1.1684 + ok(rem.isRemote, "Remote is rtcp"); 1.1685 + ok(rem.remoteId == res.id, "Remote backlink match"); 1.1686 + if(res.type == "outboundrtp") { 1.1687 + ok(rem.type == "inboundrtp", "Rtcp is inbound"); 1.1688 + ok(rem.packetsReceived !== undefined, "Rtcp packetsReceived"); 1.1689 + ok(rem.packetsReceived <= res.packetsSent, "No more than sent"); 1.1690 + ok(rem.packetsLost !== undefined, "Rtcp packetsLost"); 1.1691 + ok(rem.bytesReceived >= rem.packetsReceived * 8, "Rtcp bytesReceived"); 1.1692 + ok(rem.bytesReceived <= res.bytesSent, "No more than sent bytes"); 1.1693 + ok(rem.jitter !== undefined, "Rtcp jitter"); 1.1694 + ok(rem.mozRtt !== undefined, "Rtcp rtt"); 1.1695 + ok(rem.mozRtt >= 0, "Rtcp rtt " + rem.mozRtt + " >= 0"); 1.1696 + ok(rem.mozRtt < 60000, "Rtcp rtt " + rem.mozRtt + " < 1 min"); 1.1697 + } else { 1.1698 + ok(rem.type == "outboundrtp", "Rtcp is outbound"); 1.1699 + ok(rem.packetsSent !== undefined, "Rtcp packetsSent"); 1.1700 + // We may have received more than outdated Rtcp packetsSent 1.1701 + ok(rem.bytesSent >= rem.packetsSent * 8, "Rtcp bytesSent"); 1.1702 + } 1.1703 + ok(rem.ssrc == res.ssrc, "Remote ssrc match"); 1.1704 + } else { 1.1705 + info("No rtcp info received yet"); 1.1706 + } 1.1707 + } 1.1708 + break; 1.1709 + } 1.1710 + } 1.1711 + } 1.1712 + } 1.1713 + 1.1714 + // Use MapClass way of enumerating stats 1.1715 + var counters2 = {}; 1.1716 + stats.forEach(function(res) { 1.1717 + if (!res.isRemote) { 1.1718 + counters2[res.type] = toNum(counters2[res.type]) + 1; 1.1719 + } 1.1720 + }); 1.1721 + is(JSON.stringify(counters), JSON.stringify(counters2), 1.1722 + "Spec and MapClass variant of RTCStatsReport enumeration agree"); 1.1723 + var nin = numTracks(this._pc.getRemoteStreams()); 1.1724 + var nout = numTracks(this._pc.getLocalStreams()); 1.1725 + 1.1726 + // TODO(Bug 957145): Restore stronger inboundrtp test once Bug 948249 is fixed 1.1727 + //is(toNum(counters["inboundrtp"]), nin, "Have " + nin + " inboundrtp stat(s)"); 1.1728 + ok(toNum(counters.inboundrtp) >= nin, "Have at least " + nin + " inboundrtp stat(s) *"); 1.1729 + 1.1730 + is(toNum(counters.outboundrtp), nout, "Have " + nout + " outboundrtp stat(s)"); 1.1731 + 1.1732 + var numLocalCandidates = toNum(counters.localcandidate); 1.1733 + var numRemoteCandidates = toNum(counters.remotecandidate); 1.1734 + // If there are no tracks, there will be no stats either. 1.1735 + if (nin + nout > 0) { 1.1736 + ok(numLocalCandidates, "Have localcandidate stat(s)"); 1.1737 + ok(numRemoteCandidates, "Have remotecandidate stat(s)"); 1.1738 + } else { 1.1739 + is(numLocalCandidates, 0, "Have no localcandidate stats"); 1.1740 + is(numRemoteCandidates, 0, "Have no remotecandidate stats"); 1.1741 + } 1.1742 + }, 1.1743 + 1.1744 + /** 1.1745 + * Closes the connection 1.1746 + */ 1.1747 + close : function PCW_close() { 1.1748 + // It might be that a test has already closed the pc. In those cases 1.1749 + // we should not fail. 1.1750 + try { 1.1751 + this._pc.close(); 1.1752 + info(this + ": Closed connection."); 1.1753 + } 1.1754 + catch (e) { 1.1755 + info(this + ": Failure in closing connection - " + e.message); 1.1756 + } 1.1757 + }, 1.1758 + 1.1759 + /** 1.1760 + * Register all events during the setup of the data channel 1.1761 + * 1.1762 + * @param {Function} onDataChannelOpened 1.1763 + * Callback to execute when the data channel has been opened 1.1764 + */ 1.1765 + registerDataChannelOpenEvents : function (onDataChannelOpened) { 1.1766 + info(this + ": Register callbacks for 'ondatachannel' and 'onopen'"); 1.1767 + 1.1768 + this.ondatachannel = function (targetChannel) { 1.1769 + targetChannel.onopen = function (targetChannel) { 1.1770 + onDataChannelOpened(targetChannel); 1.1771 + }; 1.1772 + 1.1773 + this.dataChannels.push(targetChannel); 1.1774 + }; 1.1775 + }, 1.1776 + 1.1777 + /** 1.1778 + * Returns the string representation of the class 1.1779 + * 1.1780 + * @returns {String} The string representation 1.1781 + */ 1.1782 + toString : function PCW_toString() { 1.1783 + return "PeerConnectionWrapper (" + this.label + ")"; 1.1784 + } 1.1785 +};