Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | "use strict"; |
michael@0 | 6 | |
michael@0 | 7 | |
michael@0 | 8 | /** |
michael@0 | 9 | * This class mimics a state machine and handles a list of commands by |
michael@0 | 10 | * executing them synchronously. |
michael@0 | 11 | * |
michael@0 | 12 | * @constructor |
michael@0 | 13 | * @param {object} framework |
michael@0 | 14 | * A back reference to the framework which makes use of the class. It's |
michael@0 | 15 | * getting passed in as parameter to each command callback. |
michael@0 | 16 | * @param {Array[]} [commandList=[]] |
michael@0 | 17 | * Default commands to set during initialization |
michael@0 | 18 | */ |
michael@0 | 19 | function CommandChain(framework, commandList) { |
michael@0 | 20 | this._framework = framework; |
michael@0 | 21 | |
michael@0 | 22 | this._commands = commandList || [ ]; |
michael@0 | 23 | this._current = 0; |
michael@0 | 24 | |
michael@0 | 25 | this.onFinished = null; |
michael@0 | 26 | } |
michael@0 | 27 | |
michael@0 | 28 | CommandChain.prototype = { |
michael@0 | 29 | |
michael@0 | 30 | /** |
michael@0 | 31 | * Returns the index of the current command of the chain |
michael@0 | 32 | * |
michael@0 | 33 | * @returns {number} Index of the current command |
michael@0 | 34 | */ |
michael@0 | 35 | get current() { |
michael@0 | 36 | return this._current; |
michael@0 | 37 | }, |
michael@0 | 38 | |
michael@0 | 39 | /** |
michael@0 | 40 | * Checks if the chain has already processed all the commands |
michael@0 | 41 | * |
michael@0 | 42 | * @returns {boolean} True, if all commands have been processed |
michael@0 | 43 | */ |
michael@0 | 44 | get finished() { |
michael@0 | 45 | return this._current === this._commands.length; |
michael@0 | 46 | }, |
michael@0 | 47 | |
michael@0 | 48 | /** |
michael@0 | 49 | * Returns the assigned commands of the chain. |
michael@0 | 50 | * |
michael@0 | 51 | * @returns {Array[]} Commands of the chain |
michael@0 | 52 | */ |
michael@0 | 53 | get commands() { |
michael@0 | 54 | return this._commands; |
michael@0 | 55 | }, |
michael@0 | 56 | |
michael@0 | 57 | /** |
michael@0 | 58 | * Sets new commands for the chain. All existing commands will be replaced. |
michael@0 | 59 | * |
michael@0 | 60 | * @param {Array[]} commands |
michael@0 | 61 | * List of commands |
michael@0 | 62 | */ |
michael@0 | 63 | set commands(commands) { |
michael@0 | 64 | this._commands = commands; |
michael@0 | 65 | }, |
michael@0 | 66 | |
michael@0 | 67 | /** |
michael@0 | 68 | * Execute the next command in the chain. |
michael@0 | 69 | */ |
michael@0 | 70 | executeNext : function () { |
michael@0 | 71 | var self = this; |
michael@0 | 72 | |
michael@0 | 73 | function _executeNext() { |
michael@0 | 74 | if (!self.finished) { |
michael@0 | 75 | var step = self._commands[self._current]; |
michael@0 | 76 | self._current++; |
michael@0 | 77 | |
michael@0 | 78 | self.currentStepLabel = step[0]; |
michael@0 | 79 | info("Run step: " + self.currentStepLabel); |
michael@0 | 80 | step[1](self._framework); // Execute step |
michael@0 | 81 | } |
michael@0 | 82 | else if (typeof(self.onFinished) === 'function') { |
michael@0 | 83 | self.onFinished(); |
michael@0 | 84 | } |
michael@0 | 85 | } |
michael@0 | 86 | |
michael@0 | 87 | // To prevent building up the stack we have to execute the next |
michael@0 | 88 | // step asynchronously |
michael@0 | 89 | window.setTimeout(_executeNext, 0); |
michael@0 | 90 | }, |
michael@0 | 91 | |
michael@0 | 92 | /** |
michael@0 | 93 | * Add new commands to the end of the chain |
michael@0 | 94 | * |
michael@0 | 95 | * @param {Array[]} commands |
michael@0 | 96 | * List of commands |
michael@0 | 97 | */ |
michael@0 | 98 | append: function (commands) { |
michael@0 | 99 | this._commands = this._commands.concat(commands); |
michael@0 | 100 | }, |
michael@0 | 101 | |
michael@0 | 102 | /** |
michael@0 | 103 | * Returns the index of the specified command in the chain. |
michael@0 | 104 | * |
michael@0 | 105 | * @param {string} id |
michael@0 | 106 | * Identifier of the command |
michael@0 | 107 | * @returns {number} Index of the command |
michael@0 | 108 | */ |
michael@0 | 109 | indexOf: function (id) { |
michael@0 | 110 | for (var i = 0; i < this._commands.length; i++) { |
michael@0 | 111 | if (this._commands[i][0] === id) { |
michael@0 | 112 | return i; |
michael@0 | 113 | } |
michael@0 | 114 | } |
michael@0 | 115 | |
michael@0 | 116 | return -1; |
michael@0 | 117 | }, |
michael@0 | 118 | |
michael@0 | 119 | /** |
michael@0 | 120 | * Inserts the new commands after the specified command. |
michael@0 | 121 | * |
michael@0 | 122 | * @param {string} id |
michael@0 | 123 | * Identifier of the command |
michael@0 | 124 | * @param {Array[]} commands |
michael@0 | 125 | * List of commands |
michael@0 | 126 | */ |
michael@0 | 127 | insertAfter: function (id, commands) { |
michael@0 | 128 | var index = this.indexOf(id); |
michael@0 | 129 | |
michael@0 | 130 | if (index > -1) { |
michael@0 | 131 | var tail = this.removeAfter(id); |
michael@0 | 132 | |
michael@0 | 133 | this.append(commands); |
michael@0 | 134 | this.append(tail); |
michael@0 | 135 | } |
michael@0 | 136 | }, |
michael@0 | 137 | |
michael@0 | 138 | /** |
michael@0 | 139 | * Inserts the new commands before the specified command. |
michael@0 | 140 | * |
michael@0 | 141 | * @param {string} id |
michael@0 | 142 | * Identifier of the command |
michael@0 | 143 | * @param {Array[]} commands |
michael@0 | 144 | * List of commands |
michael@0 | 145 | */ |
michael@0 | 146 | insertBefore: function (id, commands) { |
michael@0 | 147 | var index = this.indexOf(id); |
michael@0 | 148 | |
michael@0 | 149 | if (index > -1) { |
michael@0 | 150 | var tail = this.removeAfter(id); |
michael@0 | 151 | var object = this.remove(id); |
michael@0 | 152 | |
michael@0 | 153 | this.append(commands); |
michael@0 | 154 | this.append(object); |
michael@0 | 155 | this.append(tail); |
michael@0 | 156 | } |
michael@0 | 157 | }, |
michael@0 | 158 | |
michael@0 | 159 | /** |
michael@0 | 160 | * Removes the specified command |
michael@0 | 161 | * |
michael@0 | 162 | * @param {string} id |
michael@0 | 163 | * Identifier of the command |
michael@0 | 164 | * @returns {object[]} Removed command |
michael@0 | 165 | */ |
michael@0 | 166 | remove : function (id) { |
michael@0 | 167 | return this._commands.splice(this.indexOf(id), 1); |
michael@0 | 168 | }, |
michael@0 | 169 | |
michael@0 | 170 | /** |
michael@0 | 171 | * Removes all commands after the specified one. |
michael@0 | 172 | * |
michael@0 | 173 | * @param {string} id |
michael@0 | 174 | * Identifier of the command |
michael@0 | 175 | * @returns {object[]} Removed commands |
michael@0 | 176 | */ |
michael@0 | 177 | removeAfter : function (id) { |
michael@0 | 178 | var index = this.indexOf(id); |
michael@0 | 179 | |
michael@0 | 180 | if (index > -1) { |
michael@0 | 181 | return this._commands.splice(index + 1); |
michael@0 | 182 | } |
michael@0 | 183 | |
michael@0 | 184 | return null; |
michael@0 | 185 | }, |
michael@0 | 186 | |
michael@0 | 187 | /** |
michael@0 | 188 | * Removes all commands before the specified one. |
michael@0 | 189 | * |
michael@0 | 190 | * @param {string} id |
michael@0 | 191 | * Identifier of the command |
michael@0 | 192 | * @returns {object[]} Removed commands |
michael@0 | 193 | */ |
michael@0 | 194 | removeBefore : function (id) { |
michael@0 | 195 | var index = this.indexOf(id); |
michael@0 | 196 | |
michael@0 | 197 | if (index > -1) { |
michael@0 | 198 | return this._commands.splice(0, index); |
michael@0 | 199 | } |
michael@0 | 200 | |
michael@0 | 201 | return null; |
michael@0 | 202 | }, |
michael@0 | 203 | |
michael@0 | 204 | /** |
michael@0 | 205 | * Replaces all commands after the specified one. |
michael@0 | 206 | * |
michael@0 | 207 | * @param {string} id |
michael@0 | 208 | * Identifier of the command |
michael@0 | 209 | * @returns {object[]} Removed commands |
michael@0 | 210 | */ |
michael@0 | 211 | replaceAfter : function (id, commands) { |
michael@0 | 212 | var oldCommands = this.removeAfter(id); |
michael@0 | 213 | this.append(commands); |
michael@0 | 214 | |
michael@0 | 215 | return oldCommands; |
michael@0 | 216 | }, |
michael@0 | 217 | |
michael@0 | 218 | /** |
michael@0 | 219 | * Replaces all commands before the specified one. |
michael@0 | 220 | * |
michael@0 | 221 | * @param {string} id |
michael@0 | 222 | * Identifier of the command |
michael@0 | 223 | * @returns {object[]} Removed commands |
michael@0 | 224 | */ |
michael@0 | 225 | replaceBefore : function (id, commands) { |
michael@0 | 226 | var oldCommands = this.removeBefore(id); |
michael@0 | 227 | this.insertBefore(id, commands); |
michael@0 | 228 | |
michael@0 | 229 | return oldCommands; |
michael@0 | 230 | }, |
michael@0 | 231 | |
michael@0 | 232 | /** |
michael@0 | 233 | * Remove all commands whose identifiers match the specified regex. |
michael@0 | 234 | * |
michael@0 | 235 | * @param {regex} id_match |
michael@0 | 236 | * Regular expression to match command identifiers. |
michael@0 | 237 | */ |
michael@0 | 238 | filterOut : function (id_match) { |
michael@0 | 239 | for (var i = this._commands.length - 1; i >= 0; i--) { |
michael@0 | 240 | if (id_match.test(this._commands[i][0])) { |
michael@0 | 241 | this._commands.splice(i, 1); |
michael@0 | 242 | } |
michael@0 | 243 | } |
michael@0 | 244 | } |
michael@0 | 245 | }; |
michael@0 | 246 | |
michael@0 | 247 | /** |
michael@0 | 248 | * This class provides a state checker for media elements which store |
michael@0 | 249 | * a media stream to check for media attribute state and events fired. |
michael@0 | 250 | * When constructed by a caller, an object instance is created with |
michael@0 | 251 | * a media element, event state checkers for canplaythrough, timeupdate, and |
michael@0 | 252 | * time changing on the media element and stream. |
michael@0 | 253 | * |
michael@0 | 254 | * @param {HTMLMediaElement} element the media element being analyzed |
michael@0 | 255 | */ |
michael@0 | 256 | function MediaElementChecker(element) { |
michael@0 | 257 | this.element = element; |
michael@0 | 258 | this.canPlayThroughFired = false; |
michael@0 | 259 | this.timeUpdateFired = false; |
michael@0 | 260 | this.timePassed = false; |
michael@0 | 261 | |
michael@0 | 262 | var self = this; |
michael@0 | 263 | var elementId = self.element.getAttribute('id'); |
michael@0 | 264 | |
michael@0 | 265 | // When canplaythrough fires, we track that it's fired and remove the |
michael@0 | 266 | // event listener. |
michael@0 | 267 | var canPlayThroughCallback = function() { |
michael@0 | 268 | info('canplaythrough fired for media element ' + elementId); |
michael@0 | 269 | self.canPlayThroughFired = true; |
michael@0 | 270 | self.element.removeEventListener('canplaythrough', canPlayThroughCallback, |
michael@0 | 271 | false); |
michael@0 | 272 | }; |
michael@0 | 273 | |
michael@0 | 274 | // When timeupdate fires, we track that it's fired and check if time |
michael@0 | 275 | // has passed on the media stream and media element. |
michael@0 | 276 | var timeUpdateCallback = function() { |
michael@0 | 277 | self.timeUpdateFired = true; |
michael@0 | 278 | info('timeupdate fired for media element ' + elementId); |
michael@0 | 279 | |
michael@0 | 280 | // If time has passed, then track that and remove the timeupdate event |
michael@0 | 281 | // listener. |
michael@0 | 282 | if(element.mozSrcObject && element.mozSrcObject.currentTime > 0 && |
michael@0 | 283 | element.currentTime > 0) { |
michael@0 | 284 | info('time passed for media element ' + elementId); |
michael@0 | 285 | self.timePassed = true; |
michael@0 | 286 | self.element.removeEventListener('timeupdate', timeUpdateCallback, |
michael@0 | 287 | false); |
michael@0 | 288 | } |
michael@0 | 289 | }; |
michael@0 | 290 | |
michael@0 | 291 | element.addEventListener('canplaythrough', canPlayThroughCallback, false); |
michael@0 | 292 | element.addEventListener('timeupdate', timeUpdateCallback, false); |
michael@0 | 293 | } |
michael@0 | 294 | |
michael@0 | 295 | MediaElementChecker.prototype = { |
michael@0 | 296 | |
michael@0 | 297 | /** |
michael@0 | 298 | * Waits until the canplaythrough & timeupdate events to fire along with |
michael@0 | 299 | * ensuring time has passed on the stream and media element. |
michael@0 | 300 | * |
michael@0 | 301 | * @param {Function} onSuccess the success callback when media flow is |
michael@0 | 302 | * established |
michael@0 | 303 | */ |
michael@0 | 304 | waitForMediaFlow : function MEC_WaitForMediaFlow(onSuccess) { |
michael@0 | 305 | var self = this; |
michael@0 | 306 | var elementId = self.element.getAttribute('id'); |
michael@0 | 307 | info('Analyzing element: ' + elementId); |
michael@0 | 308 | |
michael@0 | 309 | if(self.canPlayThroughFired && self.timeUpdateFired && self.timePassed) { |
michael@0 | 310 | ok(true, 'Media flowing for ' + elementId); |
michael@0 | 311 | onSuccess(); |
michael@0 | 312 | } else { |
michael@0 | 313 | setTimeout(function() { |
michael@0 | 314 | self.waitForMediaFlow(onSuccess); |
michael@0 | 315 | }, 100); |
michael@0 | 316 | } |
michael@0 | 317 | }, |
michael@0 | 318 | |
michael@0 | 319 | /** |
michael@0 | 320 | * Checks if there is no media flow present by checking that the ready |
michael@0 | 321 | * state of the media element is HAVE_METADATA. |
michael@0 | 322 | */ |
michael@0 | 323 | checkForNoMediaFlow : function MEC_CheckForNoMediaFlow() { |
michael@0 | 324 | ok(this.element.readyState === HTMLMediaElement.HAVE_METADATA, |
michael@0 | 325 | 'Media element has a ready state of HAVE_METADATA'); |
michael@0 | 326 | } |
michael@0 | 327 | }; |
michael@0 | 328 | |
michael@0 | 329 | /** |
michael@0 | 330 | * Query function for determining if any IP address is available for |
michael@0 | 331 | * generating SDP. |
michael@0 | 332 | * |
michael@0 | 333 | * @return false if required additional network setup. |
michael@0 | 334 | */ |
michael@0 | 335 | function isNetworkReady() { |
michael@0 | 336 | // for gonk platform |
michael@0 | 337 | if ("nsINetworkInterfaceListService" in SpecialPowers.Ci) { |
michael@0 | 338 | var listService = SpecialPowers.Cc["@mozilla.org/network/interface-list-service;1"] |
michael@0 | 339 | .getService(SpecialPowers.Ci.nsINetworkInterfaceListService); |
michael@0 | 340 | var itfList = listService.getDataInterfaceList( |
michael@0 | 341 | SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_MMS_INTERFACES | |
michael@0 | 342 | SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_SUPL_INTERFACES | |
michael@0 | 343 | SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_IMS_INTERFACES | |
michael@0 | 344 | SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_DUN_INTERFACES); |
michael@0 | 345 | var num = itfList.getNumberOfInterface(); |
michael@0 | 346 | for (var i = 0; i < num; i++) { |
michael@0 | 347 | var ips = {}; |
michael@0 | 348 | var prefixLengths = {}; |
michael@0 | 349 | var length = itfList.getInterface(i).getAddresses(ips, prefixLengths); |
michael@0 | 350 | |
michael@0 | 351 | for (var j = 0; j < length; j++) { |
michael@0 | 352 | var ip = ips.value[j]; |
michael@0 | 353 | // skip IPv6 address |
michael@0 | 354 | if (ip.indexOf(":") < 0) { |
michael@0 | 355 | info("Network interface is ready with address: " + ip); |
michael@0 | 356 | return true; |
michael@0 | 357 | } |
michael@0 | 358 | } |
michael@0 | 359 | } |
michael@0 | 360 | // ip address is not available |
michael@0 | 361 | info("Network interface is not ready, required additional network setup"); |
michael@0 | 362 | return false; |
michael@0 | 363 | } |
michael@0 | 364 | info("Network setup is not required"); |
michael@0 | 365 | return true; |
michael@0 | 366 | } |
michael@0 | 367 | |
michael@0 | 368 | /** |
michael@0 | 369 | * Network setup utils for Gonk |
michael@0 | 370 | * |
michael@0 | 371 | * @return {object} providing functions for setup/teardown data connection |
michael@0 | 372 | */ |
michael@0 | 373 | function getNetworkUtils() { |
michael@0 | 374 | var url = SimpleTest.getTestFileURL("NetworkPreparationChromeScript.js"); |
michael@0 | 375 | var script = SpecialPowers.loadChromeScript(url); |
michael@0 | 376 | |
michael@0 | 377 | var utils = { |
michael@0 | 378 | /** |
michael@0 | 379 | * Utility for setting up data connection. |
michael@0 | 380 | * |
michael@0 | 381 | * @param aCallback callback after data connection is ready. |
michael@0 | 382 | */ |
michael@0 | 383 | prepareNetwork: function(aCallback) { |
michael@0 | 384 | script.addMessageListener('network-ready', function (message) { |
michael@0 | 385 | info("Network interface is ready"); |
michael@0 | 386 | aCallback(); |
michael@0 | 387 | }); |
michael@0 | 388 | info("Setup network interface"); |
michael@0 | 389 | script.sendAsyncMessage("prepare-network", true); |
michael@0 | 390 | }, |
michael@0 | 391 | /** |
michael@0 | 392 | * Utility for tearing down data connection. |
michael@0 | 393 | * |
michael@0 | 394 | * @param aCallback callback after data connection is closed. |
michael@0 | 395 | */ |
michael@0 | 396 | tearDownNetwork: function(aCallback) { |
michael@0 | 397 | script.addMessageListener('network-disabled', function (message) { |
michael@0 | 398 | ok(true, 'network-disabled'); |
michael@0 | 399 | script.destroy(); |
michael@0 | 400 | aCallback(); |
michael@0 | 401 | }); |
michael@0 | 402 | script.sendAsyncMessage("network-cleanup", true); |
michael@0 | 403 | } |
michael@0 | 404 | }; |
michael@0 | 405 | |
michael@0 | 406 | return utils; |
michael@0 | 407 | } |
michael@0 | 408 | |
michael@0 | 409 | /** |
michael@0 | 410 | * This class handles tests for peer connections. |
michael@0 | 411 | * |
michael@0 | 412 | * @constructor |
michael@0 | 413 | * @param {object} [options={}] |
michael@0 | 414 | * Optional options for the peer connection test |
michael@0 | 415 | * @param {object} [options.commands=commandsPeerConnection] |
michael@0 | 416 | * Commands to run for the test |
michael@0 | 417 | * @param {bool} [options.is_local=true] |
michael@0 | 418 | * true if this test should run the tests for the "local" side. |
michael@0 | 419 | * @param {bool} [options.is_remote=true] |
michael@0 | 420 | * true if this test should run the tests for the "remote" side. |
michael@0 | 421 | * @param {object} [options.config_pc1=undefined] |
michael@0 | 422 | * Configuration for the local peer connection instance |
michael@0 | 423 | * @param {object} [options.config_pc2=undefined] |
michael@0 | 424 | * Configuration for the remote peer connection instance. If not defined |
michael@0 | 425 | * the configuration from the local instance will be used |
michael@0 | 426 | */ |
michael@0 | 427 | function PeerConnectionTest(options) { |
michael@0 | 428 | // If no options are specified make it an empty object |
michael@0 | 429 | options = options || { }; |
michael@0 | 430 | options.commands = options.commands || commandsPeerConnection; |
michael@0 | 431 | options.is_local = "is_local" in options ? options.is_local : true; |
michael@0 | 432 | options.is_remote = "is_remote" in options ? options.is_remote : true; |
michael@0 | 433 | |
michael@0 | 434 | var netTeardownCommand = null; |
michael@0 | 435 | if (!isNetworkReady()) { |
michael@0 | 436 | var utils = getNetworkUtils(); |
michael@0 | 437 | // Trigger network setup to obtain IP address before creating any PeerConnection. |
michael@0 | 438 | utils.prepareNetwork(function() { |
michael@0 | 439 | ok(isNetworkReady(),'setup network connection successfully'); |
michael@0 | 440 | }); |
michael@0 | 441 | |
michael@0 | 442 | netTeardownCommand = [ |
michael@0 | 443 | [ |
michael@0 | 444 | 'TEARDOWN_NETWORK', |
michael@0 | 445 | function(test) { |
michael@0 | 446 | utils.tearDownNetwork(function() { |
michael@0 | 447 | info('teardown network connection'); |
michael@0 | 448 | test.next(); |
michael@0 | 449 | }); |
michael@0 | 450 | } |
michael@0 | 451 | ] |
michael@0 | 452 | ]; |
michael@0 | 453 | } |
michael@0 | 454 | |
michael@0 | 455 | if (options.is_local) |
michael@0 | 456 | this.pcLocal = new PeerConnectionWrapper('pcLocal', options.config_pc1); |
michael@0 | 457 | else |
michael@0 | 458 | this.pcLocal = null; |
michael@0 | 459 | |
michael@0 | 460 | if (options.is_remote) |
michael@0 | 461 | this.pcRemote = new PeerConnectionWrapper('pcRemote', options.config_pc2 || options.config_pc1); |
michael@0 | 462 | else |
michael@0 | 463 | this.pcRemote = null; |
michael@0 | 464 | |
michael@0 | 465 | this.connected = false; |
michael@0 | 466 | |
michael@0 | 467 | // Create command chain instance and assign default commands |
michael@0 | 468 | this.chain = new CommandChain(this, options.commands); |
michael@0 | 469 | if (!options.is_local) { |
michael@0 | 470 | this.chain.filterOut(/^PC_LOCAL/); |
michael@0 | 471 | } |
michael@0 | 472 | if (!options.is_remote) { |
michael@0 | 473 | this.chain.filterOut(/^PC_REMOTE/); |
michael@0 | 474 | } |
michael@0 | 475 | |
michael@0 | 476 | // Insert network teardown after testcase execution. |
michael@0 | 477 | if (netTeardownCommand) { |
michael@0 | 478 | this.chain.append(netTeardownCommand); |
michael@0 | 479 | } |
michael@0 | 480 | |
michael@0 | 481 | var self = this; |
michael@0 | 482 | this.chain.onFinished = function () { |
michael@0 | 483 | self.teardown(); |
michael@0 | 484 | }; |
michael@0 | 485 | } |
michael@0 | 486 | |
michael@0 | 487 | /** |
michael@0 | 488 | * Closes the peer connection if it is active |
michael@0 | 489 | * |
michael@0 | 490 | * @param {Function} onSuccess |
michael@0 | 491 | * Callback to execute when the peer connection has been closed successfully |
michael@0 | 492 | */ |
michael@0 | 493 | PeerConnectionTest.prototype.close = function PCT_close(onSuccess) { |
michael@0 | 494 | info("Closing peer connections. Connection state=" + this.connected); |
michael@0 | 495 | |
michael@0 | 496 | function signalingstatechangeClose(state) { |
michael@0 | 497 | info("'onsignalingstatechange' event '" + state + "' received"); |
michael@0 | 498 | is(state, "closed", "onsignalingstatechange event is closed"); |
michael@0 | 499 | } |
michael@0 | 500 | |
michael@0 | 501 | // There is no onclose event for the remote peer existent yet. So close it |
michael@0 | 502 | // side-by-side with the local peer. |
michael@0 | 503 | if (this.pcLocal) { |
michael@0 | 504 | this.pcLocal.onsignalingstatechange = signalingstatechangeClose; |
michael@0 | 505 | this.pcLocal.close(); |
michael@0 | 506 | } |
michael@0 | 507 | if (this.pcRemote) { |
michael@0 | 508 | this.pcRemote.onsignalingstatechange = signalingstatechangeClose; |
michael@0 | 509 | this.pcRemote.close(); |
michael@0 | 510 | } |
michael@0 | 511 | this.connected = false; |
michael@0 | 512 | |
michael@0 | 513 | onSuccess(); |
michael@0 | 514 | }; |
michael@0 | 515 | |
michael@0 | 516 | /** |
michael@0 | 517 | * Executes the next command. |
michael@0 | 518 | */ |
michael@0 | 519 | PeerConnectionTest.prototype.next = function PCT_next() { |
michael@0 | 520 | if (this._stepTimeout) { |
michael@0 | 521 | clearTimeout(this._stepTimeout); |
michael@0 | 522 | this._stepTimeout = null; |
michael@0 | 523 | } |
michael@0 | 524 | this.chain.executeNext(); |
michael@0 | 525 | }; |
michael@0 | 526 | |
michael@0 | 527 | /** |
michael@0 | 528 | * Set a timeout for the current step. |
michael@0 | 529 | * @param {long] ms the number of milliseconds to allow for this step |
michael@0 | 530 | */ |
michael@0 | 531 | PeerConnectionTest.prototype.setStepTimeout = function(ms) { |
michael@0 | 532 | this._stepTimeout = setTimeout(function() { |
michael@0 | 533 | ok(false, "Step timed out: " + this.chain.currentStepLabel); |
michael@0 | 534 | this.next(); |
michael@0 | 535 | }.bind(this), ms); |
michael@0 | 536 | }; |
michael@0 | 537 | |
michael@0 | 538 | /** |
michael@0 | 539 | * Creates an answer for the specified peer connection instance |
michael@0 | 540 | * and automatically handles the failure case. |
michael@0 | 541 | * |
michael@0 | 542 | * @param {PeerConnectionWrapper} peer |
michael@0 | 543 | * The peer connection wrapper to run the command on |
michael@0 | 544 | * @param {function} onSuccess |
michael@0 | 545 | * Callback to execute if the offer was created successfully |
michael@0 | 546 | */ |
michael@0 | 547 | PeerConnectionTest.prototype.createAnswer = |
michael@0 | 548 | function PCT_createAnswer(peer, onSuccess) { |
michael@0 | 549 | peer.createAnswer(function (answer) { |
michael@0 | 550 | onSuccess(answer); |
michael@0 | 551 | }); |
michael@0 | 552 | }; |
michael@0 | 553 | |
michael@0 | 554 | /** |
michael@0 | 555 | * Creates an offer for the specified peer connection instance |
michael@0 | 556 | * and automatically handles the failure case. |
michael@0 | 557 | * |
michael@0 | 558 | * @param {PeerConnectionWrapper} peer |
michael@0 | 559 | * The peer connection wrapper to run the command on |
michael@0 | 560 | * @param {function} onSuccess |
michael@0 | 561 | * Callback to execute if the offer was created successfully |
michael@0 | 562 | */ |
michael@0 | 563 | PeerConnectionTest.prototype.createOffer = |
michael@0 | 564 | function PCT_createOffer(peer, onSuccess) { |
michael@0 | 565 | peer.createOffer(function (offer) { |
michael@0 | 566 | onSuccess(offer); |
michael@0 | 567 | }); |
michael@0 | 568 | }; |
michael@0 | 569 | |
michael@0 | 570 | PeerConnectionTest.prototype.setIdentityProvider = |
michael@0 | 571 | function(peer, provider, protocol, identity) { |
michael@0 | 572 | peer.setIdentityProvider(provider, protocol, identity); |
michael@0 | 573 | }; |
michael@0 | 574 | |
michael@0 | 575 | /** |
michael@0 | 576 | * Sets the local description for the specified peer connection instance |
michael@0 | 577 | * and automatically handles the failure case. |
michael@0 | 578 | * |
michael@0 | 579 | * @param {PeerConnectionWrapper} peer |
michael@0 | 580 | The peer connection wrapper to run the command on |
michael@0 | 581 | * @param {mozRTCSessionDescription} desc |
michael@0 | 582 | * Session description for the local description request |
michael@0 | 583 | * @param {function} onSuccess |
michael@0 | 584 | * Callback to execute if the local description was set successfully |
michael@0 | 585 | */ |
michael@0 | 586 | PeerConnectionTest.prototype.setLocalDescription = |
michael@0 | 587 | function PCT_setLocalDescription(peer, desc, stateExpected, onSuccess) { |
michael@0 | 588 | var eventFired = false; |
michael@0 | 589 | var stateChanged = false; |
michael@0 | 590 | |
michael@0 | 591 | function check_next_test() { |
michael@0 | 592 | if (eventFired && stateChanged) { |
michael@0 | 593 | onSuccess(); |
michael@0 | 594 | } |
michael@0 | 595 | } |
michael@0 | 596 | |
michael@0 | 597 | peer.onsignalingstatechange = function (state) { |
michael@0 | 598 | //info(peer + ": 'onsignalingstatechange' event registered, signalingState: " + peer.signalingState); |
michael@0 | 599 | info(peer + ": 'onsignalingstatechange' event '" + state + "' received"); |
michael@0 | 600 | if(stateExpected === state && eventFired == false) { |
michael@0 | 601 | eventFired = true; |
michael@0 | 602 | check_next_test(); |
michael@0 | 603 | } else { |
michael@0 | 604 | ok(false, "This event has either already fired or there has been a " + |
michael@0 | 605 | "mismatch between event received " + state + |
michael@0 | 606 | " and event expected " + stateExpected); |
michael@0 | 607 | } |
michael@0 | 608 | }; |
michael@0 | 609 | |
michael@0 | 610 | peer.setLocalDescription(desc, function () { |
michael@0 | 611 | stateChanged = true; |
michael@0 | 612 | check_next_test(); |
michael@0 | 613 | }); |
michael@0 | 614 | }; |
michael@0 | 615 | |
michael@0 | 616 | /** |
michael@0 | 617 | * Sets the media constraints for both peer connection instances. |
michael@0 | 618 | * |
michael@0 | 619 | * @param {object} constraintsLocal |
michael@0 | 620 | * Media constrains for the local peer connection instance |
michael@0 | 621 | * @param constraintsRemote |
michael@0 | 622 | */ |
michael@0 | 623 | PeerConnectionTest.prototype.setMediaConstraints = |
michael@0 | 624 | function PCT_setMediaConstraints(constraintsLocal, constraintsRemote) { |
michael@0 | 625 | if (this.pcLocal) |
michael@0 | 626 | this.pcLocal.constraints = constraintsLocal; |
michael@0 | 627 | if (this.pcRemote) |
michael@0 | 628 | this.pcRemote.constraints = constraintsRemote; |
michael@0 | 629 | }; |
michael@0 | 630 | |
michael@0 | 631 | /** |
michael@0 | 632 | * Sets the media constraints used on a createOffer call in the test. |
michael@0 | 633 | * |
michael@0 | 634 | * @param {object} constraints the media constraints to use on createOffer |
michael@0 | 635 | */ |
michael@0 | 636 | PeerConnectionTest.prototype.setOfferConstraints = |
michael@0 | 637 | function PCT_setOfferConstraints(constraints) { |
michael@0 | 638 | if (this.pcLocal) |
michael@0 | 639 | this.pcLocal.offerConstraints = constraints; |
michael@0 | 640 | }; |
michael@0 | 641 | |
michael@0 | 642 | /** |
michael@0 | 643 | * Sets the remote description for the specified peer connection instance |
michael@0 | 644 | * and automatically handles the failure case. |
michael@0 | 645 | * |
michael@0 | 646 | * @param {PeerConnectionWrapper} peer |
michael@0 | 647 | The peer connection wrapper to run the command on |
michael@0 | 648 | * @param {mozRTCSessionDescription} desc |
michael@0 | 649 | * Session description for the remote description request |
michael@0 | 650 | * @param {function} onSuccess |
michael@0 | 651 | * Callback to execute if the local description was set successfully |
michael@0 | 652 | */ |
michael@0 | 653 | PeerConnectionTest.prototype.setRemoteDescription = |
michael@0 | 654 | function PCT_setRemoteDescription(peer, desc, stateExpected, onSuccess) { |
michael@0 | 655 | var eventFired = false; |
michael@0 | 656 | var stateChanged = false; |
michael@0 | 657 | |
michael@0 | 658 | function check_next_test() { |
michael@0 | 659 | if (eventFired && stateChanged) { |
michael@0 | 660 | onSuccess(); |
michael@0 | 661 | } |
michael@0 | 662 | } |
michael@0 | 663 | |
michael@0 | 664 | peer.onsignalingstatechange = function (state) { |
michael@0 | 665 | info(peer + ": 'onsignalingstatechange' event '" + state + "' received"); |
michael@0 | 666 | if(stateExpected === state && eventFired == false) { |
michael@0 | 667 | eventFired = true; |
michael@0 | 668 | check_next_test(); |
michael@0 | 669 | } else { |
michael@0 | 670 | ok(false, "This event has either already fired or there has been a " + |
michael@0 | 671 | "mismatch between event received " + state + |
michael@0 | 672 | " and event expected " + stateExpected); |
michael@0 | 673 | } |
michael@0 | 674 | }; |
michael@0 | 675 | |
michael@0 | 676 | peer.setRemoteDescription(desc, function () { |
michael@0 | 677 | stateChanged = true; |
michael@0 | 678 | check_next_test(); |
michael@0 | 679 | }); |
michael@0 | 680 | }; |
michael@0 | 681 | |
michael@0 | 682 | /** |
michael@0 | 683 | * Start running the tests as assigned to the command chain. |
michael@0 | 684 | */ |
michael@0 | 685 | PeerConnectionTest.prototype.run = function PCT_run() { |
michael@0 | 686 | this.next(); |
michael@0 | 687 | }; |
michael@0 | 688 | |
michael@0 | 689 | /** |
michael@0 | 690 | * Clean up the objects used by the test |
michael@0 | 691 | */ |
michael@0 | 692 | PeerConnectionTest.prototype.teardown = function PCT_teardown() { |
michael@0 | 693 | this.close(function () { |
michael@0 | 694 | info("Test finished"); |
michael@0 | 695 | if (window.SimpleTest) |
michael@0 | 696 | SimpleTest.finish(); |
michael@0 | 697 | else |
michael@0 | 698 | finish(); |
michael@0 | 699 | }); |
michael@0 | 700 | }; |
michael@0 | 701 | |
michael@0 | 702 | /** |
michael@0 | 703 | * This class handles tests for data channels. |
michael@0 | 704 | * |
michael@0 | 705 | * @constructor |
michael@0 | 706 | * @param {object} [options={}] |
michael@0 | 707 | * Optional options for the peer connection test |
michael@0 | 708 | * @param {object} [options.commands=commandsDataChannel] |
michael@0 | 709 | * Commands to run for the test |
michael@0 | 710 | * @param {object} [options.config_pc1=undefined] |
michael@0 | 711 | * Configuration for the local peer connection instance |
michael@0 | 712 | * @param {object} [options.config_pc2=undefined] |
michael@0 | 713 | * Configuration for the remote peer connection instance. If not defined |
michael@0 | 714 | * the configuration from the local instance will be used |
michael@0 | 715 | */ |
michael@0 | 716 | function DataChannelTest(options) { |
michael@0 | 717 | options = options || { }; |
michael@0 | 718 | options.commands = options.commands || commandsDataChannel; |
michael@0 | 719 | |
michael@0 | 720 | PeerConnectionTest.call(this, options); |
michael@0 | 721 | } |
michael@0 | 722 | |
michael@0 | 723 | DataChannelTest.prototype = Object.create(PeerConnectionTest.prototype, { |
michael@0 | 724 | close : { |
michael@0 | 725 | /** |
michael@0 | 726 | * Close the open data channels, followed by the underlying peer connection |
michael@0 | 727 | * |
michael@0 | 728 | * @param {Function} onSuccess |
michael@0 | 729 | * Callback to execute when the connection has been closed |
michael@0 | 730 | */ |
michael@0 | 731 | value : function DCT_close(onSuccess) { |
michael@0 | 732 | var self = this; |
michael@0 | 733 | |
michael@0 | 734 | function _closeChannels() { |
michael@0 | 735 | var length = self.pcLocal.dataChannels.length; |
michael@0 | 736 | |
michael@0 | 737 | if (length > 0) { |
michael@0 | 738 | self.closeDataChannel(length - 1, function () { |
michael@0 | 739 | _closeChannels(); |
michael@0 | 740 | }); |
michael@0 | 741 | } |
michael@0 | 742 | else { |
michael@0 | 743 | PeerConnectionTest.prototype.close.call(self, onSuccess); |
michael@0 | 744 | } |
michael@0 | 745 | } |
michael@0 | 746 | |
michael@0 | 747 | _closeChannels(); |
michael@0 | 748 | } |
michael@0 | 749 | }, |
michael@0 | 750 | |
michael@0 | 751 | closeDataChannel : { |
michael@0 | 752 | /** |
michael@0 | 753 | * Close the specified data channel |
michael@0 | 754 | * |
michael@0 | 755 | * @param {Number} index |
michael@0 | 756 | * Index of the data channel to close on both sides |
michael@0 | 757 | * @param {Function} onSuccess |
michael@0 | 758 | * Callback to execute when the data channel has been closed |
michael@0 | 759 | */ |
michael@0 | 760 | value : function DCT_closeDataChannel(index, onSuccess) { |
michael@0 | 761 | var localChannel = this.pcLocal.dataChannels[index]; |
michael@0 | 762 | var remoteChannel = this.pcRemote.dataChannels[index]; |
michael@0 | 763 | |
michael@0 | 764 | var self = this; |
michael@0 | 765 | |
michael@0 | 766 | // Register handler for remote channel, cause we have to wait until |
michael@0 | 767 | // the current close operation has been finished. |
michael@0 | 768 | remoteChannel.onclose = function () { |
michael@0 | 769 | self.pcRemote.dataChannels.splice(index, 1); |
michael@0 | 770 | |
michael@0 | 771 | onSuccess(remoteChannel); |
michael@0 | 772 | }; |
michael@0 | 773 | |
michael@0 | 774 | localChannel.close(); |
michael@0 | 775 | this.pcLocal.dataChannels.splice(index, 1); |
michael@0 | 776 | } |
michael@0 | 777 | }, |
michael@0 | 778 | |
michael@0 | 779 | createDataChannel : { |
michael@0 | 780 | /** |
michael@0 | 781 | * Create a data channel |
michael@0 | 782 | * |
michael@0 | 783 | * @param {Dict} options |
michael@0 | 784 | * Options for the data channel (see nsIPeerConnection) |
michael@0 | 785 | * @param {Function} onSuccess |
michael@0 | 786 | * Callback when the creation was successful |
michael@0 | 787 | */ |
michael@0 | 788 | value : function DCT_createDataChannel(options, onSuccess) { |
michael@0 | 789 | var localChannel = null; |
michael@0 | 790 | var remoteChannel = null; |
michael@0 | 791 | var self = this; |
michael@0 | 792 | |
michael@0 | 793 | // Method to synchronize all asynchronous events. |
michael@0 | 794 | function check_next_test() { |
michael@0 | 795 | if (self.connected && localChannel && remoteChannel) { |
michael@0 | 796 | onSuccess(localChannel, remoteChannel); |
michael@0 | 797 | } |
michael@0 | 798 | } |
michael@0 | 799 | |
michael@0 | 800 | if (!options.negotiated) { |
michael@0 | 801 | // Register handlers for the remote peer |
michael@0 | 802 | this.pcRemote.registerDataChannelOpenEvents(function (channel) { |
michael@0 | 803 | remoteChannel = channel; |
michael@0 | 804 | check_next_test(); |
michael@0 | 805 | }); |
michael@0 | 806 | } |
michael@0 | 807 | |
michael@0 | 808 | // Create the datachannel and handle the local 'onopen' event |
michael@0 | 809 | this.pcLocal.createDataChannel(options, function (channel) { |
michael@0 | 810 | localChannel = channel; |
michael@0 | 811 | |
michael@0 | 812 | if (options.negotiated) { |
michael@0 | 813 | // externally negotiated - we need to open from both ends |
michael@0 | 814 | options.id = options.id || channel.id; // allow for no id to let the impl choose |
michael@0 | 815 | self.pcRemote.createDataChannel(options, function (channel) { |
michael@0 | 816 | remoteChannel = channel; |
michael@0 | 817 | check_next_test(); |
michael@0 | 818 | }); |
michael@0 | 819 | } else { |
michael@0 | 820 | check_next_test(); |
michael@0 | 821 | } |
michael@0 | 822 | }); |
michael@0 | 823 | } |
michael@0 | 824 | }, |
michael@0 | 825 | |
michael@0 | 826 | send : { |
michael@0 | 827 | /** |
michael@0 | 828 | * Send data (message or blob) to the other peer |
michael@0 | 829 | * |
michael@0 | 830 | * @param {String|Blob} data |
michael@0 | 831 | * Data to send to the other peer. For Blobs the MIME type will be lost. |
michael@0 | 832 | * @param {Function} onSuccess |
michael@0 | 833 | * Callback to execute when data has been sent |
michael@0 | 834 | * @param {Object} [options={ }] |
michael@0 | 835 | * Options to specify the data channels to be used |
michael@0 | 836 | * @param {DataChannelWrapper} [options.sourceChannel=pcLocal.dataChannels[length - 1]] |
michael@0 | 837 | * Data channel to use for sending the message |
michael@0 | 838 | * @param {DataChannelWrapper} [options.targetChannel=pcRemote.dataChannels[length - 1]] |
michael@0 | 839 | * Data channel to use for receiving the message |
michael@0 | 840 | */ |
michael@0 | 841 | value : function DCT_send(data, onSuccess, options) { |
michael@0 | 842 | options = options || { }; |
michael@0 | 843 | var source = options.sourceChannel || |
michael@0 | 844 | this.pcLocal.dataChannels[this.pcLocal.dataChannels.length - 1]; |
michael@0 | 845 | var target = options.targetChannel || |
michael@0 | 846 | this.pcRemote.dataChannels[this.pcRemote.dataChannels.length - 1]; |
michael@0 | 847 | |
michael@0 | 848 | // Register event handler for the target channel |
michael@0 | 849 | target.onmessage = function (recv_data) { |
michael@0 | 850 | onSuccess(target, recv_data); |
michael@0 | 851 | }; |
michael@0 | 852 | |
michael@0 | 853 | source.send(data); |
michael@0 | 854 | } |
michael@0 | 855 | }, |
michael@0 | 856 | |
michael@0 | 857 | setLocalDescription : { |
michael@0 | 858 | /** |
michael@0 | 859 | * Sets the local description for the specified peer connection instance |
michael@0 | 860 | * and automatically handles the failure case. In case for the final call |
michael@0 | 861 | * it will setup the requested datachannel. |
michael@0 | 862 | * |
michael@0 | 863 | * @param {PeerConnectionWrapper} peer |
michael@0 | 864 | The peer connection wrapper to run the command on |
michael@0 | 865 | * @param {mozRTCSessionDescription} desc |
michael@0 | 866 | * Session description for the local description request |
michael@0 | 867 | * @param {function} onSuccess |
michael@0 | 868 | * Callback to execute if the local description was set successfully |
michael@0 | 869 | */ |
michael@0 | 870 | value : function DCT_setLocalDescription(peer, desc, state, onSuccess) { |
michael@0 | 871 | // If the peer has a remote offer we are in the final call, and have |
michael@0 | 872 | // to wait for the datachannel connection to be open. It will also set |
michael@0 | 873 | // the local description internally. |
michael@0 | 874 | if (peer.signalingState === 'have-remote-offer') { |
michael@0 | 875 | this.waitForInitialDataChannel(peer, desc, state, onSuccess); |
michael@0 | 876 | } |
michael@0 | 877 | else { |
michael@0 | 878 | PeerConnectionTest.prototype.setLocalDescription.call(this, peer, |
michael@0 | 879 | desc, state, onSuccess); |
michael@0 | 880 | } |
michael@0 | 881 | |
michael@0 | 882 | } |
michael@0 | 883 | }, |
michael@0 | 884 | |
michael@0 | 885 | waitForInitialDataChannel : { |
michael@0 | 886 | /** |
michael@0 | 887 | * Create an initial data channel before the peer connection has been connected |
michael@0 | 888 | * |
michael@0 | 889 | * @param {PeerConnectionWrapper} peer |
michael@0 | 890 | The peer connection wrapper to run the command on |
michael@0 | 891 | * @param {mozRTCSessionDescription} desc |
michael@0 | 892 | * Session description for the local description request |
michael@0 | 893 | * @param {Function} onSuccess |
michael@0 | 894 | * Callback when the creation was successful |
michael@0 | 895 | */ |
michael@0 | 896 | value : function DCT_waitForInitialDataChannel(peer, desc, state, onSuccess) { |
michael@0 | 897 | var self = this; |
michael@0 | 898 | |
michael@0 | 899 | var targetPeer = peer; |
michael@0 | 900 | var targetChannel = null; |
michael@0 | 901 | |
michael@0 | 902 | var sourcePeer = (peer == this.pcLocal) ? this.pcRemote : this.pcLocal; |
michael@0 | 903 | var sourceChannel = null; |
michael@0 | 904 | |
michael@0 | 905 | // Method to synchronize all asynchronous events which current happen |
michael@0 | 906 | // due to a non-predictable flow. With bug 875346 fixed we will be able |
michael@0 | 907 | // to simplify this code. |
michael@0 | 908 | function check_next_test() { |
michael@0 | 909 | if (self.connected && sourceChannel && targetChannel) { |
michael@0 | 910 | onSuccess(sourceChannel, targetChannel); |
michael@0 | 911 | } |
michael@0 | 912 | } |
michael@0 | 913 | |
michael@0 | 914 | // Register 'onopen' handler for the first local data channel |
michael@0 | 915 | sourcePeer.dataChannels[0].onopen = function (channel) { |
michael@0 | 916 | sourceChannel = channel; |
michael@0 | 917 | check_next_test(); |
michael@0 | 918 | }; |
michael@0 | 919 | |
michael@0 | 920 | // Register handlers for the target peer |
michael@0 | 921 | targetPeer.registerDataChannelOpenEvents(function (channel) { |
michael@0 | 922 | targetChannel = channel; |
michael@0 | 923 | check_next_test(); |
michael@0 | 924 | }); |
michael@0 | 925 | |
michael@0 | 926 | PeerConnectionTest.prototype.setLocalDescription.call(this, targetPeer, desc, |
michael@0 | 927 | state, |
michael@0 | 928 | function () { |
michael@0 | 929 | self.connected = true; |
michael@0 | 930 | check_next_test(); |
michael@0 | 931 | } |
michael@0 | 932 | ); |
michael@0 | 933 | } |
michael@0 | 934 | } |
michael@0 | 935 | }); |
michael@0 | 936 | |
michael@0 | 937 | /** |
michael@0 | 938 | * This class acts as a wrapper around a DataChannel instance. |
michael@0 | 939 | * |
michael@0 | 940 | * @param dataChannel |
michael@0 | 941 | * @param peerConnectionWrapper |
michael@0 | 942 | * @constructor |
michael@0 | 943 | */ |
michael@0 | 944 | function DataChannelWrapper(dataChannel, peerConnectionWrapper) { |
michael@0 | 945 | this._channel = dataChannel; |
michael@0 | 946 | this._pc = peerConnectionWrapper; |
michael@0 | 947 | |
michael@0 | 948 | info("Creating " + this); |
michael@0 | 949 | |
michael@0 | 950 | /** |
michael@0 | 951 | * Setup appropriate callbacks |
michael@0 | 952 | */ |
michael@0 | 953 | |
michael@0 | 954 | this.onclose = unexpectedEventAndFinish(this, 'onclose'); |
michael@0 | 955 | this.onerror = unexpectedEventAndFinish(this, 'onerror'); |
michael@0 | 956 | this.onmessage = unexpectedEventAndFinish(this, 'onmessage'); |
michael@0 | 957 | this.onopen = unexpectedEventAndFinish(this, 'onopen'); |
michael@0 | 958 | |
michael@0 | 959 | var self = this; |
michael@0 | 960 | |
michael@0 | 961 | /** |
michael@0 | 962 | * Callback for native data channel 'onclose' events. If no custom handler |
michael@0 | 963 | * has been specified via 'this.onclose', a failure will be raised if an |
michael@0 | 964 | * event of this type gets caught. |
michael@0 | 965 | */ |
michael@0 | 966 | this._channel.onclose = function () { |
michael@0 | 967 | info(self + ": 'onclose' event fired"); |
michael@0 | 968 | |
michael@0 | 969 | self.onclose(self); |
michael@0 | 970 | self.onclose = unexpectedEventAndFinish(self, 'onclose'); |
michael@0 | 971 | }; |
michael@0 | 972 | |
michael@0 | 973 | /** |
michael@0 | 974 | * Callback for native data channel 'onmessage' events. If no custom handler |
michael@0 | 975 | * has been specified via 'this.onmessage', a failure will be raised if an |
michael@0 | 976 | * event of this type gets caught. |
michael@0 | 977 | * |
michael@0 | 978 | * @param {Object} event |
michael@0 | 979 | * Event data which includes the sent message |
michael@0 | 980 | */ |
michael@0 | 981 | this._channel.onmessage = function (event) { |
michael@0 | 982 | info(self + ": 'onmessage' event fired for '" + event.data + "'"); |
michael@0 | 983 | |
michael@0 | 984 | self.onmessage(event.data); |
michael@0 | 985 | self.onmessage = unexpectedEventAndFinish(self, 'onmessage'); |
michael@0 | 986 | }; |
michael@0 | 987 | |
michael@0 | 988 | /** |
michael@0 | 989 | * Callback for native data channel 'onopen' events. If no custom handler |
michael@0 | 990 | * has been specified via 'this.onopen', a failure will be raised if an |
michael@0 | 991 | * event of this type gets caught. |
michael@0 | 992 | */ |
michael@0 | 993 | this._channel.onopen = function () { |
michael@0 | 994 | info(self + ": 'onopen' event fired"); |
michael@0 | 995 | |
michael@0 | 996 | self.onopen(self); |
michael@0 | 997 | self.onopen = unexpectedEventAndFinish(self, 'onopen'); |
michael@0 | 998 | }; |
michael@0 | 999 | } |
michael@0 | 1000 | |
michael@0 | 1001 | DataChannelWrapper.prototype = { |
michael@0 | 1002 | /** |
michael@0 | 1003 | * Returns the binary type of the channel |
michael@0 | 1004 | * |
michael@0 | 1005 | * @returns {String} The binary type |
michael@0 | 1006 | */ |
michael@0 | 1007 | get binaryType() { |
michael@0 | 1008 | return this._channel.binaryType; |
michael@0 | 1009 | }, |
michael@0 | 1010 | |
michael@0 | 1011 | /** |
michael@0 | 1012 | * Sets the binary type of the channel |
michael@0 | 1013 | * |
michael@0 | 1014 | * @param {String} type |
michael@0 | 1015 | * The new binary type of the channel |
michael@0 | 1016 | */ |
michael@0 | 1017 | set binaryType(type) { |
michael@0 | 1018 | this._channel.binaryType = type; |
michael@0 | 1019 | }, |
michael@0 | 1020 | |
michael@0 | 1021 | /** |
michael@0 | 1022 | * Returns the label of the underlying data channel |
michael@0 | 1023 | * |
michael@0 | 1024 | * @returns {String} The label |
michael@0 | 1025 | */ |
michael@0 | 1026 | get label() { |
michael@0 | 1027 | return this._channel.label; |
michael@0 | 1028 | }, |
michael@0 | 1029 | |
michael@0 | 1030 | /** |
michael@0 | 1031 | * Returns the protocol of the underlying data channel |
michael@0 | 1032 | * |
michael@0 | 1033 | * @returns {String} The protocol |
michael@0 | 1034 | */ |
michael@0 | 1035 | get protocol() { |
michael@0 | 1036 | return this._channel.protocol; |
michael@0 | 1037 | }, |
michael@0 | 1038 | |
michael@0 | 1039 | /** |
michael@0 | 1040 | * Returns the id of the underlying data channel |
michael@0 | 1041 | * |
michael@0 | 1042 | * @returns {number} The stream id |
michael@0 | 1043 | */ |
michael@0 | 1044 | get id() { |
michael@0 | 1045 | return this._channel.id; |
michael@0 | 1046 | }, |
michael@0 | 1047 | |
michael@0 | 1048 | /** |
michael@0 | 1049 | * Returns the reliable state of the underlying data channel |
michael@0 | 1050 | * |
michael@0 | 1051 | * @returns {bool} The stream's reliable state |
michael@0 | 1052 | */ |
michael@0 | 1053 | get reliable() { |
michael@0 | 1054 | return this._channel.reliable; |
michael@0 | 1055 | }, |
michael@0 | 1056 | |
michael@0 | 1057 | // ordered, maxRetransmits and maxRetransmitTime not exposed yet |
michael@0 | 1058 | |
michael@0 | 1059 | /** |
michael@0 | 1060 | * Returns the readyState bit of the data channel |
michael@0 | 1061 | * |
michael@0 | 1062 | * @returns {String} The state of the channel |
michael@0 | 1063 | */ |
michael@0 | 1064 | get readyState() { |
michael@0 | 1065 | return this._channel.readyState; |
michael@0 | 1066 | }, |
michael@0 | 1067 | |
michael@0 | 1068 | /** |
michael@0 | 1069 | * Close the data channel |
michael@0 | 1070 | */ |
michael@0 | 1071 | close : function () { |
michael@0 | 1072 | info(this + ": Closing channel"); |
michael@0 | 1073 | this._channel.close(); |
michael@0 | 1074 | }, |
michael@0 | 1075 | |
michael@0 | 1076 | /** |
michael@0 | 1077 | * Send data through the data channel |
michael@0 | 1078 | * |
michael@0 | 1079 | * @param {String|Object} data |
michael@0 | 1080 | * Data which has to be sent through the data channel |
michael@0 | 1081 | */ |
michael@0 | 1082 | send: function DCW_send(data) { |
michael@0 | 1083 | info(this + ": Sending data '" + data + "'"); |
michael@0 | 1084 | this._channel.send(data); |
michael@0 | 1085 | }, |
michael@0 | 1086 | |
michael@0 | 1087 | /** |
michael@0 | 1088 | * Returns the string representation of the class |
michael@0 | 1089 | * |
michael@0 | 1090 | * @returns {String} The string representation |
michael@0 | 1091 | */ |
michael@0 | 1092 | toString: function DCW_toString() { |
michael@0 | 1093 | return "DataChannelWrapper (" + this._pc.label + '_' + this._channel.label + ")"; |
michael@0 | 1094 | } |
michael@0 | 1095 | }; |
michael@0 | 1096 | |
michael@0 | 1097 | |
michael@0 | 1098 | /** |
michael@0 | 1099 | * This class acts as a wrapper around a PeerConnection instance. |
michael@0 | 1100 | * |
michael@0 | 1101 | * @constructor |
michael@0 | 1102 | * @param {string} label |
michael@0 | 1103 | * Description for the peer connection instance |
michael@0 | 1104 | * @param {object} configuration |
michael@0 | 1105 | * Configuration for the peer connection instance |
michael@0 | 1106 | */ |
michael@0 | 1107 | function PeerConnectionWrapper(label, configuration) { |
michael@0 | 1108 | this.configuration = configuration; |
michael@0 | 1109 | this.label = label; |
michael@0 | 1110 | this.whenCreated = Date.now(); |
michael@0 | 1111 | |
michael@0 | 1112 | this.constraints = [ ]; |
michael@0 | 1113 | this.offerConstraints = {}; |
michael@0 | 1114 | this.streams = [ ]; |
michael@0 | 1115 | this.mediaCheckers = [ ]; |
michael@0 | 1116 | |
michael@0 | 1117 | this.dataChannels = [ ]; |
michael@0 | 1118 | |
michael@0 | 1119 | info("Creating " + this); |
michael@0 | 1120 | this._pc = new mozRTCPeerConnection(this.configuration); |
michael@0 | 1121 | is(this._pc.iceConnectionState, "new", "iceConnectionState starts at 'new'"); |
michael@0 | 1122 | |
michael@0 | 1123 | /** |
michael@0 | 1124 | * Setup callback handlers |
michael@0 | 1125 | */ |
michael@0 | 1126 | var self = this; |
michael@0 | 1127 | // This enables tests to validate that the next ice state is the one they expect to happen |
michael@0 | 1128 | this.next_ice_state = ""; // in most cases, the next state will be "checking", but in some tests "closed" |
michael@0 | 1129 | // This allows test to register their own callbacks for ICE connection state changes |
michael@0 | 1130 | this.ice_connection_callbacks = {}; |
michael@0 | 1131 | |
michael@0 | 1132 | this._pc.oniceconnectionstatechange = function() { |
michael@0 | 1133 | ok(self._pc.iceConnectionState !== undefined, "iceConnectionState should not be undefined"); |
michael@0 | 1134 | info(self + ": oniceconnectionstatechange fired, new state is: " + self._pc.iceConnectionState); |
michael@0 | 1135 | Object.keys(self.ice_connection_callbacks).forEach(function(name) { |
michael@0 | 1136 | self.ice_connection_callbacks[name](); |
michael@0 | 1137 | }); |
michael@0 | 1138 | if (self.next_ice_state !== "") { |
michael@0 | 1139 | is(self._pc.iceConnectionState, self.next_ice_state, "iceConnectionState changed to '" + |
michael@0 | 1140 | self.next_ice_state + "'"); |
michael@0 | 1141 | self.next_ice_state = ""; |
michael@0 | 1142 | } |
michael@0 | 1143 | }; |
michael@0 | 1144 | this.ondatachannel = unexpectedEventAndFinish(this, 'ondatachannel'); |
michael@0 | 1145 | this.onsignalingstatechange = unexpectedEventAndFinish(this, 'onsignalingstatechange'); |
michael@0 | 1146 | |
michael@0 | 1147 | /** |
michael@0 | 1148 | * Callback for native peer connection 'onaddstream' events. |
michael@0 | 1149 | * |
michael@0 | 1150 | * @param {Object} event |
michael@0 | 1151 | * Event data which includes the stream to be added |
michael@0 | 1152 | */ |
michael@0 | 1153 | this._pc.onaddstream = function (event) { |
michael@0 | 1154 | info(self + ": 'onaddstream' event fired for " + JSON.stringify(event.stream)); |
michael@0 | 1155 | |
michael@0 | 1156 | var type = ''; |
michael@0 | 1157 | if (event.stream.getAudioTracks().length > 0) { |
michael@0 | 1158 | type = 'audio'; |
michael@0 | 1159 | } |
michael@0 | 1160 | if (event.stream.getVideoTracks().length > 0) { |
michael@0 | 1161 | type += 'video'; |
michael@0 | 1162 | } |
michael@0 | 1163 | self.attachMedia(event.stream, type, 'remote'); |
michael@0 | 1164 | }; |
michael@0 | 1165 | |
michael@0 | 1166 | /** |
michael@0 | 1167 | * Callback for native peer connection 'ondatachannel' events. If no custom handler |
michael@0 | 1168 | * has been specified via 'this.ondatachannel', a failure will be raised if an |
michael@0 | 1169 | * event of this type gets caught. |
michael@0 | 1170 | * |
michael@0 | 1171 | * @param {Object} event |
michael@0 | 1172 | * Event data which includes the newly created data channel |
michael@0 | 1173 | */ |
michael@0 | 1174 | this._pc.ondatachannel = function (event) { |
michael@0 | 1175 | info(self + ": 'ondatachannel' event fired for " + event.channel.label); |
michael@0 | 1176 | |
michael@0 | 1177 | self.ondatachannel(new DataChannelWrapper(event.channel, self)); |
michael@0 | 1178 | self.ondatachannel = unexpectedEventAndFinish(self, 'ondatachannel'); |
michael@0 | 1179 | }; |
michael@0 | 1180 | |
michael@0 | 1181 | /** |
michael@0 | 1182 | * Callback for native peer connection 'onsignalingstatechange' events. If no |
michael@0 | 1183 | * custom handler has been specified via 'this.onsignalingstatechange', a |
michael@0 | 1184 | * failure will be raised if an event of this type is caught. |
michael@0 | 1185 | * |
michael@0 | 1186 | * @param {Object} aEvent |
michael@0 | 1187 | * Event data which includes the newly created data channel |
michael@0 | 1188 | */ |
michael@0 | 1189 | this._pc.onsignalingstatechange = function (aEvent) { |
michael@0 | 1190 | info(self + ": 'onsignalingstatechange' event fired"); |
michael@0 | 1191 | |
michael@0 | 1192 | // this calls the eventhandler only once and then overwrites it with the |
michael@0 | 1193 | // default unexpectedEvent handler |
michael@0 | 1194 | self.onsignalingstatechange(aEvent); |
michael@0 | 1195 | self.onsignalingstatechange = unexpectedEventAndFinish(self, 'onsignalingstatechange'); |
michael@0 | 1196 | }; |
michael@0 | 1197 | } |
michael@0 | 1198 | |
michael@0 | 1199 | PeerConnectionWrapper.prototype = { |
michael@0 | 1200 | |
michael@0 | 1201 | /** |
michael@0 | 1202 | * Returns the local description. |
michael@0 | 1203 | * |
michael@0 | 1204 | * @returns {object} The local description |
michael@0 | 1205 | */ |
michael@0 | 1206 | get localDescription() { |
michael@0 | 1207 | return this._pc.localDescription; |
michael@0 | 1208 | }, |
michael@0 | 1209 | |
michael@0 | 1210 | /** |
michael@0 | 1211 | * Sets the local description. |
michael@0 | 1212 | * |
michael@0 | 1213 | * @param {object} desc |
michael@0 | 1214 | * The new local description |
michael@0 | 1215 | */ |
michael@0 | 1216 | set localDescription(desc) { |
michael@0 | 1217 | this._pc.localDescription = desc; |
michael@0 | 1218 | }, |
michael@0 | 1219 | |
michael@0 | 1220 | /** |
michael@0 | 1221 | * Returns the readyState. |
michael@0 | 1222 | * |
michael@0 | 1223 | * @returns {string} |
michael@0 | 1224 | */ |
michael@0 | 1225 | get readyState() { |
michael@0 | 1226 | return this._pc.readyState; |
michael@0 | 1227 | }, |
michael@0 | 1228 | |
michael@0 | 1229 | /** |
michael@0 | 1230 | * Returns the remote description. |
michael@0 | 1231 | * |
michael@0 | 1232 | * @returns {object} The remote description |
michael@0 | 1233 | */ |
michael@0 | 1234 | get remoteDescription() { |
michael@0 | 1235 | return this._pc.remoteDescription; |
michael@0 | 1236 | }, |
michael@0 | 1237 | |
michael@0 | 1238 | /** |
michael@0 | 1239 | * Sets the remote description. |
michael@0 | 1240 | * |
michael@0 | 1241 | * @param {object} desc |
michael@0 | 1242 | * The new remote description |
michael@0 | 1243 | */ |
michael@0 | 1244 | set remoteDescription(desc) { |
michael@0 | 1245 | this._pc.remoteDescription = desc; |
michael@0 | 1246 | }, |
michael@0 | 1247 | |
michael@0 | 1248 | /** |
michael@0 | 1249 | * Returns the signaling state. |
michael@0 | 1250 | * |
michael@0 | 1251 | * @returns {object} The local description |
michael@0 | 1252 | */ |
michael@0 | 1253 | get signalingState() { |
michael@0 | 1254 | return this._pc.signalingState; |
michael@0 | 1255 | }, |
michael@0 | 1256 | /** |
michael@0 | 1257 | * Returns the ICE connection state. |
michael@0 | 1258 | * |
michael@0 | 1259 | * @returns {object} The local description |
michael@0 | 1260 | */ |
michael@0 | 1261 | get iceConnectionState() { |
michael@0 | 1262 | return this._pc.iceConnectionState; |
michael@0 | 1263 | }, |
michael@0 | 1264 | |
michael@0 | 1265 | setIdentityProvider: function(provider, protocol, identity) { |
michael@0 | 1266 | this._pc.setIdentityProvider(provider, protocol, identity); |
michael@0 | 1267 | }, |
michael@0 | 1268 | |
michael@0 | 1269 | /** |
michael@0 | 1270 | * Callback when we get media from either side. Also an appropriate |
michael@0 | 1271 | * HTML media element will be created. |
michael@0 | 1272 | * |
michael@0 | 1273 | * @param {MediaStream} stream |
michael@0 | 1274 | * Media stream to handle |
michael@0 | 1275 | * @param {string} type |
michael@0 | 1276 | * The type of media stream ('audio' or 'video') |
michael@0 | 1277 | * @param {string} side |
michael@0 | 1278 | * The location the stream is coming from ('local' or 'remote') |
michael@0 | 1279 | */ |
michael@0 | 1280 | attachMedia : function PCW_attachMedia(stream, type, side) { |
michael@0 | 1281 | info("Got media stream: " + type + " (" + side + ")"); |
michael@0 | 1282 | this.streams.push(stream); |
michael@0 | 1283 | |
michael@0 | 1284 | if (side === 'local') { |
michael@0 | 1285 | this._pc.addStream(stream); |
michael@0 | 1286 | } |
michael@0 | 1287 | |
michael@0 | 1288 | var element = createMediaElement(type, this.label + '_' + side); |
michael@0 | 1289 | this.mediaCheckers.push(new MediaElementChecker(element)); |
michael@0 | 1290 | element.mozSrcObject = stream; |
michael@0 | 1291 | element.play(); |
michael@0 | 1292 | }, |
michael@0 | 1293 | |
michael@0 | 1294 | /** |
michael@0 | 1295 | * Requests all the media streams as specified in the constrains property. |
michael@0 | 1296 | * |
michael@0 | 1297 | * @param {function} onSuccess |
michael@0 | 1298 | * Callback to execute if all media has been requested successfully |
michael@0 | 1299 | */ |
michael@0 | 1300 | getAllUserMedia : function PCW_GetAllUserMedia(onSuccess) { |
michael@0 | 1301 | var self = this; |
michael@0 | 1302 | |
michael@0 | 1303 | function _getAllUserMedia(constraintsList, index) { |
michael@0 | 1304 | if (index < constraintsList.length) { |
michael@0 | 1305 | var constraints = constraintsList[index]; |
michael@0 | 1306 | |
michael@0 | 1307 | getUserMedia(constraints, function (stream) { |
michael@0 | 1308 | var type = ''; |
michael@0 | 1309 | |
michael@0 | 1310 | if (constraints.audio) { |
michael@0 | 1311 | type = 'audio'; |
michael@0 | 1312 | } |
michael@0 | 1313 | |
michael@0 | 1314 | if (constraints.video) { |
michael@0 | 1315 | type += 'video'; |
michael@0 | 1316 | } |
michael@0 | 1317 | |
michael@0 | 1318 | self.attachMedia(stream, type, 'local'); |
michael@0 | 1319 | |
michael@0 | 1320 | _getAllUserMedia(constraintsList, index + 1); |
michael@0 | 1321 | }, generateErrorCallback()); |
michael@0 | 1322 | } else { |
michael@0 | 1323 | onSuccess(); |
michael@0 | 1324 | } |
michael@0 | 1325 | } |
michael@0 | 1326 | |
michael@0 | 1327 | info("Get " + this.constraints.length + " local streams"); |
michael@0 | 1328 | _getAllUserMedia(this.constraints, 0); |
michael@0 | 1329 | }, |
michael@0 | 1330 | |
michael@0 | 1331 | /** |
michael@0 | 1332 | * Create a new data channel instance |
michael@0 | 1333 | * |
michael@0 | 1334 | * @param {Object} options |
michael@0 | 1335 | * Options which get forwarded to nsIPeerConnection.createDataChannel |
michael@0 | 1336 | * @param {function} [onCreation=undefined] |
michael@0 | 1337 | * Callback to execute when the local data channel has been created |
michael@0 | 1338 | * @returns {DataChannelWrapper} The created data channel |
michael@0 | 1339 | */ |
michael@0 | 1340 | createDataChannel : function PCW_createDataChannel(options, onCreation) { |
michael@0 | 1341 | var label = 'channel_' + this.dataChannels.length; |
michael@0 | 1342 | info(this + ": Create data channel '" + label); |
michael@0 | 1343 | |
michael@0 | 1344 | var channel = this._pc.createDataChannel(label, options); |
michael@0 | 1345 | var wrapper = new DataChannelWrapper(channel, this); |
michael@0 | 1346 | |
michael@0 | 1347 | if (onCreation) { |
michael@0 | 1348 | wrapper.onopen = function () { |
michael@0 | 1349 | onCreation(wrapper); |
michael@0 | 1350 | }; |
michael@0 | 1351 | } |
michael@0 | 1352 | |
michael@0 | 1353 | this.dataChannels.push(wrapper); |
michael@0 | 1354 | return wrapper; |
michael@0 | 1355 | }, |
michael@0 | 1356 | |
michael@0 | 1357 | /** |
michael@0 | 1358 | * Creates an offer and automatically handles the failure case. |
michael@0 | 1359 | * |
michael@0 | 1360 | * @param {function} onSuccess |
michael@0 | 1361 | * Callback to execute if the offer was created successfully |
michael@0 | 1362 | */ |
michael@0 | 1363 | createOffer : function PCW_createOffer(onSuccess) { |
michael@0 | 1364 | var self = this; |
michael@0 | 1365 | |
michael@0 | 1366 | this._pc.createOffer(function (offer) { |
michael@0 | 1367 | info("Got offer: " + JSON.stringify(offer)); |
michael@0 | 1368 | self._last_offer = offer; |
michael@0 | 1369 | onSuccess(offer); |
michael@0 | 1370 | }, generateErrorCallback(), this.offerConstraints); |
michael@0 | 1371 | }, |
michael@0 | 1372 | |
michael@0 | 1373 | /** |
michael@0 | 1374 | * Creates an answer and automatically handles the failure case. |
michael@0 | 1375 | * |
michael@0 | 1376 | * @param {function} onSuccess |
michael@0 | 1377 | * Callback to execute if the answer was created successfully |
michael@0 | 1378 | */ |
michael@0 | 1379 | createAnswer : function PCW_createAnswer(onSuccess) { |
michael@0 | 1380 | var self = this; |
michael@0 | 1381 | |
michael@0 | 1382 | this._pc.createAnswer(function (answer) { |
michael@0 | 1383 | info(self + ": Got answer: " + JSON.stringify(answer)); |
michael@0 | 1384 | self._last_answer = answer; |
michael@0 | 1385 | onSuccess(answer); |
michael@0 | 1386 | }, generateErrorCallback()); |
michael@0 | 1387 | }, |
michael@0 | 1388 | |
michael@0 | 1389 | /** |
michael@0 | 1390 | * Sets the local description and automatically handles the failure case. |
michael@0 | 1391 | * |
michael@0 | 1392 | * @param {object} desc |
michael@0 | 1393 | * mozRTCSessionDescription for the local description request |
michael@0 | 1394 | * @param {function} onSuccess |
michael@0 | 1395 | * Callback to execute if the local description was set successfully |
michael@0 | 1396 | */ |
michael@0 | 1397 | setLocalDescription : function PCW_setLocalDescription(desc, onSuccess) { |
michael@0 | 1398 | var self = this; |
michael@0 | 1399 | this._pc.setLocalDescription(desc, function () { |
michael@0 | 1400 | info(self + ": Successfully set the local description"); |
michael@0 | 1401 | onSuccess(); |
michael@0 | 1402 | }, generateErrorCallback()); |
michael@0 | 1403 | }, |
michael@0 | 1404 | |
michael@0 | 1405 | /** |
michael@0 | 1406 | * Tries to set the local description and expect failure. Automatically |
michael@0 | 1407 | * causes the test case to fail if the call succeeds. |
michael@0 | 1408 | * |
michael@0 | 1409 | * @param {object} desc |
michael@0 | 1410 | * mozRTCSessionDescription for the local description request |
michael@0 | 1411 | * @param {function} onFailure |
michael@0 | 1412 | * Callback to execute if the call fails. |
michael@0 | 1413 | */ |
michael@0 | 1414 | setLocalDescriptionAndFail : function PCW_setLocalDescriptionAndFail(desc, onFailure) { |
michael@0 | 1415 | var self = this; |
michael@0 | 1416 | this._pc.setLocalDescription(desc, |
michael@0 | 1417 | generateErrorCallback("setLocalDescription should have failed."), |
michael@0 | 1418 | function (err) { |
michael@0 | 1419 | info(self + ": As expected, failed to set the local description"); |
michael@0 | 1420 | onFailure(err); |
michael@0 | 1421 | }); |
michael@0 | 1422 | }, |
michael@0 | 1423 | |
michael@0 | 1424 | /** |
michael@0 | 1425 | * Sets the remote description and automatically handles the failure case. |
michael@0 | 1426 | * |
michael@0 | 1427 | * @param {object} desc |
michael@0 | 1428 | * mozRTCSessionDescription for the remote description request |
michael@0 | 1429 | * @param {function} onSuccess |
michael@0 | 1430 | * Callback to execute if the remote description was set successfully |
michael@0 | 1431 | */ |
michael@0 | 1432 | setRemoteDescription : function PCW_setRemoteDescription(desc, onSuccess) { |
michael@0 | 1433 | var self = this; |
michael@0 | 1434 | this._pc.setRemoteDescription(desc, function () { |
michael@0 | 1435 | info(self + ": Successfully set remote description"); |
michael@0 | 1436 | onSuccess(); |
michael@0 | 1437 | }, generateErrorCallback()); |
michael@0 | 1438 | }, |
michael@0 | 1439 | |
michael@0 | 1440 | /** |
michael@0 | 1441 | * Tries to set the remote description and expect failure. Automatically |
michael@0 | 1442 | * causes the test case to fail if the call succeeds. |
michael@0 | 1443 | * |
michael@0 | 1444 | * @param {object} desc |
michael@0 | 1445 | * mozRTCSessionDescription for the remote description request |
michael@0 | 1446 | * @param {function} onFailure |
michael@0 | 1447 | * Callback to execute if the call fails. |
michael@0 | 1448 | */ |
michael@0 | 1449 | setRemoteDescriptionAndFail : function PCW_setRemoteDescriptionAndFail(desc, onFailure) { |
michael@0 | 1450 | var self = this; |
michael@0 | 1451 | this._pc.setRemoteDescription(desc, |
michael@0 | 1452 | generateErrorCallback("setRemoteDescription should have failed."), |
michael@0 | 1453 | function (err) { |
michael@0 | 1454 | info(self + ": As expected, failed to set the remote description"); |
michael@0 | 1455 | onFailure(err); |
michael@0 | 1456 | }); |
michael@0 | 1457 | }, |
michael@0 | 1458 | |
michael@0 | 1459 | /** |
michael@0 | 1460 | * Adds an ICE candidate and automatically handles the failure case. |
michael@0 | 1461 | * |
michael@0 | 1462 | * @param {object} candidate |
michael@0 | 1463 | * SDP candidate |
michael@0 | 1464 | * @param {function} onSuccess |
michael@0 | 1465 | * Callback to execute if the local description was set successfully |
michael@0 | 1466 | */ |
michael@0 | 1467 | addIceCandidate : function PCW_addIceCandidate(candidate, onSuccess) { |
michael@0 | 1468 | var self = this; |
michael@0 | 1469 | |
michael@0 | 1470 | this._pc.addIceCandidate(candidate, function () { |
michael@0 | 1471 | info(self + ": Successfully added an ICE candidate"); |
michael@0 | 1472 | onSuccess(); |
michael@0 | 1473 | }, generateErrorCallback()); |
michael@0 | 1474 | }, |
michael@0 | 1475 | |
michael@0 | 1476 | /** |
michael@0 | 1477 | * Tries to add an ICE candidate and expects failure. Automatically |
michael@0 | 1478 | * causes the test case to fail if the call succeeds. |
michael@0 | 1479 | * |
michael@0 | 1480 | * @param {object} candidate |
michael@0 | 1481 | * SDP candidate |
michael@0 | 1482 | * @param {function} onFailure |
michael@0 | 1483 | * Callback to execute if the call fails. |
michael@0 | 1484 | */ |
michael@0 | 1485 | addIceCandidateAndFail : function PCW_addIceCandidateAndFail(candidate, onFailure) { |
michael@0 | 1486 | var self = this; |
michael@0 | 1487 | |
michael@0 | 1488 | this._pc.addIceCandidate(candidate, |
michael@0 | 1489 | generateErrorCallback("addIceCandidate should have failed."), |
michael@0 | 1490 | function (err) { |
michael@0 | 1491 | info(self + ": As expected, failed to add an ICE candidate"); |
michael@0 | 1492 | onFailure(err); |
michael@0 | 1493 | }) ; |
michael@0 | 1494 | }, |
michael@0 | 1495 | |
michael@0 | 1496 | /** |
michael@0 | 1497 | * Returns if the ICE the connection state is "connected". |
michael@0 | 1498 | * |
michael@0 | 1499 | * @returns {boolean} True if the connection state is "connected", otherwise false. |
michael@0 | 1500 | */ |
michael@0 | 1501 | isIceConnected : function PCW_isIceConnected() { |
michael@0 | 1502 | info("iceConnectionState: " + this.iceConnectionState); |
michael@0 | 1503 | return this.iceConnectionState === "connected"; |
michael@0 | 1504 | }, |
michael@0 | 1505 | |
michael@0 | 1506 | /** |
michael@0 | 1507 | * Returns if the ICE the connection state is "checking". |
michael@0 | 1508 | * |
michael@0 | 1509 | * @returns {boolean} True if the connection state is "checking", otherwise false. |
michael@0 | 1510 | */ |
michael@0 | 1511 | isIceChecking : function PCW_isIceChecking() { |
michael@0 | 1512 | return this.iceConnectionState === "checking"; |
michael@0 | 1513 | }, |
michael@0 | 1514 | |
michael@0 | 1515 | /** |
michael@0 | 1516 | * Returns if the ICE the connection state is "new". |
michael@0 | 1517 | * |
michael@0 | 1518 | * @returns {boolean} True if the connection state is "new", otherwise false. |
michael@0 | 1519 | */ |
michael@0 | 1520 | isIceNew : function PCW_isIceNew() { |
michael@0 | 1521 | return this.iceConnectionState === "new"; |
michael@0 | 1522 | }, |
michael@0 | 1523 | |
michael@0 | 1524 | /** |
michael@0 | 1525 | * Checks if the ICE connection state still waits for a connection to get |
michael@0 | 1526 | * established. |
michael@0 | 1527 | * |
michael@0 | 1528 | * @returns {boolean} True if the connection state is "checking" or "new", |
michael@0 | 1529 | * otherwise false. |
michael@0 | 1530 | */ |
michael@0 | 1531 | isIceConnectionPending : function PCW_isIceConnectionPending() { |
michael@0 | 1532 | return (this.isIceChecking() || this.isIceNew()); |
michael@0 | 1533 | }, |
michael@0 | 1534 | |
michael@0 | 1535 | /** |
michael@0 | 1536 | * Registers a callback for the ICE connection state change and |
michael@0 | 1537 | * reports success (=connected) or failure via the callbacks. |
michael@0 | 1538 | * States "new" and "checking" are ignored. |
michael@0 | 1539 | * |
michael@0 | 1540 | * @param {function} onSuccess |
michael@0 | 1541 | * Callback if ICE connection status is "connected". |
michael@0 | 1542 | * @param {function} onFailure |
michael@0 | 1543 | * Callback if ICE connection reaches a different state than |
michael@0 | 1544 | * "new", "checking" or "connected". |
michael@0 | 1545 | */ |
michael@0 | 1546 | waitForIceConnected : function PCW_waitForIceConnected(onSuccess, onFailure) { |
michael@0 | 1547 | var self = this; |
michael@0 | 1548 | var mySuccess = onSuccess; |
michael@0 | 1549 | var myFailure = onFailure; |
michael@0 | 1550 | |
michael@0 | 1551 | function iceConnectedChanged () { |
michael@0 | 1552 | if (self.isIceConnected()) { |
michael@0 | 1553 | delete self.ice_connection_callbacks.waitForIceConnected; |
michael@0 | 1554 | mySuccess(); |
michael@0 | 1555 | } else if (! self.isIceConnectionPending()) { |
michael@0 | 1556 | delete self.ice_connection_callbacks.waitForIceConnected; |
michael@0 | 1557 | myFailure(); |
michael@0 | 1558 | } |
michael@0 | 1559 | } |
michael@0 | 1560 | |
michael@0 | 1561 | self.ice_connection_callbacks.waitForIceConnected = iceConnectedChanged; |
michael@0 | 1562 | }, |
michael@0 | 1563 | |
michael@0 | 1564 | /** |
michael@0 | 1565 | * Checks that we are getting the media streams we expect. |
michael@0 | 1566 | * |
michael@0 | 1567 | * @param {object} constraintsRemote |
michael@0 | 1568 | * The media constraints of the remote peer connection object |
michael@0 | 1569 | */ |
michael@0 | 1570 | checkMediaStreams : function PCW_checkMediaStreams(constraintsRemote) { |
michael@0 | 1571 | is(this._pc.getLocalStreams().length, this.constraints.length, |
michael@0 | 1572 | this + ' has ' + this.constraints.length + ' local streams'); |
michael@0 | 1573 | |
michael@0 | 1574 | // TODO: change this when multiple incoming streams are supported (bug 834835) |
michael@0 | 1575 | is(this._pc.getRemoteStreams().length, 1, |
michael@0 | 1576 | this + ' has ' + 1 + ' remote streams'); |
michael@0 | 1577 | }, |
michael@0 | 1578 | |
michael@0 | 1579 | /** |
michael@0 | 1580 | * Check that media flow is present on all media elements involved in this |
michael@0 | 1581 | * test by waiting for confirmation that media flow is present. |
michael@0 | 1582 | * |
michael@0 | 1583 | * @param {Function} onSuccess the success callback when media flow |
michael@0 | 1584 | * is confirmed on all media elements |
michael@0 | 1585 | */ |
michael@0 | 1586 | checkMediaFlowPresent : function PCW_checkMediaFlowPresent(onSuccess) { |
michael@0 | 1587 | var self = this; |
michael@0 | 1588 | |
michael@0 | 1589 | function _checkMediaFlowPresent(index, onSuccess) { |
michael@0 | 1590 | if(index >= self.mediaCheckers.length) { |
michael@0 | 1591 | onSuccess(); |
michael@0 | 1592 | } else { |
michael@0 | 1593 | var mediaChecker = self.mediaCheckers[index]; |
michael@0 | 1594 | mediaChecker.waitForMediaFlow(function() { |
michael@0 | 1595 | _checkMediaFlowPresent(index + 1, onSuccess); |
michael@0 | 1596 | }); |
michael@0 | 1597 | } |
michael@0 | 1598 | } |
michael@0 | 1599 | |
michael@0 | 1600 | _checkMediaFlowPresent(0, onSuccess); |
michael@0 | 1601 | }, |
michael@0 | 1602 | |
michael@0 | 1603 | /** |
michael@0 | 1604 | * Check that stats are present by checking for known stats. |
michael@0 | 1605 | * |
michael@0 | 1606 | * @param {Function} onSuccess the success callback to return stats to |
michael@0 | 1607 | */ |
michael@0 | 1608 | getStats : function PCW_getStats(selector, onSuccess) { |
michael@0 | 1609 | var self = this; |
michael@0 | 1610 | |
michael@0 | 1611 | this._pc.getStats(selector, function(stats) { |
michael@0 | 1612 | info(self + ": Got stats: " + JSON.stringify(stats)); |
michael@0 | 1613 | self._last_stats = stats; |
michael@0 | 1614 | onSuccess(stats); |
michael@0 | 1615 | }, generateErrorCallback()); |
michael@0 | 1616 | }, |
michael@0 | 1617 | |
michael@0 | 1618 | /** |
michael@0 | 1619 | * Checks that we are getting the media streams we expect. |
michael@0 | 1620 | * |
michael@0 | 1621 | * @param {object} stats |
michael@0 | 1622 | * The stats to check from this PeerConnectionWrapper |
michael@0 | 1623 | */ |
michael@0 | 1624 | checkStats : function PCW_checkStats(stats) { |
michael@0 | 1625 | function toNum(obj) { |
michael@0 | 1626 | return obj? obj : 0; |
michael@0 | 1627 | } |
michael@0 | 1628 | function numTracks(streams) { |
michael@0 | 1629 | var n = 0; |
michael@0 | 1630 | streams.forEach(function(stream) { |
michael@0 | 1631 | n += stream.getAudioTracks().length + stream.getVideoTracks().length; |
michael@0 | 1632 | }); |
michael@0 | 1633 | return n; |
michael@0 | 1634 | } |
michael@0 | 1635 | |
michael@0 | 1636 | const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1; |
michael@0 | 1637 | |
michael@0 | 1638 | // Use spec way of enumerating stats |
michael@0 | 1639 | var counters = {}; |
michael@0 | 1640 | for (var key in stats) { |
michael@0 | 1641 | if (stats.hasOwnProperty(key)) { |
michael@0 | 1642 | var res = stats[key]; |
michael@0 | 1643 | // validate stats |
michael@0 | 1644 | ok(res.id == key, "Coherent stats id"); |
michael@0 | 1645 | var nowish = Date.now() + 1000; // TODO: clock drift observed |
michael@0 | 1646 | var minimum = this.whenCreated - 1000; // on Windows XP (Bug 979649) |
michael@0 | 1647 | if (isWinXP) { |
michael@0 | 1648 | todo(false, "Can't reliably test rtcp timestamps on WinXP (Bug 979649)"); |
michael@0 | 1649 | } else { |
michael@0 | 1650 | ok(res.timestamp >= minimum, |
michael@0 | 1651 | "Valid " + (res.isRemote? "rtcp" : "rtp") + " timestamp " + |
michael@0 | 1652 | res.timestamp + " >= " + minimum + " (" + |
michael@0 | 1653 | (res.timestamp - minimum) + " ms)"); |
michael@0 | 1654 | ok(res.timestamp <= nowish, |
michael@0 | 1655 | "Valid " + (res.isRemote? "rtcp" : "rtp") + " timestamp " + |
michael@0 | 1656 | res.timestamp + " <= " + nowish + " (" + |
michael@0 | 1657 | (res.timestamp - nowish) + " ms)"); |
michael@0 | 1658 | } |
michael@0 | 1659 | if (!res.isRemote) { |
michael@0 | 1660 | counters[res.type] = toNum(counters[res.type]) + 1; |
michael@0 | 1661 | |
michael@0 | 1662 | switch (res.type) { |
michael@0 | 1663 | case "inboundrtp": |
michael@0 | 1664 | case "outboundrtp": { |
michael@0 | 1665 | // ssrc is a 32 bit number returned as a string by spec |
michael@0 | 1666 | ok(res.ssrc.length > 0, "Ssrc has length"); |
michael@0 | 1667 | ok(res.ssrc.length < 11, "Ssrc not lengthy"); |
michael@0 | 1668 | ok(!/[^0-9]/.test(res.ssrc), "Ssrc numeric"); |
michael@0 | 1669 | ok(parseInt(res.ssrc) < Math.pow(2,32), "Ssrc within limits"); |
michael@0 | 1670 | |
michael@0 | 1671 | if (res.type == "outboundrtp") { |
michael@0 | 1672 | ok(res.packetsSent !== undefined, "Rtp packetsSent"); |
michael@0 | 1673 | // minimum fragment is 8 (from RFC 791) |
michael@0 | 1674 | ok(res.bytesSent >= res.packetsSent * 8, "Rtp bytesSent"); |
michael@0 | 1675 | } else { |
michael@0 | 1676 | ok(res.packetsReceived !== undefined, "Rtp packetsReceived"); |
michael@0 | 1677 | ok(res.bytesReceived >= res.packetsReceived * 8, "Rtp bytesReceived"); |
michael@0 | 1678 | } |
michael@0 | 1679 | if (res.remoteId) { |
michael@0 | 1680 | var rem = stats[res.remoteId]; |
michael@0 | 1681 | ok(rem.isRemote, "Remote is rtcp"); |
michael@0 | 1682 | ok(rem.remoteId == res.id, "Remote backlink match"); |
michael@0 | 1683 | if(res.type == "outboundrtp") { |
michael@0 | 1684 | ok(rem.type == "inboundrtp", "Rtcp is inbound"); |
michael@0 | 1685 | ok(rem.packetsReceived !== undefined, "Rtcp packetsReceived"); |
michael@0 | 1686 | ok(rem.packetsReceived <= res.packetsSent, "No more than sent"); |
michael@0 | 1687 | ok(rem.packetsLost !== undefined, "Rtcp packetsLost"); |
michael@0 | 1688 | ok(rem.bytesReceived >= rem.packetsReceived * 8, "Rtcp bytesReceived"); |
michael@0 | 1689 | ok(rem.bytesReceived <= res.bytesSent, "No more than sent bytes"); |
michael@0 | 1690 | ok(rem.jitter !== undefined, "Rtcp jitter"); |
michael@0 | 1691 | ok(rem.mozRtt !== undefined, "Rtcp rtt"); |
michael@0 | 1692 | ok(rem.mozRtt >= 0, "Rtcp rtt " + rem.mozRtt + " >= 0"); |
michael@0 | 1693 | ok(rem.mozRtt < 60000, "Rtcp rtt " + rem.mozRtt + " < 1 min"); |
michael@0 | 1694 | } else { |
michael@0 | 1695 | ok(rem.type == "outboundrtp", "Rtcp is outbound"); |
michael@0 | 1696 | ok(rem.packetsSent !== undefined, "Rtcp packetsSent"); |
michael@0 | 1697 | // We may have received more than outdated Rtcp packetsSent |
michael@0 | 1698 | ok(rem.bytesSent >= rem.packetsSent * 8, "Rtcp bytesSent"); |
michael@0 | 1699 | } |
michael@0 | 1700 | ok(rem.ssrc == res.ssrc, "Remote ssrc match"); |
michael@0 | 1701 | } else { |
michael@0 | 1702 | info("No rtcp info received yet"); |
michael@0 | 1703 | } |
michael@0 | 1704 | } |
michael@0 | 1705 | break; |
michael@0 | 1706 | } |
michael@0 | 1707 | } |
michael@0 | 1708 | } |
michael@0 | 1709 | } |
michael@0 | 1710 | |
michael@0 | 1711 | // Use MapClass way of enumerating stats |
michael@0 | 1712 | var counters2 = {}; |
michael@0 | 1713 | stats.forEach(function(res) { |
michael@0 | 1714 | if (!res.isRemote) { |
michael@0 | 1715 | counters2[res.type] = toNum(counters2[res.type]) + 1; |
michael@0 | 1716 | } |
michael@0 | 1717 | }); |
michael@0 | 1718 | is(JSON.stringify(counters), JSON.stringify(counters2), |
michael@0 | 1719 | "Spec and MapClass variant of RTCStatsReport enumeration agree"); |
michael@0 | 1720 | var nin = numTracks(this._pc.getRemoteStreams()); |
michael@0 | 1721 | var nout = numTracks(this._pc.getLocalStreams()); |
michael@0 | 1722 | |
michael@0 | 1723 | // TODO(Bug 957145): Restore stronger inboundrtp test once Bug 948249 is fixed |
michael@0 | 1724 | //is(toNum(counters["inboundrtp"]), nin, "Have " + nin + " inboundrtp stat(s)"); |
michael@0 | 1725 | ok(toNum(counters.inboundrtp) >= nin, "Have at least " + nin + " inboundrtp stat(s) *"); |
michael@0 | 1726 | |
michael@0 | 1727 | is(toNum(counters.outboundrtp), nout, "Have " + nout + " outboundrtp stat(s)"); |
michael@0 | 1728 | |
michael@0 | 1729 | var numLocalCandidates = toNum(counters.localcandidate); |
michael@0 | 1730 | var numRemoteCandidates = toNum(counters.remotecandidate); |
michael@0 | 1731 | // If there are no tracks, there will be no stats either. |
michael@0 | 1732 | if (nin + nout > 0) { |
michael@0 | 1733 | ok(numLocalCandidates, "Have localcandidate stat(s)"); |
michael@0 | 1734 | ok(numRemoteCandidates, "Have remotecandidate stat(s)"); |
michael@0 | 1735 | } else { |
michael@0 | 1736 | is(numLocalCandidates, 0, "Have no localcandidate stats"); |
michael@0 | 1737 | is(numRemoteCandidates, 0, "Have no remotecandidate stats"); |
michael@0 | 1738 | } |
michael@0 | 1739 | }, |
michael@0 | 1740 | |
michael@0 | 1741 | /** |
michael@0 | 1742 | * Closes the connection |
michael@0 | 1743 | */ |
michael@0 | 1744 | close : function PCW_close() { |
michael@0 | 1745 | // It might be that a test has already closed the pc. In those cases |
michael@0 | 1746 | // we should not fail. |
michael@0 | 1747 | try { |
michael@0 | 1748 | this._pc.close(); |
michael@0 | 1749 | info(this + ": Closed connection."); |
michael@0 | 1750 | } |
michael@0 | 1751 | catch (e) { |
michael@0 | 1752 | info(this + ": Failure in closing connection - " + e.message); |
michael@0 | 1753 | } |
michael@0 | 1754 | }, |
michael@0 | 1755 | |
michael@0 | 1756 | /** |
michael@0 | 1757 | * Register all events during the setup of the data channel |
michael@0 | 1758 | * |
michael@0 | 1759 | * @param {Function} onDataChannelOpened |
michael@0 | 1760 | * Callback to execute when the data channel has been opened |
michael@0 | 1761 | */ |
michael@0 | 1762 | registerDataChannelOpenEvents : function (onDataChannelOpened) { |
michael@0 | 1763 | info(this + ": Register callbacks for 'ondatachannel' and 'onopen'"); |
michael@0 | 1764 | |
michael@0 | 1765 | this.ondatachannel = function (targetChannel) { |
michael@0 | 1766 | targetChannel.onopen = function (targetChannel) { |
michael@0 | 1767 | onDataChannelOpened(targetChannel); |
michael@0 | 1768 | }; |
michael@0 | 1769 | |
michael@0 | 1770 | this.dataChannels.push(targetChannel); |
michael@0 | 1771 | }; |
michael@0 | 1772 | }, |
michael@0 | 1773 | |
michael@0 | 1774 | /** |
michael@0 | 1775 | * Returns the string representation of the class |
michael@0 | 1776 | * |
michael@0 | 1777 | * @returns {String} The string representation |
michael@0 | 1778 | */ |
michael@0 | 1779 | toString : function PCW_toString() { |
michael@0 | 1780 | return "PeerConnectionWrapper (" + this.label + ")"; |
michael@0 | 1781 | } |
michael@0 | 1782 | }; |