gecko/dom/media/tests/mochitest/pc.js

1728 lines
51 KiB
JavaScript

/* 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 + ")";
}
};