gecko/dom/media/tests/mochitest/pc.js
2013-05-16 21:47:50 -05:00

774 lines
21 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.
*/
function CommandChain(framework) {
this._framework = framework;
this._commands = [ ];
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;
}
};
/**
* Default list of commands to execute for a PeerConnection test.
*/
var commandsPeerConnection = [
[
'PC_LOCAL_GUM',
function (test) {
test.pcLocal.getAllUserMedia(function () {
test.next();
});
}
],
[
'PC_REMOTE_GUM',
function (test) {
test.pcRemote.getAllUserMedia(function () {
test.next();
});
}
],
[
'PC_CHECK_INITIAL_SIGNALINGSTATE',
function (test) {
is(test.pcLocal.signalingState,"stable", "Initial local signalingState is stable");
is(test.pcRemote.signalingState,"stable", "Initial remote signalingState is stable");
test.next();
}
],
[
'PC_LOCAL_CREATE_OFFER',
function (test) {
test.pcLocal.createOffer(function () {
is(test.pcLocal.signalingState, "stable", "Local create offer does not change signaling state");
test.next();
});
}
],
[
'PC_LOCAL_SET_LOCAL_DESCRIPTION',
function (test) {
test.expectStateChange(test.pcLocal, "have-local-offer", test);
test.pcLocal.setLocalDescription(test.pcLocal._last_offer,
test.checkStateInCallback(test.pcLocal, "have-local-offer", test));
}
],
[
'PC_REMOTE_SET_REMOTE_DESCRIPTION',
function (test) {
test.expectStateChange(test.pcRemote, "have-remote-offer", test);
test.pcRemote.setRemoteDescription(test.pcLocal._last_offer,
test.checkStateInCallback(test.pcRemote, "have-remote-offer", test));
}
],
[
'PC_REMOTE_CREATE_ANSWER',
function (test) {
test.pcRemote.createAnswer(function () {
is(test.pcRemote.signalingState, "have-remote-offer", "Remote create offer does not change signaling state");
test.next();
});
}
],
[
'PC_LOCAL_SET_REMOTE_DESCRIPTION',
function (test) {
test.expectStateChange(test.pcLocal, "stable", test);
test.pcLocal.setRemoteDescription(test.pcRemote._last_answer,
test.checkStateInCallback(test.pcLocal, "stable", test));
}
],
[
'PC_REMOTE_SET_LOCAL_DESCRIPTION',
function (test) {
test.expectStateChange(test.pcRemote, "stable", test);
test.pcRemote.setLocalDescription(test.pcRemote._last_answer,
test.checkStateInCallback(test.pcRemote, "stable", test));
}
],
[
'PC_LOCAL_CHECK_MEDIA',
function (test) {
test.pcLocal.checkMedia(test.pcRemote.constraints);
test.next();
}
],
[
'PC_REMOTE_CHECK_MEDIA',
function (test) {
test.pcRemote.checkMedia(test.pcLocal.constraints);
test.next();
}
]
];
/**
* This class handles tests for peer connections.
*
* @constructor
* @param {object} [options={}]
* Optional options for the peer connection 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 PeerConnectionTest(options) {
// If no options are specified make it an empty object
options = options || { };
this.pcLocal = new PeerConnectionWrapper('pcLocal', options.config_pc1);
this.pcRemote = new PeerConnectionWrapper('pcRemote', options.config_pc2 || options.config_pc1);
// Create command chain instance and assign default commands
this.chain = new CommandChain(this);
this.chain.commands = commandsPeerConnection;
var self = this;
this.chain.onFinished = function () {
self.teardown();
}
}
/**
* Executes the next command.
*/
PeerConnectionTest.prototype.next = function PCT_next() {
this.chain.executeNext();
};
/**
* 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) {
this.pcLocal.constraints = constraintsLocal;
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) {
this.pcLocal.offerConstraints = constraints;
};
/**
* 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() {
if (this.pcLocal) {
this.pcLocal.close();
this.pcLocal = null;
}
if (this.pcRemote) {
this.pcRemote.close();
this.pcRemote = null;
}
info("Test finished");
SimpleTest.finish();
};
/**
* Sets up the "onsignalingstatechange" handler for the indicated peerconnection
* as a one-shot test. If the test.commandSuccess flag is set when the event
* happens, then the next test in the command chain is triggered. After
* running, this sets the event handler so that it will fail the test if
* it fires again before we expect it. This is intended to be used in
* conjunction with checkStateInCallback, below.
*
* @param {pcw} PeerConnectionWrapper
* The peer connection to expect a state change on
* @param {state} string
* The state that we expect to change to
* @param {test} PeerConnectionTest
* The test strucure currently in use.
*/
PeerConnectionTest.prototype.expectStateChange =
function PCT_expectStateChange(pcw, state, test) {
pcw.signalingChangeEvent = false;
pcw._pc.onsignalingstatechange = function() {
pcw._pc.onsignalingstatechange = unexpectedCallbackAndFinish(new Error);
is(pcw._pc.signalingState, state, pcw.label + ": State is " + state + " in onsignalingstatechange");
pcw.signalingChangeEvent = true;
if (pcw.commandSuccess) {
test.next();
} else {
info("Waiting for success callback...");
}
};
}
/**
* Returns a function, suitable for use as a success callback, that
* checks the signaling state of the PC; and, if the signalingstatechange
* event has already fired, moves on to the next test case. This is
* intended to be used in conjunction with expectStateChange, above.
*
* @param {pcw} PeerConnectionWrapper
* The peer connection to expect a state change on
* @param {state} string
* The state that we expect to change to
* @param {test} PeerConnectionTest
* The test strucure currently in use.
*/
PeerConnectionTest.prototype.checkStateInCallback =
function PCT_checkStateInCallback(pcw, state, test) {
pcw.commandSuccess = false;
return function() {
pcw.commandSuccess = true;
is(pcw.signalingState, state, pcw.label + ": State is " + state + " in success callback");
if (pcw.signalingChangeEvent) {
test.next();
} else {
info("Waiting for signalingstatechange event...");
}
};
}
/**
* This class handles 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.constraints = [ ];
this.offerConstraints = {};
this.streams = [ ];
info("Creating new PeerConnectionWrapper: " + this.label);
this._pc = new mozRTCPeerConnection(this.configuration);
var self = this;
this._pc.onaddstream = function (event) {
// Bug 834835: Assume type is video until we get get{Audio,Video}Tracks.
self.attachMedia(event.stream, 'video', 'remote');
};
// Make sure no signaling state changes are fired until we expect them to
this._pc.onsignalingstatechange = unexpectedCallbackAndFinish(new Error);
}
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 remote signaling state.
*
* @returns {object} The local description
*/
get signalingState() {
return this._pc.signalingState;
},
/**
* 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);
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 = constraints;
if (constraints.audio) {
type = 'audio';
}
if (constraints.video) {
type += 'video';
}
self.attachMedia(stream, type, 'local');
_getAllUserMedia(constraintsList, index + 1);
}, unexpectedCallbackAndFinish(new Error));
} else {
onSuccess();
}
}
info("Get " + this.constraints.length + " local streams");
_getAllUserMedia(this.constraints, 0);
},
/**
* 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(new Error), 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('Got answer for ' + self.label + ': ' + JSON.stringify(answer));
self._last_answer = answer;
onSuccess(answer);
}, unexpectedCallbackAndFinish(new Error));
},
/**
* 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("Successfully set the local description for " + self.label);
onSuccess();
}, unexpectedCallbackAndFinish(new Error));
},
/**
* 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,
unexpectedSuccessCallbackAndFinish(new Error, "setLocalDescription should have failed."),
function (err) {
info("As expected, failed to set the local description for " + self.label);
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("Successfully set remote description for " + self.label);
onSuccess();
}, unexpectedCallbackAndFinish(new Error));
},
/**
* 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,
unexpectedSuccessCallbackAndFinish(new Error, "setRemoteDescription should have failed."),
function (err) {
info("As expected, failed to set the remote description for " + self.label);
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("Successfully added an ICE candidate to " + self.label);
onSuccess();
}, unexpectedCallbackAndFinish(new Error));
},
/**
* 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,
unexpectedSuccessCallbackAndFinish(new Error, "addIceCandidate should have failed."),
function (err) {
info("As expected, failed to add an ICE candidate to " + self.label);
onFailure(err);
}) ;
},
/**
* Checks that we are getting the media we expect.
*
* @param {object} constraintsRemote
* The media constraints of the remote peer connection object
*/
checkMedia : function PCW_checkMedia(constraintsRemote) {
is(this._pc.localStreams.length, this.constraints.length,
this.label + ' has ' + this.constraints.length + ' local streams');
// TODO: change this when multiple incoming streams are allowed.
is(this._pc.remoteStreams.length, 1,
this.label + ' has ' + 1 + ' remote streams');
},
/**
* 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.label + ": Closed connection.");
} catch (e) {
info(this.label + ": Failure in closing connection - " + e.message);
}
}
};