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