michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: michael@0: /** michael@0: * This class mimics a state machine and handles a list of commands by michael@0: * executing them synchronously. michael@0: * michael@0: * @constructor michael@0: * @param {object} framework michael@0: * A back reference to the framework which makes use of the class. It's michael@0: * getting passed in as parameter to each command callback. michael@0: * @param {Array[]} [commandList=[]] michael@0: * Default commands to set during initialization michael@0: */ michael@0: function CommandChain(framework, commandList) { michael@0: this._framework = framework; michael@0: michael@0: this._commands = commandList || [ ]; michael@0: this._current = 0; michael@0: michael@0: this.onFinished = null; michael@0: } michael@0: michael@0: CommandChain.prototype = { michael@0: michael@0: /** michael@0: * Returns the index of the current command of the chain michael@0: * michael@0: * @returns {number} Index of the current command michael@0: */ michael@0: get current() { michael@0: return this._current; michael@0: }, michael@0: michael@0: /** michael@0: * Checks if the chain has already processed all the commands michael@0: * michael@0: * @returns {boolean} True, if all commands have been processed michael@0: */ michael@0: get finished() { michael@0: return this._current === this._commands.length; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the assigned commands of the chain. michael@0: * michael@0: * @returns {Array[]} Commands of the chain michael@0: */ michael@0: get commands() { michael@0: return this._commands; michael@0: }, michael@0: michael@0: /** michael@0: * Sets new commands for the chain. All existing commands will be replaced. michael@0: * michael@0: * @param {Array[]} commands michael@0: * List of commands michael@0: */ michael@0: set commands(commands) { michael@0: this._commands = commands; michael@0: }, michael@0: michael@0: /** michael@0: * Execute the next command in the chain. michael@0: */ michael@0: executeNext : function () { michael@0: var self = this; michael@0: michael@0: function _executeNext() { michael@0: if (!self.finished) { michael@0: var step = self._commands[self._current]; michael@0: self._current++; michael@0: michael@0: self.currentStepLabel = step[0]; michael@0: info("Run step: " + self.currentStepLabel); michael@0: step[1](self._framework); // Execute step michael@0: } michael@0: else if (typeof(self.onFinished) === 'function') { michael@0: self.onFinished(); michael@0: } michael@0: } michael@0: michael@0: // To prevent building up the stack we have to execute the next michael@0: // step asynchronously michael@0: window.setTimeout(_executeNext, 0); michael@0: }, michael@0: michael@0: /** michael@0: * Add new commands to the end of the chain michael@0: * michael@0: * @param {Array[]} commands michael@0: * List of commands michael@0: */ michael@0: append: function (commands) { michael@0: this._commands = this._commands.concat(commands); michael@0: }, michael@0: michael@0: /** michael@0: * Returns the index of the specified command in the chain. michael@0: * michael@0: * @param {string} id michael@0: * Identifier of the command michael@0: * @returns {number} Index of the command michael@0: */ michael@0: indexOf: function (id) { michael@0: for (var i = 0; i < this._commands.length; i++) { michael@0: if (this._commands[i][0] === id) { michael@0: return i; michael@0: } michael@0: } michael@0: michael@0: return -1; michael@0: }, michael@0: michael@0: /** michael@0: * Inserts the new commands after the specified command. michael@0: * michael@0: * @param {string} id michael@0: * Identifier of the command michael@0: * @param {Array[]} commands michael@0: * List of commands michael@0: */ michael@0: insertAfter: function (id, commands) { michael@0: var index = this.indexOf(id); michael@0: michael@0: if (index > -1) { michael@0: var tail = this.removeAfter(id); michael@0: michael@0: this.append(commands); michael@0: this.append(tail); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Inserts the new commands before the specified command. michael@0: * michael@0: * @param {string} id michael@0: * Identifier of the command michael@0: * @param {Array[]} commands michael@0: * List of commands michael@0: */ michael@0: insertBefore: function (id, commands) { michael@0: var index = this.indexOf(id); michael@0: michael@0: if (index > -1) { michael@0: var tail = this.removeAfter(id); michael@0: var object = this.remove(id); michael@0: michael@0: this.append(commands); michael@0: this.append(object); michael@0: this.append(tail); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Removes the specified command michael@0: * michael@0: * @param {string} id michael@0: * Identifier of the command michael@0: * @returns {object[]} Removed command michael@0: */ michael@0: remove : function (id) { michael@0: return this._commands.splice(this.indexOf(id), 1); michael@0: }, michael@0: michael@0: /** michael@0: * Removes all commands after the specified one. michael@0: * michael@0: * @param {string} id michael@0: * Identifier of the command michael@0: * @returns {object[]} Removed commands michael@0: */ michael@0: removeAfter : function (id) { michael@0: var index = this.indexOf(id); michael@0: michael@0: if (index > -1) { michael@0: return this._commands.splice(index + 1); michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Removes all commands before the specified one. michael@0: * michael@0: * @param {string} id michael@0: * Identifier of the command michael@0: * @returns {object[]} Removed commands michael@0: */ michael@0: removeBefore : function (id) { michael@0: var index = this.indexOf(id); michael@0: michael@0: if (index > -1) { michael@0: return this._commands.splice(0, index); michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Replaces all commands after the specified one. michael@0: * michael@0: * @param {string} id michael@0: * Identifier of the command michael@0: * @returns {object[]} Removed commands michael@0: */ michael@0: replaceAfter : function (id, commands) { michael@0: var oldCommands = this.removeAfter(id); michael@0: this.append(commands); michael@0: michael@0: return oldCommands; michael@0: }, michael@0: michael@0: /** michael@0: * Replaces all commands before the specified one. michael@0: * michael@0: * @param {string} id michael@0: * Identifier of the command michael@0: * @returns {object[]} Removed commands michael@0: */ michael@0: replaceBefore : function (id, commands) { michael@0: var oldCommands = this.removeBefore(id); michael@0: this.insertBefore(id, commands); michael@0: michael@0: return oldCommands; michael@0: }, michael@0: michael@0: /** michael@0: * Remove all commands whose identifiers match the specified regex. michael@0: * michael@0: * @param {regex} id_match michael@0: * Regular expression to match command identifiers. michael@0: */ michael@0: filterOut : function (id_match) { michael@0: for (var i = this._commands.length - 1; i >= 0; i--) { michael@0: if (id_match.test(this._commands[i][0])) { michael@0: this._commands.splice(i, 1); michael@0: } michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * This class provides a state checker for media elements which store michael@0: * a media stream to check for media attribute state and events fired. michael@0: * When constructed by a caller, an object instance is created with michael@0: * a media element, event state checkers for canplaythrough, timeupdate, and michael@0: * time changing on the media element and stream. michael@0: * michael@0: * @param {HTMLMediaElement} element the media element being analyzed michael@0: */ michael@0: function MediaElementChecker(element) { michael@0: this.element = element; michael@0: this.canPlayThroughFired = false; michael@0: this.timeUpdateFired = false; michael@0: this.timePassed = false; michael@0: michael@0: var self = this; michael@0: var elementId = self.element.getAttribute('id'); michael@0: michael@0: // When canplaythrough fires, we track that it's fired and remove the michael@0: // event listener. michael@0: var canPlayThroughCallback = function() { michael@0: info('canplaythrough fired for media element ' + elementId); michael@0: self.canPlayThroughFired = true; michael@0: self.element.removeEventListener('canplaythrough', canPlayThroughCallback, michael@0: false); michael@0: }; michael@0: michael@0: // When timeupdate fires, we track that it's fired and check if time michael@0: // has passed on the media stream and media element. michael@0: var timeUpdateCallback = function() { michael@0: self.timeUpdateFired = true; michael@0: info('timeupdate fired for media element ' + elementId); michael@0: michael@0: // If time has passed, then track that and remove the timeupdate event michael@0: // listener. michael@0: if(element.mozSrcObject && element.mozSrcObject.currentTime > 0 && michael@0: element.currentTime > 0) { michael@0: info('time passed for media element ' + elementId); michael@0: self.timePassed = true; michael@0: self.element.removeEventListener('timeupdate', timeUpdateCallback, michael@0: false); michael@0: } michael@0: }; michael@0: michael@0: element.addEventListener('canplaythrough', canPlayThroughCallback, false); michael@0: element.addEventListener('timeupdate', timeUpdateCallback, false); michael@0: } michael@0: michael@0: MediaElementChecker.prototype = { michael@0: michael@0: /** michael@0: * Waits until the canplaythrough & timeupdate events to fire along with michael@0: * ensuring time has passed on the stream and media element. michael@0: * michael@0: * @param {Function} onSuccess the success callback when media flow is michael@0: * established michael@0: */ michael@0: waitForMediaFlow : function MEC_WaitForMediaFlow(onSuccess) { michael@0: var self = this; michael@0: var elementId = self.element.getAttribute('id'); michael@0: info('Analyzing element: ' + elementId); michael@0: michael@0: if(self.canPlayThroughFired && self.timeUpdateFired && self.timePassed) { michael@0: ok(true, 'Media flowing for ' + elementId); michael@0: onSuccess(); michael@0: } else { michael@0: setTimeout(function() { michael@0: self.waitForMediaFlow(onSuccess); michael@0: }, 100); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Checks if there is no media flow present by checking that the ready michael@0: * state of the media element is HAVE_METADATA. michael@0: */ michael@0: checkForNoMediaFlow : function MEC_CheckForNoMediaFlow() { michael@0: ok(this.element.readyState === HTMLMediaElement.HAVE_METADATA, michael@0: 'Media element has a ready state of HAVE_METADATA'); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Query function for determining if any IP address is available for michael@0: * generating SDP. michael@0: * michael@0: * @return false if required additional network setup. michael@0: */ michael@0: function isNetworkReady() { michael@0: // for gonk platform michael@0: if ("nsINetworkInterfaceListService" in SpecialPowers.Ci) { michael@0: var listService = SpecialPowers.Cc["@mozilla.org/network/interface-list-service;1"] michael@0: .getService(SpecialPowers.Ci.nsINetworkInterfaceListService); michael@0: var itfList = listService.getDataInterfaceList( michael@0: SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_MMS_INTERFACES | michael@0: SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_SUPL_INTERFACES | michael@0: SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_IMS_INTERFACES | michael@0: SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_DUN_INTERFACES); michael@0: var num = itfList.getNumberOfInterface(); michael@0: for (var i = 0; i < num; i++) { michael@0: var ips = {}; michael@0: var prefixLengths = {}; michael@0: var length = itfList.getInterface(i).getAddresses(ips, prefixLengths); michael@0: michael@0: for (var j = 0; j < length; j++) { michael@0: var ip = ips.value[j]; michael@0: // skip IPv6 address michael@0: if (ip.indexOf(":") < 0) { michael@0: info("Network interface is ready with address: " + ip); michael@0: return true; michael@0: } michael@0: } michael@0: } michael@0: // ip address is not available michael@0: info("Network interface is not ready, required additional network setup"); michael@0: return false; michael@0: } michael@0: info("Network setup is not required"); michael@0: return true; michael@0: } michael@0: michael@0: /** michael@0: * Network setup utils for Gonk michael@0: * michael@0: * @return {object} providing functions for setup/teardown data connection michael@0: */ michael@0: function getNetworkUtils() { michael@0: var url = SimpleTest.getTestFileURL("NetworkPreparationChromeScript.js"); michael@0: var script = SpecialPowers.loadChromeScript(url); michael@0: michael@0: var utils = { michael@0: /** michael@0: * Utility for setting up data connection. michael@0: * michael@0: * @param aCallback callback after data connection is ready. michael@0: */ michael@0: prepareNetwork: function(aCallback) { michael@0: script.addMessageListener('network-ready', function (message) { michael@0: info("Network interface is ready"); michael@0: aCallback(); michael@0: }); michael@0: info("Setup network interface"); michael@0: script.sendAsyncMessage("prepare-network", true); michael@0: }, michael@0: /** michael@0: * Utility for tearing down data connection. michael@0: * michael@0: * @param aCallback callback after data connection is closed. michael@0: */ michael@0: tearDownNetwork: function(aCallback) { michael@0: script.addMessageListener('network-disabled', function (message) { michael@0: ok(true, 'network-disabled'); michael@0: script.destroy(); michael@0: aCallback(); michael@0: }); michael@0: script.sendAsyncMessage("network-cleanup", true); michael@0: } michael@0: }; michael@0: michael@0: return utils; michael@0: } michael@0: michael@0: /** michael@0: * This class handles tests for peer connections. michael@0: * michael@0: * @constructor michael@0: * @param {object} [options={}] michael@0: * Optional options for the peer connection test michael@0: * @param {object} [options.commands=commandsPeerConnection] michael@0: * Commands to run for the test michael@0: * @param {bool} [options.is_local=true] michael@0: * true if this test should run the tests for the "local" side. michael@0: * @param {bool} [options.is_remote=true] michael@0: * true if this test should run the tests for the "remote" side. michael@0: * @param {object} [options.config_pc1=undefined] michael@0: * Configuration for the local peer connection instance michael@0: * @param {object} [options.config_pc2=undefined] michael@0: * Configuration for the remote peer connection instance. If not defined michael@0: * the configuration from the local instance will be used michael@0: */ michael@0: function PeerConnectionTest(options) { michael@0: // If no options are specified make it an empty object michael@0: options = options || { }; michael@0: options.commands = options.commands || commandsPeerConnection; michael@0: options.is_local = "is_local" in options ? options.is_local : true; michael@0: options.is_remote = "is_remote" in options ? options.is_remote : true; michael@0: michael@0: var netTeardownCommand = null; michael@0: if (!isNetworkReady()) { michael@0: var utils = getNetworkUtils(); michael@0: // Trigger network setup to obtain IP address before creating any PeerConnection. michael@0: utils.prepareNetwork(function() { michael@0: ok(isNetworkReady(),'setup network connection successfully'); michael@0: }); michael@0: michael@0: netTeardownCommand = [ michael@0: [ michael@0: 'TEARDOWN_NETWORK', michael@0: function(test) { michael@0: utils.tearDownNetwork(function() { michael@0: info('teardown network connection'); michael@0: test.next(); michael@0: }); michael@0: } michael@0: ] michael@0: ]; michael@0: } michael@0: michael@0: if (options.is_local) michael@0: this.pcLocal = new PeerConnectionWrapper('pcLocal', options.config_pc1); michael@0: else michael@0: this.pcLocal = null; michael@0: michael@0: if (options.is_remote) michael@0: this.pcRemote = new PeerConnectionWrapper('pcRemote', options.config_pc2 || options.config_pc1); michael@0: else michael@0: this.pcRemote = null; michael@0: michael@0: this.connected = false; michael@0: michael@0: // Create command chain instance and assign default commands michael@0: this.chain = new CommandChain(this, options.commands); michael@0: if (!options.is_local) { michael@0: this.chain.filterOut(/^PC_LOCAL/); michael@0: } michael@0: if (!options.is_remote) { michael@0: this.chain.filterOut(/^PC_REMOTE/); michael@0: } michael@0: michael@0: // Insert network teardown after testcase execution. michael@0: if (netTeardownCommand) { michael@0: this.chain.append(netTeardownCommand); michael@0: } michael@0: michael@0: var self = this; michael@0: this.chain.onFinished = function () { michael@0: self.teardown(); michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Closes the peer connection if it is active michael@0: * michael@0: * @param {Function} onSuccess michael@0: * Callback to execute when the peer connection has been closed successfully michael@0: */ michael@0: PeerConnectionTest.prototype.close = function PCT_close(onSuccess) { michael@0: info("Closing peer connections. Connection state=" + this.connected); michael@0: michael@0: function signalingstatechangeClose(state) { michael@0: info("'onsignalingstatechange' event '" + state + "' received"); michael@0: is(state, "closed", "onsignalingstatechange event is closed"); michael@0: } michael@0: michael@0: // There is no onclose event for the remote peer existent yet. So close it michael@0: // side-by-side with the local peer. michael@0: if (this.pcLocal) { michael@0: this.pcLocal.onsignalingstatechange = signalingstatechangeClose; michael@0: this.pcLocal.close(); michael@0: } michael@0: if (this.pcRemote) { michael@0: this.pcRemote.onsignalingstatechange = signalingstatechangeClose; michael@0: this.pcRemote.close(); michael@0: } michael@0: this.connected = false; michael@0: michael@0: onSuccess(); michael@0: }; michael@0: michael@0: /** michael@0: * Executes the next command. michael@0: */ michael@0: PeerConnectionTest.prototype.next = function PCT_next() { michael@0: if (this._stepTimeout) { michael@0: clearTimeout(this._stepTimeout); michael@0: this._stepTimeout = null; michael@0: } michael@0: this.chain.executeNext(); michael@0: }; michael@0: michael@0: /** michael@0: * Set a timeout for the current step. michael@0: * @param {long] ms the number of milliseconds to allow for this step michael@0: */ michael@0: PeerConnectionTest.prototype.setStepTimeout = function(ms) { michael@0: this._stepTimeout = setTimeout(function() { michael@0: ok(false, "Step timed out: " + this.chain.currentStepLabel); michael@0: this.next(); michael@0: }.bind(this), ms); michael@0: }; michael@0: michael@0: /** michael@0: * Creates an answer for the specified peer connection instance michael@0: * and automatically handles the failure case. michael@0: * michael@0: * @param {PeerConnectionWrapper} peer michael@0: * The peer connection wrapper to run the command on michael@0: * @param {function} onSuccess michael@0: * Callback to execute if the offer was created successfully michael@0: */ michael@0: PeerConnectionTest.prototype.createAnswer = michael@0: function PCT_createAnswer(peer, onSuccess) { michael@0: peer.createAnswer(function (answer) { michael@0: onSuccess(answer); michael@0: }); michael@0: }; michael@0: michael@0: /** michael@0: * Creates an offer for the specified peer connection instance michael@0: * and automatically handles the failure case. michael@0: * michael@0: * @param {PeerConnectionWrapper} peer michael@0: * The peer connection wrapper to run the command on michael@0: * @param {function} onSuccess michael@0: * Callback to execute if the offer was created successfully michael@0: */ michael@0: PeerConnectionTest.prototype.createOffer = michael@0: function PCT_createOffer(peer, onSuccess) { michael@0: peer.createOffer(function (offer) { michael@0: onSuccess(offer); michael@0: }); michael@0: }; michael@0: michael@0: PeerConnectionTest.prototype.setIdentityProvider = michael@0: function(peer, provider, protocol, identity) { michael@0: peer.setIdentityProvider(provider, protocol, identity); michael@0: }; michael@0: michael@0: /** michael@0: * Sets the local description for the specified peer connection instance michael@0: * and automatically handles the failure case. michael@0: * michael@0: * @param {PeerConnectionWrapper} peer michael@0: The peer connection wrapper to run the command on michael@0: * @param {mozRTCSessionDescription} desc michael@0: * Session description for the local description request michael@0: * @param {function} onSuccess michael@0: * Callback to execute if the local description was set successfully michael@0: */ michael@0: PeerConnectionTest.prototype.setLocalDescription = michael@0: function PCT_setLocalDescription(peer, desc, stateExpected, onSuccess) { michael@0: var eventFired = false; michael@0: var stateChanged = false; michael@0: michael@0: function check_next_test() { michael@0: if (eventFired && stateChanged) { michael@0: onSuccess(); michael@0: } michael@0: } michael@0: michael@0: peer.onsignalingstatechange = function (state) { michael@0: //info(peer + ": 'onsignalingstatechange' event registered, signalingState: " + peer.signalingState); michael@0: info(peer + ": 'onsignalingstatechange' event '" + state + "' received"); michael@0: if(stateExpected === state && eventFired == false) { michael@0: eventFired = true; michael@0: check_next_test(); michael@0: } else { michael@0: ok(false, "This event has either already fired or there has been a " + michael@0: "mismatch between event received " + state + michael@0: " and event expected " + stateExpected); michael@0: } michael@0: }; michael@0: michael@0: peer.setLocalDescription(desc, function () { michael@0: stateChanged = true; michael@0: check_next_test(); michael@0: }); michael@0: }; michael@0: michael@0: /** michael@0: * Sets the media constraints for both peer connection instances. michael@0: * michael@0: * @param {object} constraintsLocal michael@0: * Media constrains for the local peer connection instance michael@0: * @param constraintsRemote michael@0: */ michael@0: PeerConnectionTest.prototype.setMediaConstraints = michael@0: function PCT_setMediaConstraints(constraintsLocal, constraintsRemote) { michael@0: if (this.pcLocal) michael@0: this.pcLocal.constraints = constraintsLocal; michael@0: if (this.pcRemote) michael@0: this.pcRemote.constraints = constraintsRemote; michael@0: }; michael@0: michael@0: /** michael@0: * Sets the media constraints used on a createOffer call in the test. michael@0: * michael@0: * @param {object} constraints the media constraints to use on createOffer michael@0: */ michael@0: PeerConnectionTest.prototype.setOfferConstraints = michael@0: function PCT_setOfferConstraints(constraints) { michael@0: if (this.pcLocal) michael@0: this.pcLocal.offerConstraints = constraints; michael@0: }; michael@0: michael@0: /** michael@0: * Sets the remote description for the specified peer connection instance michael@0: * and automatically handles the failure case. michael@0: * michael@0: * @param {PeerConnectionWrapper} peer michael@0: The peer connection wrapper to run the command on michael@0: * @param {mozRTCSessionDescription} desc michael@0: * Session description for the remote description request michael@0: * @param {function} onSuccess michael@0: * Callback to execute if the local description was set successfully michael@0: */ michael@0: PeerConnectionTest.prototype.setRemoteDescription = michael@0: function PCT_setRemoteDescription(peer, desc, stateExpected, onSuccess) { michael@0: var eventFired = false; michael@0: var stateChanged = false; michael@0: michael@0: function check_next_test() { michael@0: if (eventFired && stateChanged) { michael@0: onSuccess(); michael@0: } michael@0: } michael@0: michael@0: peer.onsignalingstatechange = function (state) { michael@0: info(peer + ": 'onsignalingstatechange' event '" + state + "' received"); michael@0: if(stateExpected === state && eventFired == false) { michael@0: eventFired = true; michael@0: check_next_test(); michael@0: } else { michael@0: ok(false, "This event has either already fired or there has been a " + michael@0: "mismatch between event received " + state + michael@0: " and event expected " + stateExpected); michael@0: } michael@0: }; michael@0: michael@0: peer.setRemoteDescription(desc, function () { michael@0: stateChanged = true; michael@0: check_next_test(); michael@0: }); michael@0: }; michael@0: michael@0: /** michael@0: * Start running the tests as assigned to the command chain. michael@0: */ michael@0: PeerConnectionTest.prototype.run = function PCT_run() { michael@0: this.next(); michael@0: }; michael@0: michael@0: /** michael@0: * Clean up the objects used by the test michael@0: */ michael@0: PeerConnectionTest.prototype.teardown = function PCT_teardown() { michael@0: this.close(function () { michael@0: info("Test finished"); michael@0: if (window.SimpleTest) michael@0: SimpleTest.finish(); michael@0: else michael@0: finish(); michael@0: }); michael@0: }; michael@0: michael@0: /** michael@0: * This class handles tests for data channels. michael@0: * michael@0: * @constructor michael@0: * @param {object} [options={}] michael@0: * Optional options for the peer connection test michael@0: * @param {object} [options.commands=commandsDataChannel] michael@0: * Commands to run for the test michael@0: * @param {object} [options.config_pc1=undefined] michael@0: * Configuration for the local peer connection instance michael@0: * @param {object} [options.config_pc2=undefined] michael@0: * Configuration for the remote peer connection instance. If not defined michael@0: * the configuration from the local instance will be used michael@0: */ michael@0: function DataChannelTest(options) { michael@0: options = options || { }; michael@0: options.commands = options.commands || commandsDataChannel; michael@0: michael@0: PeerConnectionTest.call(this, options); michael@0: } michael@0: michael@0: DataChannelTest.prototype = Object.create(PeerConnectionTest.prototype, { michael@0: close : { michael@0: /** michael@0: * Close the open data channels, followed by the underlying peer connection michael@0: * michael@0: * @param {Function} onSuccess michael@0: * Callback to execute when the connection has been closed michael@0: */ michael@0: value : function DCT_close(onSuccess) { michael@0: var self = this; michael@0: michael@0: function _closeChannels() { michael@0: var length = self.pcLocal.dataChannels.length; michael@0: michael@0: if (length > 0) { michael@0: self.closeDataChannel(length - 1, function () { michael@0: _closeChannels(); michael@0: }); michael@0: } michael@0: else { michael@0: PeerConnectionTest.prototype.close.call(self, onSuccess); michael@0: } michael@0: } michael@0: michael@0: _closeChannels(); michael@0: } michael@0: }, michael@0: michael@0: closeDataChannel : { michael@0: /** michael@0: * Close the specified data channel michael@0: * michael@0: * @param {Number} index michael@0: * Index of the data channel to close on both sides michael@0: * @param {Function} onSuccess michael@0: * Callback to execute when the data channel has been closed michael@0: */ michael@0: value : function DCT_closeDataChannel(index, onSuccess) { michael@0: var localChannel = this.pcLocal.dataChannels[index]; michael@0: var remoteChannel = this.pcRemote.dataChannels[index]; michael@0: michael@0: var self = this; michael@0: michael@0: // Register handler for remote channel, cause we have to wait until michael@0: // the current close operation has been finished. michael@0: remoteChannel.onclose = function () { michael@0: self.pcRemote.dataChannels.splice(index, 1); michael@0: michael@0: onSuccess(remoteChannel); michael@0: }; michael@0: michael@0: localChannel.close(); michael@0: this.pcLocal.dataChannels.splice(index, 1); michael@0: } michael@0: }, michael@0: michael@0: createDataChannel : { michael@0: /** michael@0: * Create a data channel michael@0: * michael@0: * @param {Dict} options michael@0: * Options for the data channel (see nsIPeerConnection) michael@0: * @param {Function} onSuccess michael@0: * Callback when the creation was successful michael@0: */ michael@0: value : function DCT_createDataChannel(options, onSuccess) { michael@0: var localChannel = null; michael@0: var remoteChannel = null; michael@0: var self = this; michael@0: michael@0: // Method to synchronize all asynchronous events. michael@0: function check_next_test() { michael@0: if (self.connected && localChannel && remoteChannel) { michael@0: onSuccess(localChannel, remoteChannel); michael@0: } michael@0: } michael@0: michael@0: if (!options.negotiated) { michael@0: // Register handlers for the remote peer michael@0: this.pcRemote.registerDataChannelOpenEvents(function (channel) { michael@0: remoteChannel = channel; michael@0: check_next_test(); michael@0: }); michael@0: } michael@0: michael@0: // Create the datachannel and handle the local 'onopen' event michael@0: this.pcLocal.createDataChannel(options, function (channel) { michael@0: localChannel = channel; michael@0: michael@0: if (options.negotiated) { michael@0: // externally negotiated - we need to open from both ends michael@0: options.id = options.id || channel.id; // allow for no id to let the impl choose michael@0: self.pcRemote.createDataChannel(options, function (channel) { michael@0: remoteChannel = channel; michael@0: check_next_test(); michael@0: }); michael@0: } else { michael@0: check_next_test(); michael@0: } michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: send : { michael@0: /** michael@0: * Send data (message or blob) to the other peer michael@0: * michael@0: * @param {String|Blob} data michael@0: * Data to send to the other peer. For Blobs the MIME type will be lost. michael@0: * @param {Function} onSuccess michael@0: * Callback to execute when data has been sent michael@0: * @param {Object} [options={ }] michael@0: * Options to specify the data channels to be used michael@0: * @param {DataChannelWrapper} [options.sourceChannel=pcLocal.dataChannels[length - 1]] michael@0: * Data channel to use for sending the message michael@0: * @param {DataChannelWrapper} [options.targetChannel=pcRemote.dataChannels[length - 1]] michael@0: * Data channel to use for receiving the message michael@0: */ michael@0: value : function DCT_send(data, onSuccess, options) { michael@0: options = options || { }; michael@0: var source = options.sourceChannel || michael@0: this.pcLocal.dataChannels[this.pcLocal.dataChannels.length - 1]; michael@0: var target = options.targetChannel || michael@0: this.pcRemote.dataChannels[this.pcRemote.dataChannels.length - 1]; michael@0: michael@0: // Register event handler for the target channel michael@0: target.onmessage = function (recv_data) { michael@0: onSuccess(target, recv_data); michael@0: }; michael@0: michael@0: source.send(data); michael@0: } michael@0: }, michael@0: michael@0: setLocalDescription : { michael@0: /** michael@0: * Sets the local description for the specified peer connection instance michael@0: * and automatically handles the failure case. In case for the final call michael@0: * it will setup the requested datachannel. michael@0: * michael@0: * @param {PeerConnectionWrapper} peer michael@0: The peer connection wrapper to run the command on michael@0: * @param {mozRTCSessionDescription} desc michael@0: * Session description for the local description request michael@0: * @param {function} onSuccess michael@0: * Callback to execute if the local description was set successfully michael@0: */ michael@0: value : function DCT_setLocalDescription(peer, desc, state, onSuccess) { michael@0: // If the peer has a remote offer we are in the final call, and have michael@0: // to wait for the datachannel connection to be open. It will also set michael@0: // the local description internally. michael@0: if (peer.signalingState === 'have-remote-offer') { michael@0: this.waitForInitialDataChannel(peer, desc, state, onSuccess); michael@0: } michael@0: else { michael@0: PeerConnectionTest.prototype.setLocalDescription.call(this, peer, michael@0: desc, state, onSuccess); michael@0: } michael@0: michael@0: } michael@0: }, michael@0: michael@0: waitForInitialDataChannel : { michael@0: /** michael@0: * Create an initial data channel before the peer connection has been connected michael@0: * michael@0: * @param {PeerConnectionWrapper} peer michael@0: The peer connection wrapper to run the command on michael@0: * @param {mozRTCSessionDescription} desc michael@0: * Session description for the local description request michael@0: * @param {Function} onSuccess michael@0: * Callback when the creation was successful michael@0: */ michael@0: value : function DCT_waitForInitialDataChannel(peer, desc, state, onSuccess) { michael@0: var self = this; michael@0: michael@0: var targetPeer = peer; michael@0: var targetChannel = null; michael@0: michael@0: var sourcePeer = (peer == this.pcLocal) ? this.pcRemote : this.pcLocal; michael@0: var sourceChannel = null; michael@0: michael@0: // Method to synchronize all asynchronous events which current happen michael@0: // due to a non-predictable flow. With bug 875346 fixed we will be able michael@0: // to simplify this code. michael@0: function check_next_test() { michael@0: if (self.connected && sourceChannel && targetChannel) { michael@0: onSuccess(sourceChannel, targetChannel); michael@0: } michael@0: } michael@0: michael@0: // Register 'onopen' handler for the first local data channel michael@0: sourcePeer.dataChannels[0].onopen = function (channel) { michael@0: sourceChannel = channel; michael@0: check_next_test(); michael@0: }; michael@0: michael@0: // Register handlers for the target peer michael@0: targetPeer.registerDataChannelOpenEvents(function (channel) { michael@0: targetChannel = channel; michael@0: check_next_test(); michael@0: }); michael@0: michael@0: PeerConnectionTest.prototype.setLocalDescription.call(this, targetPeer, desc, michael@0: state, michael@0: function () { michael@0: self.connected = true; michael@0: check_next_test(); michael@0: } michael@0: ); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * This class acts as a wrapper around a DataChannel instance. michael@0: * michael@0: * @param dataChannel michael@0: * @param peerConnectionWrapper michael@0: * @constructor michael@0: */ michael@0: function DataChannelWrapper(dataChannel, peerConnectionWrapper) { michael@0: this._channel = dataChannel; michael@0: this._pc = peerConnectionWrapper; michael@0: michael@0: info("Creating " + this); michael@0: michael@0: /** michael@0: * Setup appropriate callbacks michael@0: */ michael@0: michael@0: this.onclose = unexpectedEventAndFinish(this, 'onclose'); michael@0: this.onerror = unexpectedEventAndFinish(this, 'onerror'); michael@0: this.onmessage = unexpectedEventAndFinish(this, 'onmessage'); michael@0: this.onopen = unexpectedEventAndFinish(this, 'onopen'); michael@0: michael@0: var self = this; michael@0: michael@0: /** michael@0: * Callback for native data channel 'onclose' events. If no custom handler michael@0: * has been specified via 'this.onclose', a failure will be raised if an michael@0: * event of this type gets caught. michael@0: */ michael@0: this._channel.onclose = function () { michael@0: info(self + ": 'onclose' event fired"); michael@0: michael@0: self.onclose(self); michael@0: self.onclose = unexpectedEventAndFinish(self, 'onclose'); michael@0: }; michael@0: michael@0: /** michael@0: * Callback for native data channel 'onmessage' events. If no custom handler michael@0: * has been specified via 'this.onmessage', a failure will be raised if an michael@0: * event of this type gets caught. michael@0: * michael@0: * @param {Object} event michael@0: * Event data which includes the sent message michael@0: */ michael@0: this._channel.onmessage = function (event) { michael@0: info(self + ": 'onmessage' event fired for '" + event.data + "'"); michael@0: michael@0: self.onmessage(event.data); michael@0: self.onmessage = unexpectedEventAndFinish(self, 'onmessage'); michael@0: }; michael@0: michael@0: /** michael@0: * Callback for native data channel 'onopen' events. If no custom handler michael@0: * has been specified via 'this.onopen', a failure will be raised if an michael@0: * event of this type gets caught. michael@0: */ michael@0: this._channel.onopen = function () { michael@0: info(self + ": 'onopen' event fired"); michael@0: michael@0: self.onopen(self); michael@0: self.onopen = unexpectedEventAndFinish(self, 'onopen'); michael@0: }; michael@0: } michael@0: michael@0: DataChannelWrapper.prototype = { michael@0: /** michael@0: * Returns the binary type of the channel michael@0: * michael@0: * @returns {String} The binary type michael@0: */ michael@0: get binaryType() { michael@0: return this._channel.binaryType; michael@0: }, michael@0: michael@0: /** michael@0: * Sets the binary type of the channel michael@0: * michael@0: * @param {String} type michael@0: * The new binary type of the channel michael@0: */ michael@0: set binaryType(type) { michael@0: this._channel.binaryType = type; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the label of the underlying data channel michael@0: * michael@0: * @returns {String} The label michael@0: */ michael@0: get label() { michael@0: return this._channel.label; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the protocol of the underlying data channel michael@0: * michael@0: * @returns {String} The protocol michael@0: */ michael@0: get protocol() { michael@0: return this._channel.protocol; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the id of the underlying data channel michael@0: * michael@0: * @returns {number} The stream id michael@0: */ michael@0: get id() { michael@0: return this._channel.id; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the reliable state of the underlying data channel michael@0: * michael@0: * @returns {bool} The stream's reliable state michael@0: */ michael@0: get reliable() { michael@0: return this._channel.reliable; michael@0: }, michael@0: michael@0: // ordered, maxRetransmits and maxRetransmitTime not exposed yet michael@0: michael@0: /** michael@0: * Returns the readyState bit of the data channel michael@0: * michael@0: * @returns {String} The state of the channel michael@0: */ michael@0: get readyState() { michael@0: return this._channel.readyState; michael@0: }, michael@0: michael@0: /** michael@0: * Close the data channel michael@0: */ michael@0: close : function () { michael@0: info(this + ": Closing channel"); michael@0: this._channel.close(); michael@0: }, michael@0: michael@0: /** michael@0: * Send data through the data channel michael@0: * michael@0: * @param {String|Object} data michael@0: * Data which has to be sent through the data channel michael@0: */ michael@0: send: function DCW_send(data) { michael@0: info(this + ": Sending data '" + data + "'"); michael@0: this._channel.send(data); michael@0: }, michael@0: michael@0: /** michael@0: * Returns the string representation of the class michael@0: * michael@0: * @returns {String} The string representation michael@0: */ michael@0: toString: function DCW_toString() { michael@0: return "DataChannelWrapper (" + this._pc.label + '_' + this._channel.label + ")"; michael@0: } michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * This class acts as a wrapper around a PeerConnection instance. michael@0: * michael@0: * @constructor michael@0: * @param {string} label michael@0: * Description for the peer connection instance michael@0: * @param {object} configuration michael@0: * Configuration for the peer connection instance michael@0: */ michael@0: function PeerConnectionWrapper(label, configuration) { michael@0: this.configuration = configuration; michael@0: this.label = label; michael@0: this.whenCreated = Date.now(); michael@0: michael@0: this.constraints = [ ]; michael@0: this.offerConstraints = {}; michael@0: this.streams = [ ]; michael@0: this.mediaCheckers = [ ]; michael@0: michael@0: this.dataChannels = [ ]; michael@0: michael@0: info("Creating " + this); michael@0: this._pc = new mozRTCPeerConnection(this.configuration); michael@0: is(this._pc.iceConnectionState, "new", "iceConnectionState starts at 'new'"); michael@0: michael@0: /** michael@0: * Setup callback handlers michael@0: */ michael@0: var self = this; michael@0: // This enables tests to validate that the next ice state is the one they expect to happen michael@0: this.next_ice_state = ""; // in most cases, the next state will be "checking", but in some tests "closed" michael@0: // This allows test to register their own callbacks for ICE connection state changes michael@0: this.ice_connection_callbacks = {}; michael@0: michael@0: this._pc.oniceconnectionstatechange = function() { michael@0: ok(self._pc.iceConnectionState !== undefined, "iceConnectionState should not be undefined"); michael@0: info(self + ": oniceconnectionstatechange fired, new state is: " + self._pc.iceConnectionState); michael@0: Object.keys(self.ice_connection_callbacks).forEach(function(name) { michael@0: self.ice_connection_callbacks[name](); michael@0: }); michael@0: if (self.next_ice_state !== "") { michael@0: is(self._pc.iceConnectionState, self.next_ice_state, "iceConnectionState changed to '" + michael@0: self.next_ice_state + "'"); michael@0: self.next_ice_state = ""; michael@0: } michael@0: }; michael@0: this.ondatachannel = unexpectedEventAndFinish(this, 'ondatachannel'); michael@0: this.onsignalingstatechange = unexpectedEventAndFinish(this, 'onsignalingstatechange'); michael@0: michael@0: /** michael@0: * Callback for native peer connection 'onaddstream' events. michael@0: * michael@0: * @param {Object} event michael@0: * Event data which includes the stream to be added michael@0: */ michael@0: this._pc.onaddstream = function (event) { michael@0: info(self + ": 'onaddstream' event fired for " + JSON.stringify(event.stream)); michael@0: michael@0: var type = ''; michael@0: if (event.stream.getAudioTracks().length > 0) { michael@0: type = 'audio'; michael@0: } michael@0: if (event.stream.getVideoTracks().length > 0) { michael@0: type += 'video'; michael@0: } michael@0: self.attachMedia(event.stream, type, 'remote'); michael@0: }; michael@0: michael@0: /** michael@0: * Callback for native peer connection 'ondatachannel' events. If no custom handler michael@0: * has been specified via 'this.ondatachannel', a failure will be raised if an michael@0: * event of this type gets caught. michael@0: * michael@0: * @param {Object} event michael@0: * Event data which includes the newly created data channel michael@0: */ michael@0: this._pc.ondatachannel = function (event) { michael@0: info(self + ": 'ondatachannel' event fired for " + event.channel.label); michael@0: michael@0: self.ondatachannel(new DataChannelWrapper(event.channel, self)); michael@0: self.ondatachannel = unexpectedEventAndFinish(self, 'ondatachannel'); michael@0: }; michael@0: michael@0: /** michael@0: * Callback for native peer connection 'onsignalingstatechange' events. If no michael@0: * custom handler has been specified via 'this.onsignalingstatechange', a michael@0: * failure will be raised if an event of this type is caught. michael@0: * michael@0: * @param {Object} aEvent michael@0: * Event data which includes the newly created data channel michael@0: */ michael@0: this._pc.onsignalingstatechange = function (aEvent) { michael@0: info(self + ": 'onsignalingstatechange' event fired"); michael@0: michael@0: // this calls the eventhandler only once and then overwrites it with the michael@0: // default unexpectedEvent handler michael@0: self.onsignalingstatechange(aEvent); michael@0: self.onsignalingstatechange = unexpectedEventAndFinish(self, 'onsignalingstatechange'); michael@0: }; michael@0: } michael@0: michael@0: PeerConnectionWrapper.prototype = { michael@0: michael@0: /** michael@0: * Returns the local description. michael@0: * michael@0: * @returns {object} The local description michael@0: */ michael@0: get localDescription() { michael@0: return this._pc.localDescription; michael@0: }, michael@0: michael@0: /** michael@0: * Sets the local description. michael@0: * michael@0: * @param {object} desc michael@0: * The new local description michael@0: */ michael@0: set localDescription(desc) { michael@0: this._pc.localDescription = desc; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the readyState. michael@0: * michael@0: * @returns {string} michael@0: */ michael@0: get readyState() { michael@0: return this._pc.readyState; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the remote description. michael@0: * michael@0: * @returns {object} The remote description michael@0: */ michael@0: get remoteDescription() { michael@0: return this._pc.remoteDescription; michael@0: }, michael@0: michael@0: /** michael@0: * Sets the remote description. michael@0: * michael@0: * @param {object} desc michael@0: * The new remote description michael@0: */ michael@0: set remoteDescription(desc) { michael@0: this._pc.remoteDescription = desc; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the signaling state. michael@0: * michael@0: * @returns {object} The local description michael@0: */ michael@0: get signalingState() { michael@0: return this._pc.signalingState; michael@0: }, michael@0: /** michael@0: * Returns the ICE connection state. michael@0: * michael@0: * @returns {object} The local description michael@0: */ michael@0: get iceConnectionState() { michael@0: return this._pc.iceConnectionState; michael@0: }, michael@0: michael@0: setIdentityProvider: function(provider, protocol, identity) { michael@0: this._pc.setIdentityProvider(provider, protocol, identity); michael@0: }, michael@0: michael@0: /** michael@0: * Callback when we get media from either side. Also an appropriate michael@0: * HTML media element will be created. michael@0: * michael@0: * @param {MediaStream} stream michael@0: * Media stream to handle michael@0: * @param {string} type michael@0: * The type of media stream ('audio' or 'video') michael@0: * @param {string} side michael@0: * The location the stream is coming from ('local' or 'remote') michael@0: */ michael@0: attachMedia : function PCW_attachMedia(stream, type, side) { michael@0: info("Got media stream: " + type + " (" + side + ")"); michael@0: this.streams.push(stream); michael@0: michael@0: if (side === 'local') { michael@0: this._pc.addStream(stream); michael@0: } michael@0: michael@0: var element = createMediaElement(type, this.label + '_' + side); michael@0: this.mediaCheckers.push(new MediaElementChecker(element)); michael@0: element.mozSrcObject = stream; michael@0: element.play(); michael@0: }, michael@0: michael@0: /** michael@0: * Requests all the media streams as specified in the constrains property. michael@0: * michael@0: * @param {function} onSuccess michael@0: * Callback to execute if all media has been requested successfully michael@0: */ michael@0: getAllUserMedia : function PCW_GetAllUserMedia(onSuccess) { michael@0: var self = this; michael@0: michael@0: function _getAllUserMedia(constraintsList, index) { michael@0: if (index < constraintsList.length) { michael@0: var constraints = constraintsList[index]; michael@0: michael@0: getUserMedia(constraints, function (stream) { michael@0: var type = ''; michael@0: michael@0: if (constraints.audio) { michael@0: type = 'audio'; michael@0: } michael@0: michael@0: if (constraints.video) { michael@0: type += 'video'; michael@0: } michael@0: michael@0: self.attachMedia(stream, type, 'local'); michael@0: michael@0: _getAllUserMedia(constraintsList, index + 1); michael@0: }, generateErrorCallback()); michael@0: } else { michael@0: onSuccess(); michael@0: } michael@0: } michael@0: michael@0: info("Get " + this.constraints.length + " local streams"); michael@0: _getAllUserMedia(this.constraints, 0); michael@0: }, michael@0: michael@0: /** michael@0: * Create a new data channel instance michael@0: * michael@0: * @param {Object} options michael@0: * Options which get forwarded to nsIPeerConnection.createDataChannel michael@0: * @param {function} [onCreation=undefined] michael@0: * Callback to execute when the local data channel has been created michael@0: * @returns {DataChannelWrapper} The created data channel michael@0: */ michael@0: createDataChannel : function PCW_createDataChannel(options, onCreation) { michael@0: var label = 'channel_' + this.dataChannels.length; michael@0: info(this + ": Create data channel '" + label); michael@0: michael@0: var channel = this._pc.createDataChannel(label, options); michael@0: var wrapper = new DataChannelWrapper(channel, this); michael@0: michael@0: if (onCreation) { michael@0: wrapper.onopen = function () { michael@0: onCreation(wrapper); michael@0: }; michael@0: } michael@0: michael@0: this.dataChannels.push(wrapper); michael@0: return wrapper; michael@0: }, michael@0: michael@0: /** michael@0: * Creates an offer and automatically handles the failure case. michael@0: * michael@0: * @param {function} onSuccess michael@0: * Callback to execute if the offer was created successfully michael@0: */ michael@0: createOffer : function PCW_createOffer(onSuccess) { michael@0: var self = this; michael@0: michael@0: this._pc.createOffer(function (offer) { michael@0: info("Got offer: " + JSON.stringify(offer)); michael@0: self._last_offer = offer; michael@0: onSuccess(offer); michael@0: }, generateErrorCallback(), this.offerConstraints); michael@0: }, michael@0: michael@0: /** michael@0: * Creates an answer and automatically handles the failure case. michael@0: * michael@0: * @param {function} onSuccess michael@0: * Callback to execute if the answer was created successfully michael@0: */ michael@0: createAnswer : function PCW_createAnswer(onSuccess) { michael@0: var self = this; michael@0: michael@0: this._pc.createAnswer(function (answer) { michael@0: info(self + ": Got answer: " + JSON.stringify(answer)); michael@0: self._last_answer = answer; michael@0: onSuccess(answer); michael@0: }, generateErrorCallback()); michael@0: }, michael@0: michael@0: /** michael@0: * Sets the local description and automatically handles the failure case. michael@0: * michael@0: * @param {object} desc michael@0: * mozRTCSessionDescription for the local description request michael@0: * @param {function} onSuccess michael@0: * Callback to execute if the local description was set successfully michael@0: */ michael@0: setLocalDescription : function PCW_setLocalDescription(desc, onSuccess) { michael@0: var self = this; michael@0: this._pc.setLocalDescription(desc, function () { michael@0: info(self + ": Successfully set the local description"); michael@0: onSuccess(); michael@0: }, generateErrorCallback()); michael@0: }, michael@0: michael@0: /** michael@0: * Tries to set the local description and expect failure. Automatically michael@0: * causes the test case to fail if the call succeeds. michael@0: * michael@0: * @param {object} desc michael@0: * mozRTCSessionDescription for the local description request michael@0: * @param {function} onFailure michael@0: * Callback to execute if the call fails. michael@0: */ michael@0: setLocalDescriptionAndFail : function PCW_setLocalDescriptionAndFail(desc, onFailure) { michael@0: var self = this; michael@0: this._pc.setLocalDescription(desc, michael@0: generateErrorCallback("setLocalDescription should have failed."), michael@0: function (err) { michael@0: info(self + ": As expected, failed to set the local description"); michael@0: onFailure(err); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Sets the remote description and automatically handles the failure case. michael@0: * michael@0: * @param {object} desc michael@0: * mozRTCSessionDescription for the remote description request michael@0: * @param {function} onSuccess michael@0: * Callback to execute if the remote description was set successfully michael@0: */ michael@0: setRemoteDescription : function PCW_setRemoteDescription(desc, onSuccess) { michael@0: var self = this; michael@0: this._pc.setRemoteDescription(desc, function () { michael@0: info(self + ": Successfully set remote description"); michael@0: onSuccess(); michael@0: }, generateErrorCallback()); michael@0: }, michael@0: michael@0: /** michael@0: * Tries to set the remote description and expect failure. Automatically michael@0: * causes the test case to fail if the call succeeds. michael@0: * michael@0: * @param {object} desc michael@0: * mozRTCSessionDescription for the remote description request michael@0: * @param {function} onFailure michael@0: * Callback to execute if the call fails. michael@0: */ michael@0: setRemoteDescriptionAndFail : function PCW_setRemoteDescriptionAndFail(desc, onFailure) { michael@0: var self = this; michael@0: this._pc.setRemoteDescription(desc, michael@0: generateErrorCallback("setRemoteDescription should have failed."), michael@0: function (err) { michael@0: info(self + ": As expected, failed to set the remote description"); michael@0: onFailure(err); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Adds an ICE candidate and automatically handles the failure case. michael@0: * michael@0: * @param {object} candidate michael@0: * SDP candidate michael@0: * @param {function} onSuccess michael@0: * Callback to execute if the local description was set successfully michael@0: */ michael@0: addIceCandidate : function PCW_addIceCandidate(candidate, onSuccess) { michael@0: var self = this; michael@0: michael@0: this._pc.addIceCandidate(candidate, function () { michael@0: info(self + ": Successfully added an ICE candidate"); michael@0: onSuccess(); michael@0: }, generateErrorCallback()); michael@0: }, michael@0: michael@0: /** michael@0: * Tries to add an ICE candidate and expects failure. Automatically michael@0: * causes the test case to fail if the call succeeds. michael@0: * michael@0: * @param {object} candidate michael@0: * SDP candidate michael@0: * @param {function} onFailure michael@0: * Callback to execute if the call fails. michael@0: */ michael@0: addIceCandidateAndFail : function PCW_addIceCandidateAndFail(candidate, onFailure) { michael@0: var self = this; michael@0: michael@0: this._pc.addIceCandidate(candidate, michael@0: generateErrorCallback("addIceCandidate should have failed."), michael@0: function (err) { michael@0: info(self + ": As expected, failed to add an ICE candidate"); michael@0: onFailure(err); michael@0: }) ; michael@0: }, michael@0: michael@0: /** michael@0: * Returns if the ICE the connection state is "connected". michael@0: * michael@0: * @returns {boolean} True if the connection state is "connected", otherwise false. michael@0: */ michael@0: isIceConnected : function PCW_isIceConnected() { michael@0: info("iceConnectionState: " + this.iceConnectionState); michael@0: return this.iceConnectionState === "connected"; michael@0: }, michael@0: michael@0: /** michael@0: * Returns if the ICE the connection state is "checking". michael@0: * michael@0: * @returns {boolean} True if the connection state is "checking", otherwise false. michael@0: */ michael@0: isIceChecking : function PCW_isIceChecking() { michael@0: return this.iceConnectionState === "checking"; michael@0: }, michael@0: michael@0: /** michael@0: * Returns if the ICE the connection state is "new". michael@0: * michael@0: * @returns {boolean} True if the connection state is "new", otherwise false. michael@0: */ michael@0: isIceNew : function PCW_isIceNew() { michael@0: return this.iceConnectionState === "new"; michael@0: }, michael@0: michael@0: /** michael@0: * Checks if the ICE connection state still waits for a connection to get michael@0: * established. michael@0: * michael@0: * @returns {boolean} True if the connection state is "checking" or "new", michael@0: * otherwise false. michael@0: */ michael@0: isIceConnectionPending : function PCW_isIceConnectionPending() { michael@0: return (this.isIceChecking() || this.isIceNew()); michael@0: }, michael@0: michael@0: /** michael@0: * Registers a callback for the ICE connection state change and michael@0: * reports success (=connected) or failure via the callbacks. michael@0: * States "new" and "checking" are ignored. michael@0: * michael@0: * @param {function} onSuccess michael@0: * Callback if ICE connection status is "connected". michael@0: * @param {function} onFailure michael@0: * Callback if ICE connection reaches a different state than michael@0: * "new", "checking" or "connected". michael@0: */ michael@0: waitForIceConnected : function PCW_waitForIceConnected(onSuccess, onFailure) { michael@0: var self = this; michael@0: var mySuccess = onSuccess; michael@0: var myFailure = onFailure; michael@0: michael@0: function iceConnectedChanged () { michael@0: if (self.isIceConnected()) { michael@0: delete self.ice_connection_callbacks.waitForIceConnected; michael@0: mySuccess(); michael@0: } else if (! self.isIceConnectionPending()) { michael@0: delete self.ice_connection_callbacks.waitForIceConnected; michael@0: myFailure(); michael@0: } michael@0: } michael@0: michael@0: self.ice_connection_callbacks.waitForIceConnected = iceConnectedChanged; michael@0: }, michael@0: michael@0: /** michael@0: * Checks that we are getting the media streams we expect. michael@0: * michael@0: * @param {object} constraintsRemote michael@0: * The media constraints of the remote peer connection object michael@0: */ michael@0: checkMediaStreams : function PCW_checkMediaStreams(constraintsRemote) { michael@0: is(this._pc.getLocalStreams().length, this.constraints.length, michael@0: this + ' has ' + this.constraints.length + ' local streams'); michael@0: michael@0: // TODO: change this when multiple incoming streams are supported (bug 834835) michael@0: is(this._pc.getRemoteStreams().length, 1, michael@0: this + ' has ' + 1 + ' remote streams'); michael@0: }, michael@0: michael@0: /** michael@0: * Check that media flow is present on all media elements involved in this michael@0: * test by waiting for confirmation that media flow is present. michael@0: * michael@0: * @param {Function} onSuccess the success callback when media flow michael@0: * is confirmed on all media elements michael@0: */ michael@0: checkMediaFlowPresent : function PCW_checkMediaFlowPresent(onSuccess) { michael@0: var self = this; michael@0: michael@0: function _checkMediaFlowPresent(index, onSuccess) { michael@0: if(index >= self.mediaCheckers.length) { michael@0: onSuccess(); michael@0: } else { michael@0: var mediaChecker = self.mediaCheckers[index]; michael@0: mediaChecker.waitForMediaFlow(function() { michael@0: _checkMediaFlowPresent(index + 1, onSuccess); michael@0: }); michael@0: } michael@0: } michael@0: michael@0: _checkMediaFlowPresent(0, onSuccess); michael@0: }, michael@0: michael@0: /** michael@0: * Check that stats are present by checking for known stats. michael@0: * michael@0: * @param {Function} onSuccess the success callback to return stats to michael@0: */ michael@0: getStats : function PCW_getStats(selector, onSuccess) { michael@0: var self = this; michael@0: michael@0: this._pc.getStats(selector, function(stats) { michael@0: info(self + ": Got stats: " + JSON.stringify(stats)); michael@0: self._last_stats = stats; michael@0: onSuccess(stats); michael@0: }, generateErrorCallback()); michael@0: }, michael@0: michael@0: /** michael@0: * Checks that we are getting the media streams we expect. michael@0: * michael@0: * @param {object} stats michael@0: * The stats to check from this PeerConnectionWrapper michael@0: */ michael@0: checkStats : function PCW_checkStats(stats) { michael@0: function toNum(obj) { michael@0: return obj? obj : 0; michael@0: } michael@0: function numTracks(streams) { michael@0: var n = 0; michael@0: streams.forEach(function(stream) { michael@0: n += stream.getAudioTracks().length + stream.getVideoTracks().length; michael@0: }); michael@0: return n; michael@0: } michael@0: michael@0: const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1; michael@0: michael@0: // Use spec way of enumerating stats michael@0: var counters = {}; michael@0: for (var key in stats) { michael@0: if (stats.hasOwnProperty(key)) { michael@0: var res = stats[key]; michael@0: // validate stats michael@0: ok(res.id == key, "Coherent stats id"); michael@0: var nowish = Date.now() + 1000; // TODO: clock drift observed michael@0: var minimum = this.whenCreated - 1000; // on Windows XP (Bug 979649) michael@0: if (isWinXP) { michael@0: todo(false, "Can't reliably test rtcp timestamps on WinXP (Bug 979649)"); michael@0: } else { michael@0: ok(res.timestamp >= minimum, michael@0: "Valid " + (res.isRemote? "rtcp" : "rtp") + " timestamp " + michael@0: res.timestamp + " >= " + minimum + " (" + michael@0: (res.timestamp - minimum) + " ms)"); michael@0: ok(res.timestamp <= nowish, michael@0: "Valid " + (res.isRemote? "rtcp" : "rtp") + " timestamp " + michael@0: res.timestamp + " <= " + nowish + " (" + michael@0: (res.timestamp - nowish) + " ms)"); michael@0: } michael@0: if (!res.isRemote) { michael@0: counters[res.type] = toNum(counters[res.type]) + 1; michael@0: michael@0: switch (res.type) { michael@0: case "inboundrtp": michael@0: case "outboundrtp": { michael@0: // ssrc is a 32 bit number returned as a string by spec michael@0: ok(res.ssrc.length > 0, "Ssrc has length"); michael@0: ok(res.ssrc.length < 11, "Ssrc not lengthy"); michael@0: ok(!/[^0-9]/.test(res.ssrc), "Ssrc numeric"); michael@0: ok(parseInt(res.ssrc) < Math.pow(2,32), "Ssrc within limits"); michael@0: michael@0: if (res.type == "outboundrtp") { michael@0: ok(res.packetsSent !== undefined, "Rtp packetsSent"); michael@0: // minimum fragment is 8 (from RFC 791) michael@0: ok(res.bytesSent >= res.packetsSent * 8, "Rtp bytesSent"); michael@0: } else { michael@0: ok(res.packetsReceived !== undefined, "Rtp packetsReceived"); michael@0: ok(res.bytesReceived >= res.packetsReceived * 8, "Rtp bytesReceived"); michael@0: } michael@0: if (res.remoteId) { michael@0: var rem = stats[res.remoteId]; michael@0: ok(rem.isRemote, "Remote is rtcp"); michael@0: ok(rem.remoteId == res.id, "Remote backlink match"); michael@0: if(res.type == "outboundrtp") { michael@0: ok(rem.type == "inboundrtp", "Rtcp is inbound"); michael@0: ok(rem.packetsReceived !== undefined, "Rtcp packetsReceived"); michael@0: ok(rem.packetsReceived <= res.packetsSent, "No more than sent"); michael@0: ok(rem.packetsLost !== undefined, "Rtcp packetsLost"); michael@0: ok(rem.bytesReceived >= rem.packetsReceived * 8, "Rtcp bytesReceived"); michael@0: ok(rem.bytesReceived <= res.bytesSent, "No more than sent bytes"); michael@0: ok(rem.jitter !== undefined, "Rtcp jitter"); michael@0: ok(rem.mozRtt !== undefined, "Rtcp rtt"); michael@0: ok(rem.mozRtt >= 0, "Rtcp rtt " + rem.mozRtt + " >= 0"); michael@0: ok(rem.mozRtt < 60000, "Rtcp rtt " + rem.mozRtt + " < 1 min"); michael@0: } else { michael@0: ok(rem.type == "outboundrtp", "Rtcp is outbound"); michael@0: ok(rem.packetsSent !== undefined, "Rtcp packetsSent"); michael@0: // We may have received more than outdated Rtcp packetsSent michael@0: ok(rem.bytesSent >= rem.packetsSent * 8, "Rtcp bytesSent"); michael@0: } michael@0: ok(rem.ssrc == res.ssrc, "Remote ssrc match"); michael@0: } else { michael@0: info("No rtcp info received yet"); michael@0: } michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Use MapClass way of enumerating stats michael@0: var counters2 = {}; michael@0: stats.forEach(function(res) { michael@0: if (!res.isRemote) { michael@0: counters2[res.type] = toNum(counters2[res.type]) + 1; michael@0: } michael@0: }); michael@0: is(JSON.stringify(counters), JSON.stringify(counters2), michael@0: "Spec and MapClass variant of RTCStatsReport enumeration agree"); michael@0: var nin = numTracks(this._pc.getRemoteStreams()); michael@0: var nout = numTracks(this._pc.getLocalStreams()); michael@0: michael@0: // TODO(Bug 957145): Restore stronger inboundrtp test once Bug 948249 is fixed michael@0: //is(toNum(counters["inboundrtp"]), nin, "Have " + nin + " inboundrtp stat(s)"); michael@0: ok(toNum(counters.inboundrtp) >= nin, "Have at least " + nin + " inboundrtp stat(s) *"); michael@0: michael@0: is(toNum(counters.outboundrtp), nout, "Have " + nout + " outboundrtp stat(s)"); michael@0: michael@0: var numLocalCandidates = toNum(counters.localcandidate); michael@0: var numRemoteCandidates = toNum(counters.remotecandidate); michael@0: // If there are no tracks, there will be no stats either. michael@0: if (nin + nout > 0) { michael@0: ok(numLocalCandidates, "Have localcandidate stat(s)"); michael@0: ok(numRemoteCandidates, "Have remotecandidate stat(s)"); michael@0: } else { michael@0: is(numLocalCandidates, 0, "Have no localcandidate stats"); michael@0: is(numRemoteCandidates, 0, "Have no remotecandidate stats"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Closes the connection michael@0: */ michael@0: close : function PCW_close() { michael@0: // It might be that a test has already closed the pc. In those cases michael@0: // we should not fail. michael@0: try { michael@0: this._pc.close(); michael@0: info(this + ": Closed connection."); michael@0: } michael@0: catch (e) { michael@0: info(this + ": Failure in closing connection - " + e.message); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Register all events during the setup of the data channel michael@0: * michael@0: * @param {Function} onDataChannelOpened michael@0: * Callback to execute when the data channel has been opened michael@0: */ michael@0: registerDataChannelOpenEvents : function (onDataChannelOpened) { michael@0: info(this + ": Register callbacks for 'ondatachannel' and 'onopen'"); michael@0: michael@0: this.ondatachannel = function (targetChannel) { michael@0: targetChannel.onopen = function (targetChannel) { michael@0: onDataChannelOpened(targetChannel); michael@0: }; michael@0: michael@0: this.dataChannels.push(targetChannel); michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the string representation of the class michael@0: * michael@0: * @returns {String} The string representation michael@0: */ michael@0: toString : function PCW_toString() { michael@0: return "PeerConnectionWrapper (" + this.label + ")"; michael@0: } michael@0: };