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