mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
2316 lines
72 KiB
JavaScript
2316 lines
72 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/. */
|
|
|
|
"use strict";
|
|
|
|
const LOOPBACK_ADDR = "127.0.0.";
|
|
|
|
const iceStateTransitions = {
|
|
"new": ["checking", "closed"], //Note: 'failed' might need to added here
|
|
// even though it is not in the standard
|
|
"checking": ["new", "connected", "failed", "closed"], //Note: do we need to
|
|
// allow 'completed' in
|
|
// here as well?
|
|
"connected": ["new", "completed", "disconnected", "closed"],
|
|
"completed": ["new", "disconnected", "closed"],
|
|
"disconnected": ["new", "connected", "completed", "failed", "closed"],
|
|
"failed": ["new", "disconnected", "closed"],
|
|
"closed": []
|
|
}
|
|
|
|
const signalingStateTransitions = {
|
|
"stable": ["have-local-offer", "have-remote-offer", "closed"],
|
|
"have-local-offer": ["have-remote-pranswer", "stable", "closed", "have-local-offer"],
|
|
"have-remote-pranswer": ["stable", "closed", "have-remote-pranswer"],
|
|
"have-remote-offer": ["have-local-pranswer", "stable", "closed", "have-remote-offer"],
|
|
"have-local-pranswer": ["stable", "closed", "have-local-pranswer"],
|
|
"closed": []
|
|
}
|
|
|
|
/**
|
|
* 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++;
|
|
|
|
self.currentStepLabel = step[0];
|
|
info("Run step: " + self.currentStepLabel);
|
|
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');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Only calls info() if SimpleTest.info() is available
|
|
*/
|
|
function safeInfo(message) {
|
|
if (typeof(info) === "function") {
|
|
info(message);
|
|
}
|
|
}
|
|
|
|
// Also remove mode 0 if it's offered
|
|
// Note, we don't bother removing the fmtp lines, which makes a good test
|
|
// for some SDP parsing issues.
|
|
function removeVP8(sdp) {
|
|
var updated_sdp = sdp.replace("a=rtpmap:120 VP8/90000\r\n","");
|
|
updated_sdp = updated_sdp.replace("RTP/SAVPF 120 126 97\r\n","RTP/SAVPF 126 97\r\n");
|
|
updated_sdp = updated_sdp.replace("RTP/SAVPF 120 126\r\n","RTP/SAVPF 126\r\n");
|
|
updated_sdp = updated_sdp.replace("a=rtcp-fb:120 nack\r\n","");
|
|
updated_sdp = updated_sdp.replace("a=rtcp-fb:120 nack pli\r\n","");
|
|
updated_sdp = updated_sdp.replace("a=rtcp-fb:120 ccm fir\r\n","");
|
|
return updated_sdp;
|
|
}
|
|
|
|
/**
|
|
* 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 |
|
|
SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_IMS_INTERFACES |
|
|
SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_DUN_INTERFACES);
|
|
var num = itfList.getNumberOfInterface();
|
|
for (var i = 0; i < num; i++) {
|
|
var ips = {};
|
|
var prefixLengths = {};
|
|
var length = itfList.getInterface(i).getAddresses(ips, prefixLengths);
|
|
|
|
for (var j = 0; j < length; j++) {
|
|
var ip = ips.value[j];
|
|
// skip IPv6 address until bug 797262 is implemented
|
|
if (ip.indexOf(":") < 0) {
|
|
safeInfo("Network interface is ready with address: " + ip);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
// ip address is not available
|
|
safeInfo("Network interface is not ready, required additional network setup");
|
|
return false;
|
|
}
|
|
safeInfo("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(onSuccess) {
|
|
script.addMessageListener('network-ready', function (message) {
|
|
info("Network interface is ready");
|
|
onSuccess();
|
|
});
|
|
info("Setting up network interface");
|
|
script.sendAsyncMessage("prepare-network", true);
|
|
},
|
|
/**
|
|
* Utility for tearing down data connection.
|
|
*
|
|
* @param aCallback callback after data connection is closed.
|
|
*/
|
|
tearDownNetwork: function(onSuccess, onFailure) {
|
|
if (isNetworkReady()) {
|
|
script.addMessageListener('network-disabled', function (message) {
|
|
info("Network interface torn down");
|
|
script.destroy();
|
|
onSuccess();
|
|
});
|
|
info("Tearing down network interface");
|
|
script.sendAsyncMessage("network-cleanup", true);
|
|
} else {
|
|
info("No network to tear down");
|
|
onFailure();
|
|
}
|
|
}
|
|
};
|
|
|
|
return utils;
|
|
}
|
|
|
|
/**
|
|
* Setup network on Gonk if needed and execute test once network is up
|
|
*
|
|
*/
|
|
function startNetworkAndTest(onSuccess) {
|
|
if (!isNetworkReady()) {
|
|
SimpleTest.waitForExplicitFinish();
|
|
var utils = getNetworkUtils();
|
|
// Trigger network setup to obtain IP address before creating any PeerConnection.
|
|
utils.prepareNetwork(onSuccess);
|
|
} else {
|
|
onSuccess();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A wrapper around SimpleTest.finish() to handle B2G network teardown
|
|
*/
|
|
function networkTestFinished() {
|
|
if ("nsINetworkInterfaceListService" in SpecialPowers.Ci) {
|
|
var utils = getNetworkUtils();
|
|
utils.tearDownNetwork(SimpleTest.finish, SimpleTest.finish);
|
|
} else {
|
|
SimpleTest.finish();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A wrapper around runTest() which handles B2G network setup and teardown
|
|
*/
|
|
function runNetworkTest(testFunction) {
|
|
startNetworkAndTest(function() {
|
|
runTest(testFunction);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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_local=undefined]
|
|
* Configuration for the local peer connection instance
|
|
* @param {object} [options.config_remote=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;
|
|
|
|
if (typeof turnServers !== "undefined") {
|
|
if ((!options.turn_disabled_local) && (turnServers.local)) {
|
|
if (!options.hasOwnProperty("config_local")) {
|
|
options.config_local = {};
|
|
}
|
|
if (!options.config_local.hasOwnProperty("iceServers")) {
|
|
options.config_local.iceServers = turnServers.local.iceServers;
|
|
}
|
|
}
|
|
if ((!options.turn_disabled_remote) && (turnServers.remote)) {
|
|
if (!options.hasOwnProperty("config_remote")) {
|
|
options.config_remote = {};
|
|
}
|
|
if (!options.config_remote.hasOwnProperty("iceServers")) {
|
|
options.config_remote.iceServers = turnServers.remote.iceServers;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.is_local)
|
|
this.pcLocal = new PeerConnectionWrapper('pcLocal', options.config_local, options.h264);
|
|
else
|
|
this.pcLocal = null;
|
|
|
|
if (options.is_remote)
|
|
this.pcRemote = new PeerConnectionWrapper('pcRemote', options.config_remote || options.config_local, options.h264);
|
|
else
|
|
this.pcRemote = null;
|
|
|
|
// 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/);
|
|
}
|
|
|
|
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");
|
|
|
|
var self = this;
|
|
var closeTimeout = null;
|
|
var waitingForLocal = false;
|
|
var waitingForRemote = false;
|
|
var everythingClosed = false;
|
|
|
|
function verifyClosed() {
|
|
if ((self.waitingForLocal || self.waitingForRemote) ||
|
|
(self.pcLocal && (self.pcLocal.signalingState !== "closed")) ||
|
|
(self.pcRemote && (self.pcRemote.signalingState !== "closed"))) {
|
|
info("still waiting for closure");
|
|
}
|
|
else if (!everythingClosed) {
|
|
info("No closure pending");
|
|
if (self.pcLocal) {
|
|
is(self.pcLocal.signalingState, "closed", "pcLocal is in 'closed' state");
|
|
}
|
|
if (self.pcRemote) {
|
|
is(self.pcRemote.signalingState, "closed", "pcRemote is in 'closed' state");
|
|
}
|
|
clearTimeout(closeTimeout);
|
|
everythingClosed = true;
|
|
onSuccess();
|
|
}
|
|
}
|
|
|
|
function signalingstatechangeLocalClose(state) {
|
|
info("'onsignalingstatechange' event '" + state + "' received");
|
|
is(state, "closed", "onsignalingstatechange event is closed");
|
|
self.waitingForLocal = false;
|
|
verifyClosed();
|
|
}
|
|
|
|
function signalingstatechangeRemoteClose(state) {
|
|
info("'onsignalingstatechange' event '" + state + "' received");
|
|
is(state, "closed", "onsignalingstatechange event is closed");
|
|
self.waitingForRemote = false;
|
|
verifyClosed();
|
|
}
|
|
|
|
function closeEverything() {
|
|
if ((self.pcLocal) && (self.pcLocal.signalingState !== "closed")) {
|
|
info("Closing pcLocal");
|
|
self.pcLocal.onsignalingstatechange = signalingstatechangeLocalClose;
|
|
self.waitingForLocal = true;
|
|
self.pcLocal.close();
|
|
}
|
|
if ((self.pcRemote) && (self.pcRemote.signalingState !== "closed")) {
|
|
info("Closing pcRemote");
|
|
self.pcRemote.onsignalingstatechange = signalingstatechangeRemoteClose;
|
|
self.waitingForRemote = true;
|
|
self.pcRemote.close();
|
|
}
|
|
// give the signals handlers time to fire
|
|
setTimeout(verifyClosed, 1000);
|
|
}
|
|
|
|
closeTimeout = setTimeout(function() {
|
|
var closed = ((self.pcLocal && (self.pcLocal.signalingState === "closed")) &&
|
|
(self.pcRemote && (self.pcRemote.signalingState === "closed")));
|
|
ok(closed, "Closing PeerConnections timed out");
|
|
// it is not a success, but the show must go on
|
|
onSuccess();
|
|
}, 60000);
|
|
|
|
closeEverything();
|
|
};
|
|
|
|
/**
|
|
* Executes the next command.
|
|
*/
|
|
PeerConnectionTest.prototype.next = function PCT_next() {
|
|
if (this._stepTimeout) {
|
|
clearTimeout(this._stepTimeout);
|
|
this._stepTimeout = null;
|
|
}
|
|
this.chain.executeNext();
|
|
};
|
|
|
|
/**
|
|
* Set a timeout for the current step.
|
|
* @param {long] ms the number of milliseconds to allow for this step
|
|
*/
|
|
PeerConnectionTest.prototype.setStepTimeout = function(ms) {
|
|
this._stepTimeout = setTimeout(function() {
|
|
ok(false, "Step timed out: " + this.chain.currentStepLabel);
|
|
this.next();
|
|
}.bind(this), ms);
|
|
};
|
|
|
|
/**
|
|
* 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, stateExpected, onSuccess) {
|
|
var eventFired = false;
|
|
var stateChanged = false;
|
|
|
|
function check_next_test() {
|
|
if (eventFired && stateChanged) {
|
|
onSuccess();
|
|
}
|
|
}
|
|
|
|
peer.onsignalingstatechange = function (state) {
|
|
info(peer + ": 'onsignalingstatechange' event '" + state + "' received");
|
|
if(stateExpected === state && eventFired == false) {
|
|
eventFired = true;
|
|
peer.setLocalDescStableEventDate = new Date();
|
|
check_next_test();
|
|
} else {
|
|
ok(false, "This event has either already fired or there has been a " +
|
|
"mismatch between event received " + state +
|
|
" and event expected " + stateExpected);
|
|
}
|
|
};
|
|
|
|
peer.setLocalDescription(desc, function () {
|
|
stateChanged = true;
|
|
peer.setLocalDescDate = new Date();
|
|
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 options used on a createOffer call in the test.
|
|
*
|
|
* @param {object} options the media constraints to use on createOffer
|
|
*/
|
|
PeerConnectionTest.prototype.setOfferOptions =
|
|
function PCT_setOfferOptions(options) {
|
|
if (this.pcLocal)
|
|
this.pcLocal.offerOptions = options;
|
|
};
|
|
|
|
/**
|
|
* 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, stateExpected, onSuccess) {
|
|
var eventFired = false;
|
|
var stateChanged = false;
|
|
|
|
function check_next_test() {
|
|
if (eventFired && stateChanged) {
|
|
onSuccess();
|
|
}
|
|
}
|
|
|
|
peer.onsignalingstatechange = function (state) {
|
|
info(peer + ": 'onsignalingstatechange' event '" + state + "' received");
|
|
if(stateExpected === state && eventFired == false) {
|
|
eventFired = true;
|
|
peer.setRemoteDescStableEventDate = new Date();
|
|
check_next_test();
|
|
} else {
|
|
ok(false, "This event has either already fired or there has been a " +
|
|
"mismatch between event received " + state +
|
|
" and event expected " + stateExpected);
|
|
}
|
|
};
|
|
|
|
peer.setRemoteDescription(desc, function () {
|
|
stateChanged = true;
|
|
peer.setRemoteDescDate = new Date();
|
|
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)
|
|
networkTestFinished();
|
|
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_local=undefined]
|
|
* Configuration for the local peer connection instance
|
|
* @param {object} [options.config_remote=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 all connections have been closed
|
|
*/
|
|
value : function DCT_close(onSuccess) {
|
|
var self = this;
|
|
var pendingDcClose = []
|
|
var closeTimeout = null;
|
|
|
|
info("DataChannelTest.close() called");
|
|
|
|
function _closePeerConnection() {
|
|
info("DataChannelTest closing PeerConnection");
|
|
PeerConnectionTest.prototype.close.call(self, onSuccess);
|
|
}
|
|
|
|
function _closePeerConnectionCallback(index) {
|
|
info("_closePeerConnection called with index " + index);
|
|
var pos = pendingDcClose.indexOf(index);
|
|
if (pos != -1) {
|
|
pendingDcClose.splice(pos, 1);
|
|
}
|
|
else {
|
|
info("_closePeerConnection index " + index + " is missing from pendingDcClose: " + pendingDcClose);
|
|
}
|
|
if (pendingDcClose.length === 0) {
|
|
clearTimeout(closeTimeout);
|
|
_closePeerConnection();
|
|
}
|
|
}
|
|
|
|
var myDataChannels = null;
|
|
if (self.pcLocal) {
|
|
myDataChannels = self.pcLocal.dataChannels;
|
|
}
|
|
else if (self.pcRemote) {
|
|
myDataChannels = self.pcRemote.dataChannels;
|
|
}
|
|
var length = myDataChannels.length;
|
|
for (var i = 0; i < length; i++) {
|
|
var dataChannel = myDataChannels[i];
|
|
if (dataChannel.readyState !== "closed") {
|
|
pendingDcClose.push(i);
|
|
self.closeDataChannels(i, _closePeerConnectionCallback);
|
|
}
|
|
}
|
|
if (pendingDcClose.length === 0) {
|
|
_closePeerConnection();
|
|
}
|
|
else {
|
|
closeTimeout = setTimeout(function() {
|
|
ok(false, "Failed to properly close data channels: " +
|
|
pendingDcClose);
|
|
_closePeerConnection();
|
|
}, 60000);
|
|
}
|
|
}
|
|
},
|
|
|
|
closeDataChannels : {
|
|
/**
|
|
* Close the specified data channels
|
|
*
|
|
* @param {Number} index
|
|
* Index of the data channels to close on both sides
|
|
* @param {Function} onSuccess
|
|
* Callback to execute when the data channels has been closed
|
|
*/
|
|
value : function DCT_closeDataChannels(index, onSuccess) {
|
|
info("_closeDataChannels called with index: " + index);
|
|
var localChannel = null;
|
|
if (this.pcLocal) {
|
|
localChannel = this.pcLocal.dataChannels[index];
|
|
}
|
|
var remoteChannel = null;
|
|
if (this.pcRemote) {
|
|
remoteChannel = this.pcRemote.dataChannels[index];
|
|
}
|
|
|
|
var self = this;
|
|
var wait = false;
|
|
var pollingMode = false;
|
|
var everythingClosed = false;
|
|
var verifyInterval = null;
|
|
var remoteCloseTimer = null;
|
|
|
|
function _allChannelsAreClosed() {
|
|
var ret = null;
|
|
if (localChannel) {
|
|
ret = (localChannel.readyState === "closed");
|
|
}
|
|
if (remoteChannel) {
|
|
if (ret !== null) {
|
|
ret = (ret && (remoteChannel.readyState === "closed"));
|
|
}
|
|
else {
|
|
ret = (remoteChannel.readyState === "closed");
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
function verifyClosedChannels() {
|
|
if (everythingClosed) {
|
|
// safety protection against events firing late
|
|
return;
|
|
}
|
|
if (_allChannelsAreClosed) {
|
|
ok(true, "DataChannel(s) have reached 'closed' state for data channel " + index);
|
|
if (remoteCloseTimer !== null) {
|
|
clearTimeout(remoteCloseTimer);
|
|
}
|
|
if (verifyInterval !== null) {
|
|
clearInterval(verifyInterval);
|
|
}
|
|
everythingClosed = true;
|
|
onSuccess(index);
|
|
}
|
|
else {
|
|
info("Still waiting for DataChannel closure");
|
|
}
|
|
}
|
|
|
|
if ((localChannel) && (localChannel.readyState !== "closed")) {
|
|
// in case of steeplechase there is no far end, so we can only poll
|
|
if (remoteChannel) {
|
|
remoteChannel.onclose = function () {
|
|
is(remoteChannel.readyState, "closed", "remoteChannel is in state 'closed'");
|
|
verifyClosedChannels();
|
|
};
|
|
}
|
|
else {
|
|
pollingMode = true;
|
|
verifyInterval = setInterval(verifyClosedChannels, 1000);
|
|
}
|
|
|
|
localChannel.close();
|
|
wait = true;
|
|
}
|
|
if ((remoteChannel) && (remoteChannel.readyState !== "closed")) {
|
|
if (localChannel) {
|
|
localChannel.onclose = function () {
|
|
is(localChannel.readyState, "closed", "localChannel is in state 'closed'");
|
|
verifyClosedChannels();
|
|
};
|
|
|
|
// Apparently we are running a local test which has both ends of the
|
|
// data channel locally available, so by default lets wait for the
|
|
// remoteChannel.onclose handler from above to confirm closure on both
|
|
// ends.
|
|
remoteCloseTimer = setTimeout(function() {
|
|
todo(false, "localChannel.close() did not resulted in close signal on remote side");
|
|
remoteChannel.close();
|
|
verifyClosedChannels();
|
|
}, 30000);
|
|
}
|
|
else {
|
|
pollingMode = true;
|
|
verifyTimer = setInterval(verifyClosedChannels, 1000);
|
|
|
|
remoteChannel.close();
|
|
}
|
|
|
|
wait = true;
|
|
}
|
|
|
|
if (!wait) {
|
|
onSuccess(index);
|
|
}
|
|
}
|
|
},
|
|
|
|
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 (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 || { };
|
|
var source = options.sourceChannel ||
|
|
this.pcLocal.dataChannels[this.pcLocal.dataChannels.length - 1];
|
|
var 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.
|
|
*
|
|
* @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, state, onSuccess) {
|
|
PeerConnectionTest.prototype.setLocalDescription.call(this, peer,
|
|
desc, state, onSuccess);
|
|
|
|
}
|
|
},
|
|
|
|
waitForInitialDataChannel : {
|
|
/**
|
|
* Wait for the initial data channel to get into the open state
|
|
*
|
|
* @param {PeerConnectionWrapper} peer
|
|
* The peer connection wrapper to run the command on
|
|
* @param {Function} onSuccess
|
|
* Callback when the creation was successful
|
|
*/
|
|
value : function DCT_waitForInitialDataChannel(peer, onSuccess, onFailure) {
|
|
var dcConnectionTimeout = null;
|
|
var dcOpened = false;
|
|
|
|
function dataChannelConnected(channel) {
|
|
// in case the switch statement below had called onSuccess already we
|
|
// don't want to call it again
|
|
if (!dcOpened) {
|
|
clearTimeout(dcConnectionTimeout);
|
|
is(channel.readyState, "open", peer + " dataChannels[0] switched to state: 'open'");
|
|
dcOpened = true;
|
|
onSuccess();
|
|
}
|
|
}
|
|
|
|
// TODO: drno: convert dataChannels into an object and make
|
|
// registerDataChannelOpenEvent a generic function
|
|
if (peer == this.pcLocal) {
|
|
peer.dataChannels[0].onopen = dataChannelConnected;
|
|
} else {
|
|
peer.registerDataChannelOpenEvents(dataChannelConnected);
|
|
}
|
|
|
|
if (peer.dataChannels.length >= 1) {
|
|
// snapshot of the live value as it might change during test execution
|
|
const readyState = peer.dataChannels[0].readyState;
|
|
switch (readyState) {
|
|
case "open": {
|
|
is(readyState, "open", peer + " dataChannels[0] is already in state: 'open'");
|
|
dcOpened = true;
|
|
onSuccess();
|
|
break;
|
|
}
|
|
case "connecting": {
|
|
is(readyState, "connecting", peer + " dataChannels[0] is in state: 'connecting'");
|
|
if (onFailure) {
|
|
dcConnectionTimeout = setTimeout(function () {
|
|
is(peer.dataChannels[0].readyState, "open", peer + " timed out while waiting for dataChannels[0] to open");
|
|
onFailure();
|
|
}, 60000);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
ok(false, "dataChannels[0] is in unexpected state " + readyState);
|
|
if (onFailure) {
|
|
onFailure()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
* 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, h264) {
|
|
this.configuration = configuration;
|
|
this.label = label;
|
|
this.whenCreated = Date.now();
|
|
|
|
this.constraints = [ ];
|
|
this.offerOptions = {};
|
|
this.streams = [ ];
|
|
this.mediaCheckers = [ ];
|
|
|
|
this.dataChannels = [ ];
|
|
|
|
this.onAddStreamFired = false;
|
|
this.addStreamCallbacks = {};
|
|
|
|
this.h264 = typeof h264 !== "undefined" ? true : false;
|
|
|
|
info("Creating " + this);
|
|
this._pc = new mozRTCPeerConnection(this.configuration);
|
|
|
|
/**
|
|
* 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);
|
|
Object.keys(self.ice_connection_callbacks).forEach(function(name) {
|
|
self.ice_connection_callbacks[name]();
|
|
});
|
|
if (self.next_ice_state !== "") {
|
|
is(self._pc.iceConnectionState, self.next_ice_state, "iceConnectionState changed to '" +
|
|
self.next_ice_state + "'");
|
|
self.next_ice_state = "";
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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));
|
|
// TODO: remove this once Bugs 998552 and 998546 are closed
|
|
self.onAddStreamFired = true;
|
|
|
|
var type = '';
|
|
if (event.stream.getAudioTracks().length > 0) {
|
|
type = 'audio';
|
|
}
|
|
if (event.stream.getVideoTracks().length > 0) {
|
|
type += 'video';
|
|
}
|
|
self.attachMedia(event.stream, type, 'remote');
|
|
|
|
Object.keys(self.addStreamCallbacks).forEach(function(name) {
|
|
info(self + " calling addStreamCallback " + name);
|
|
self.addStreamCallbacks[name]();
|
|
});
|
|
};
|
|
|
|
this.ondatachannel = unexpectedEventAndFinish(this, 'ondatachannel');
|
|
|
|
/**
|
|
* 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');
|
|
};
|
|
|
|
this.onsignalingstatechange = unexpectedEventAndFinish(this, 'onsignalingstatechange');
|
|
this.signalingStateCallbacks = {};
|
|
|
|
/**
|
|
* 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 (anEvent) {
|
|
info(self + ": 'onsignalingstatechange' event fired");
|
|
|
|
Object.keys(self.signalingStateCallbacks).forEach(function(name) {
|
|
self.signalingStateCallbacks[name](anEvent);
|
|
});
|
|
// this calls the eventhandler only once and then overwrites it with the
|
|
// default unexpectedEvent handler
|
|
self.onsignalingstatechange(anEvent);
|
|
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 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') {
|
|
stream.getTracks().forEach(function(track) {
|
|
this._pc.addTrack(track, stream);
|
|
}.bind(this));
|
|
}
|
|
|
|
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);
|
|
}, generateErrorCallback());
|
|
} else {
|
|
onSuccess();
|
|
}
|
|
}
|
|
|
|
if (this.constraints.length === 0) {
|
|
info("Skipping GUM: no UserMedia requested");
|
|
onSuccess();
|
|
}
|
|
else {
|
|
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;
|
|
if (self.h264) {
|
|
isnot(offer.sdp.search("H264/90000"), -1, "H.264 should be present in the SDP offer");
|
|
offer.sdp = removeVP8(offer.sdp);
|
|
}
|
|
onSuccess(offer);
|
|
}, generateErrorCallback(), this.offerOptions);
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
}, generateErrorCallback());
|
|
},
|
|
|
|
/**
|
|
* 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();
|
|
}, generateErrorCallback());
|
|
},
|
|
|
|
/**
|
|
* 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,
|
|
generateErrorCallback("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();
|
|
}, generateErrorCallback());
|
|
},
|
|
|
|
/**
|
|
* 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,
|
|
generateErrorCallback("setRemoteDescription should have failed."),
|
|
function (err) {
|
|
info(self + ": As expected, failed to set the remote description");
|
|
onFailure(err);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Registers a callback for the signaling state change and
|
|
* appends the new state to an array for logging it later.
|
|
*/
|
|
logSignalingState: function PCW_logSignalingState() {
|
|
var self = this;
|
|
|
|
function _logSignalingState(state) {
|
|
var newstate = self._pc.signalingState;
|
|
var oldstate = self.signalingStateLog[self.signalingStateLog.length - 1]
|
|
if (Object.keys(signalingStateTransitions).indexOf(oldstate) != -1) {
|
|
ok(signalingStateTransitions[oldstate].indexOf(newstate) != -1, self + ": legal signaling state transition from " + oldstate + " to " + newstate);
|
|
} else {
|
|
ok(false, self + ": old signaling state " + oldstate + " missing in signaling transition array");
|
|
}
|
|
self.signalingStateLog.push(newstate);
|
|
}
|
|
|
|
self.signalingStateLog = [self._pc.signalingState];
|
|
self.signalingStateCallbacks.logSignalingStatus = _logSignalingState;
|
|
},
|
|
|
|
/**
|
|
* 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();
|
|
}, generateErrorCallback());
|
|
},
|
|
|
|
/**
|
|
* 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,
|
|
generateErrorCallback("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
|
|
* appends the new state to an array for logging it later.
|
|
*/
|
|
logIceConnectionState: function PCW_logIceConnectionState() {
|
|
var self = this;
|
|
|
|
function logIceConState () {
|
|
var newstate = self._pc.iceConnectionState;
|
|
var oldstate = self.iceConnectionLog[self.iceConnectionLog.length - 1]
|
|
if (Object.keys(iceStateTransitions).indexOf(oldstate) != -1) {
|
|
ok(iceStateTransitions[oldstate].indexOf(newstate) != -1, self + ": legal ICE state transition from " + oldstate + " to " + newstate);
|
|
} else {
|
|
ok(false, self + ": old ICE state " + oldstate + " missing in ICE transition array");
|
|
}
|
|
self.iceConnectionLog.push(newstate);
|
|
}
|
|
|
|
self.iceConnectionLog = [self._pc.iceConnectionState];
|
|
self.ice_connection_callbacks.logIceStatus = logIceConState;
|
|
},
|
|
|
|
/**
|
|
* 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 = iceConnectedChanged;
|
|
},
|
|
|
|
/**
|
|
* Counts the amount of audio tracks in a given media constraint.
|
|
*
|
|
* @param constraints
|
|
* The contraint to be examined.
|
|
*/
|
|
countAudioTracksInMediaConstraint : function
|
|
PCW_countAudioTracksInMediaConstraint(constraints) {
|
|
if ((!constraints) || (constraints.length === 0)) {
|
|
return 0;
|
|
}
|
|
var audioTracks = 0;
|
|
for (var i = 0; i < constraints.length; i++) {
|
|
if (constraints[i].audio) {
|
|
audioTracks++;
|
|
}
|
|
}
|
|
return audioTracks;
|
|
},
|
|
|
|
/**
|
|
* Checks for audio in given offer options.
|
|
*
|
|
* @param options
|
|
* The options to be examined.
|
|
*/
|
|
audioInOfferOptions : function
|
|
PCW_audioInOfferOptions(options) {
|
|
if (!options) {
|
|
return 0;
|
|
}
|
|
if (options.offerToReceiveAudio) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Counts the amount of video tracks in a given media constraint.
|
|
*
|
|
* @param constraint
|
|
* The contraint to be examined.
|
|
*/
|
|
countVideoTracksInMediaConstraint : function
|
|
PCW_countVideoTracksInMediaConstraint(constraints) {
|
|
if ((!constraints) || (constraints.length === 0)) {
|
|
return 0;
|
|
}
|
|
var videoTracks = 0;
|
|
for (var i = 0; i < constraints.length; i++) {
|
|
if (constraints[i].video) {
|
|
videoTracks++;
|
|
}
|
|
}
|
|
return videoTracks;
|
|
},
|
|
|
|
/**
|
|
* Checks for video in given offer options.
|
|
*
|
|
* @param options
|
|
* The options to be examined.
|
|
*/
|
|
videoInOfferOptions : function
|
|
PCW_videoInOfferOptions(options) {
|
|
if (!options) {
|
|
return 0;
|
|
}
|
|
if (options.offerToReceiveVideo) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Counts the amount of audio tracks in a given set of streams.
|
|
*
|
|
* @param streams
|
|
* An array of streams (as returned by getLocalStreams()) to be
|
|
* examined.
|
|
*/
|
|
countAudioTracksInStreams : function PCW_countAudioTracksInStreams(streams) {
|
|
if (!streams || (streams.length === 0)) {
|
|
return 0;
|
|
}
|
|
var audioTracks = 0;
|
|
streams.forEach(function(st) {
|
|
audioTracks += st.getAudioTracks().length;
|
|
});
|
|
return audioTracks;
|
|
},
|
|
|
|
/*
|
|
* Counts the amount of video tracks in a given set of streams.
|
|
*
|
|
* @param streams
|
|
* An array of streams (as returned by getLocalStreams()) to be
|
|
* examined.
|
|
*/
|
|
countVideoTracksInStreams: function PCW_countVideoTracksInStreams(streams) {
|
|
if (!streams || (streams.length === 0)) {
|
|
return 0;
|
|
}
|
|
var videoTracks = 0;
|
|
streams.forEach(function(st) {
|
|
videoTracks += st.getVideoTracks().length;
|
|
});
|
|
return videoTracks;
|
|
},
|
|
|
|
/**
|
|
* Checks that we are getting the media tracks we expect.
|
|
*
|
|
* @param {object} constraintsRemote
|
|
* The media constraints of the local and remote peer connection object
|
|
*/
|
|
checkMediaTracks : function PCW_checkMediaTracks(constraintsRemote, onSuccess) {
|
|
var self = this;
|
|
var addStreamTimeout = null;
|
|
|
|
function _checkMediaTracks(constraintsRemote, onSuccess) {
|
|
if (addStreamTimeout !== null) {
|
|
clearTimeout(addStreamTimeout);
|
|
}
|
|
|
|
var localConstraintAudioTracks =
|
|
self.countAudioTracksInMediaConstraint(self.constraints);
|
|
var localStreams = self._pc.getLocalStreams();
|
|
var localAudioTracks = self.countAudioTracksInStreams(localStreams, false);
|
|
is(localAudioTracks, localConstraintAudioTracks, self + ' has ' +
|
|
localAudioTracks + ' local audio tracks');
|
|
|
|
var localConstraintVideoTracks =
|
|
self.countVideoTracksInMediaConstraint(self.constraints);
|
|
var localVideoTracks = self.countVideoTracksInStreams(localStreams, false);
|
|
is(localVideoTracks, localConstraintVideoTracks, self + ' has ' +
|
|
localVideoTracks + ' local video tracks');
|
|
|
|
var remoteConstraintAudioTracks =
|
|
self.countAudioTracksInMediaConstraint(constraintsRemote);
|
|
var remoteStreams = self._pc.getRemoteStreams();
|
|
var remoteAudioTracks = self.countAudioTracksInStreams(remoteStreams, false);
|
|
is(remoteAudioTracks, remoteConstraintAudioTracks, self + ' has ' +
|
|
remoteAudioTracks + ' remote audio tracks');
|
|
|
|
var remoteConstraintVideoTracks =
|
|
self.countVideoTracksInMediaConstraint(constraintsRemote);
|
|
var remoteVideoTracks = self.countVideoTracksInStreams(remoteStreams, false);
|
|
is(remoteVideoTracks, remoteConstraintVideoTracks, self + ' has ' +
|
|
remoteVideoTracks + ' remote video tracks');
|
|
|
|
onSuccess();
|
|
}
|
|
|
|
// we have to do this check as the onaddstream never fires if the remote
|
|
// stream has no track at all!
|
|
var expectedRemoteTracks =
|
|
self.countAudioTracksInMediaConstraint(constraintsRemote) +
|
|
self.countVideoTracksInMediaConstraint(constraintsRemote);
|
|
|
|
// TODO: remove this once Bugs 998552 and 998546 are closed
|
|
if ((self.onAddStreamFired) || (expectedRemoteTracks == 0)) {
|
|
_checkMediaTracks(constraintsRemote, onSuccess);
|
|
} else {
|
|
info(self + " checkMediaTracks() got called before onAddStream fired");
|
|
// we rely on the outer mochitest timeout to catch the case where
|
|
// onaddstream never fires
|
|
self.addStreamCallbacks.checkMediaTracks = function() {
|
|
_checkMediaTracks(constraintsRemote, onSuccess);
|
|
};
|
|
addStreamTimeout = setTimeout(function () {
|
|
ok(self.onAddStreamFired, self + " checkMediaTracks() timed out waiting for onaddstream event to fire");
|
|
if (!self.onAddStreamFired) {
|
|
onSuccess();
|
|
}
|
|
}, 60000);
|
|
}
|
|
},
|
|
|
|
verifySdp : function PCW_verifySdp(desc, expectedType, constraints, offerOptions) {
|
|
info("Examining this SessionDescription: " + JSON.stringify(desc));
|
|
info("constraints: " + JSON.stringify(constraints));
|
|
info("offerOptions: " + JSON.stringify(offerOptions));
|
|
ok(desc, "SessionDescription is not null");
|
|
is(desc.type, expectedType, "SessionDescription type is " + expectedType);
|
|
ok(desc.sdp.length > 10, "SessionDescription body length is plausible");
|
|
ok(desc.sdp.contains("a=ice-ufrag"), "ICE username is present in SDP");
|
|
ok(desc.sdp.contains("a=ice-pwd"), "ICE password is present in SDP");
|
|
ok(desc.sdp.contains("a=fingerprint"), "ICE fingerprint is present in SDP");
|
|
//TODO: update this for loopback support bug 1027350
|
|
ok(!desc.sdp.contains(LOOPBACK_ADDR), "loopback interface is absent from SDP");
|
|
//TODO: update this for trickle ICE bug 1041832
|
|
ok(desc.sdp.contains("a=candidate"), "at least one ICE candidate is present in SDP");
|
|
//TODO: how can we check for absence/presence of m=application?
|
|
|
|
//TODO: how to handle media contraints + offer options
|
|
var audioTracks = this.countAudioTracksInMediaConstraint(constraints);
|
|
if (constraints.length === 0) {
|
|
audioTracks = this.audioInOfferOptions(offerOptions);
|
|
}
|
|
info("expected audio tracks: " + audioTracks);
|
|
if (audioTracks == 0) {
|
|
ok(!desc.sdp.contains("m=audio"), "audio m-line is absent from SDP");
|
|
} else {
|
|
ok(desc.sdp.contains("m=audio"), "audio m-line is present in SDP");
|
|
ok(desc.sdp.contains("a=rtpmap:109 opus/48000/2"), "OPUS codec is present in SDP");
|
|
//TODO: ideally the rtcp-mux should be for the m=audio, and not just
|
|
// anywhere in the SDP (JS SDP parser bug 1045429)
|
|
ok(desc.sdp.contains("a=rtcp-mux"), "RTCP Mux is offered in SDP");
|
|
|
|
}
|
|
|
|
//TODO: how to handle media contraints + offer options
|
|
var videoTracks = this.countVideoTracksInMediaConstraint(constraints);
|
|
if (constraints.length === 0) {
|
|
videoTracks = this.videoInOfferOptions(offerOptions);
|
|
}
|
|
info("expected video tracks: " + videoTracks);
|
|
if (videoTracks == 0) {
|
|
ok(!desc.sdp.contains("m=video"), "video m-line is absent from SDP");
|
|
} else {
|
|
ok(desc.sdp.contains("m=video"), "video m-line is present in SDP");
|
|
if (this.h264) {
|
|
ok(desc.sdp.contains("a=rtpmap:126 H264/90000"), "H.264 codec is present in SDP");
|
|
} else {
|
|
ok(desc.sdp.contains("a=rtpmap:120 VP8/90000"), "VP8 codec is present in SDP");
|
|
}
|
|
ok(desc.sdp.contains("a=rtcp-mux"), "RTCP Mux is offered in SDP");
|
|
}
|
|
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
}, generateErrorCallback());
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
|
|
|
|
// 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)
|
|
if (isWinXP) {
|
|
todo(false, "Can't reliably test rtcp timestamps on WinXP (Bug 979649)");
|
|
} else {
|
|
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");
|
|
// We assume minimum payload to be 1 byte (guess from RFC 3550)
|
|
ok(res.bytesSent >= res.packetsSent, "Rtp bytesSent");
|
|
} else {
|
|
ok(res.packetsReceived !== undefined, "Rtp packetsReceived");
|
|
ok(res.bytesReceived >= res.packetsReceived, "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, "Rtcp bytesReceived");
|
|
ok(rem.bytesReceived <= res.bytesSent, "No more than sent bytes");
|
|
ok(rem.jitter !== undefined, "Rtcp jitter");
|
|
ok(rem.mozRtt !== undefined, "Rtcp rtt");
|
|
ok(rem.mozRtt >= 0, "Rtcp rtt " + rem.mozRtt + " >= 0");
|
|
ok(rem.mozRtt < 60000, "Rtcp rtt " + rem.mozRtt + " < 1 min");
|
|
} 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, "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 = onDataChannelOpened;
|
|
this.dataChannels.push(targetChannel);
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Returns the string representation of the class
|
|
*
|
|
* @returns {String} The string representation
|
|
*/
|
|
toString : function PCW_toString() {
|
|
return "PeerConnectionWrapper (" + this.label + ")";
|
|
}
|
|
};
|