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
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/. */
5 "use strict";
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;
22 this._commands = commandList || [ ];
23 this._current = 0;
25 this.onFinished = null;
26 }
28 CommandChain.prototype = {
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 },
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 },
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 },
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 },
67 /**
68 * Execute the next command in the chain.
69 */
70 executeNext : function () {
71 var self = this;
73 function _executeNext() {
74 if (!self.finished) {
75 var step = self._commands[self._current];
76 self._current++;
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 }
87 // To prevent building up the stack we have to execute the next
88 // step asynchronously
89 window.setTimeout(_executeNext, 0);
90 },
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 },
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 }
116 return -1;
117 },
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);
130 if (index > -1) {
131 var tail = this.removeAfter(id);
133 this.append(commands);
134 this.append(tail);
135 }
136 },
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);
149 if (index > -1) {
150 var tail = this.removeAfter(id);
151 var object = this.remove(id);
153 this.append(commands);
154 this.append(object);
155 this.append(tail);
156 }
157 },
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 },
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);
180 if (index > -1) {
181 return this._commands.splice(index + 1);
182 }
184 return null;
185 },
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);
197 if (index > -1) {
198 return this._commands.splice(0, index);
199 }
201 return null;
202 },
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);
215 return oldCommands;
216 },
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);
229 return oldCommands;
230 },
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 };
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;
262 var self = this;
263 var elementId = self.element.getAttribute('id');
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 };
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);
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 };
291 element.addEventListener('canplaythrough', canPlayThroughCallback, false);
292 element.addEventListener('timeupdate', timeUpdateCallback, false);
293 }
295 MediaElementChecker.prototype = {
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);
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 },
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 };
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);
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 }
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);
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 };
406 return utils;
407 }
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;
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 });
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 }
455 if (options.is_local)
456 this.pcLocal = new PeerConnectionWrapper('pcLocal', options.config_pc1);
457 else
458 this.pcLocal = null;
460 if (options.is_remote)
461 this.pcRemote = new PeerConnectionWrapper('pcRemote', options.config_pc2 || options.config_pc1);
462 else
463 this.pcRemote = null;
465 this.connected = false;
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 }
476 // Insert network teardown after testcase execution.
477 if (netTeardownCommand) {
478 this.chain.append(netTeardownCommand);
479 }
481 var self = this;
482 this.chain.onFinished = function () {
483 self.teardown();
484 };
485 }
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);
496 function signalingstatechangeClose(state) {
497 info("'onsignalingstatechange' event '" + state + "' received");
498 is(state, "closed", "onsignalingstatechange event is closed");
499 }
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;
513 onSuccess();
514 };
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 };
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 };
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 };
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 };
570 PeerConnectionTest.prototype.setIdentityProvider =
571 function(peer, provider, protocol, identity) {
572 peer.setIdentityProvider(provider, protocol, identity);
573 };
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;
591 function check_next_test() {
592 if (eventFired && stateChanged) {
593 onSuccess();
594 }
595 }
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 };
610 peer.setLocalDescription(desc, function () {
611 stateChanged = true;
612 check_next_test();
613 });
614 };
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 };
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 };
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;
658 function check_next_test() {
659 if (eventFired && stateChanged) {
660 onSuccess();
661 }
662 }
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 };
676 peer.setRemoteDescription(desc, function () {
677 stateChanged = true;
678 check_next_test();
679 });
680 };
682 /**
683 * Start running the tests as assigned to the command chain.
684 */
685 PeerConnectionTest.prototype.run = function PCT_run() {
686 this.next();
687 };
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 };
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;
720 PeerConnectionTest.call(this, options);
721 }
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;
734 function _closeChannels() {
735 var length = self.pcLocal.dataChannels.length;
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 }
747 _closeChannels();
748 }
749 },
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];
764 var self = this;
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);
771 onSuccess(remoteChannel);
772 };
774 localChannel.close();
775 this.pcLocal.dataChannels.splice(index, 1);
776 }
777 },
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;
793 // Method to synchronize all asynchronous events.
794 function check_next_test() {
795 if (self.connected && localChannel && remoteChannel) {
796 onSuccess(localChannel, remoteChannel);
797 }
798 }
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 }
808 // Create the datachannel and handle the local 'onopen' event
809 this.pcLocal.createDataChannel(options, function (channel) {
810 localChannel = channel;
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 },
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];
848 // Register event handler for the target channel
849 target.onmessage = function (recv_data) {
850 onSuccess(target, recv_data);
851 };
853 source.send(data);
854 }
855 },
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 }
882 }
883 },
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;
899 var targetPeer = peer;
900 var targetChannel = null;
902 var sourcePeer = (peer == this.pcLocal) ? this.pcRemote : this.pcLocal;
903 var sourceChannel = null;
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 }
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 };
920 // Register handlers for the target peer
921 targetPeer.registerDataChannelOpenEvents(function (channel) {
922 targetChannel = channel;
923 check_next_test();
924 });
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 });
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;
948 info("Creating " + this);
950 /**
951 * Setup appropriate callbacks
952 */
954 this.onclose = unexpectedEventAndFinish(this, 'onclose');
955 this.onerror = unexpectedEventAndFinish(this, 'onerror');
956 this.onmessage = unexpectedEventAndFinish(this, 'onmessage');
957 this.onopen = unexpectedEventAndFinish(this, 'onopen');
959 var self = this;
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");
969 self.onclose(self);
970 self.onclose = unexpectedEventAndFinish(self, 'onclose');
971 };
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 + "'");
984 self.onmessage(event.data);
985 self.onmessage = unexpectedEventAndFinish(self, 'onmessage');
986 };
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");
996 self.onopen(self);
997 self.onopen = unexpectedEventAndFinish(self, 'onopen');
998 };
999 }
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 },
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 },
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 },
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 },
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 },
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 },
1057 // ordered, maxRetransmits and maxRetransmitTime not exposed yet
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 },
1068 /**
1069 * Close the data channel
1070 */
1071 close : function () {
1072 info(this + ": Closing channel");
1073 this._channel.close();
1074 },
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 },
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 };
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();
1112 this.constraints = [ ];
1113 this.offerConstraints = {};
1114 this.streams = [ ];
1115 this.mediaCheckers = [ ];
1117 this.dataChannels = [ ];
1119 info("Creating " + this);
1120 this._pc = new mozRTCPeerConnection(this.configuration);
1121 is(this._pc.iceConnectionState, "new", "iceConnectionState starts at 'new'");
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 = {};
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');
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));
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 };
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);
1177 self.ondatachannel(new DataChannelWrapper(event.channel, self));
1178 self.ondatachannel = unexpectedEventAndFinish(self, 'ondatachannel');
1179 };
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");
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 }
1199 PeerConnectionWrapper.prototype = {
1201 /**
1202 * Returns the local description.
1203 *
1204 * @returns {object} The local description
1205 */
1206 get localDescription() {
1207 return this._pc.localDescription;
1208 },
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 },
1220 /**
1221 * Returns the readyState.
1222 *
1223 * @returns {string}
1224 */
1225 get readyState() {
1226 return this._pc.readyState;
1227 },
1229 /**
1230 * Returns the remote description.
1231 *
1232 * @returns {object} The remote description
1233 */
1234 get remoteDescription() {
1235 return this._pc.remoteDescription;
1236 },
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 },
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 },
1265 setIdentityProvider: function(provider, protocol, identity) {
1266 this._pc.setIdentityProvider(provider, protocol, identity);
1267 },
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);
1284 if (side === 'local') {
1285 this._pc.addStream(stream);
1286 }
1288 var element = createMediaElement(type, this.label + '_' + side);
1289 this.mediaCheckers.push(new MediaElementChecker(element));
1290 element.mozSrcObject = stream;
1291 element.play();
1292 },
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;
1303 function _getAllUserMedia(constraintsList, index) {
1304 if (index < constraintsList.length) {
1305 var constraints = constraintsList[index];
1307 getUserMedia(constraints, function (stream) {
1308 var type = '';
1310 if (constraints.audio) {
1311 type = 'audio';
1312 }
1314 if (constraints.video) {
1315 type += 'video';
1316 }
1318 self.attachMedia(stream, type, 'local');
1320 _getAllUserMedia(constraintsList, index + 1);
1321 }, generateErrorCallback());
1322 } else {
1323 onSuccess();
1324 }
1325 }
1327 info("Get " + this.constraints.length + " local streams");
1328 _getAllUserMedia(this.constraints, 0);
1329 },
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);
1344 var channel = this._pc.createDataChannel(label, options);
1345 var wrapper = new DataChannelWrapper(channel, this);
1347 if (onCreation) {
1348 wrapper.onopen = function () {
1349 onCreation(wrapper);
1350 };
1351 }
1353 this.dataChannels.push(wrapper);
1354 return wrapper;
1355 },
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;
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 },
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;
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 },
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 },
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 },
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 },
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 },
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;
1470 this._pc.addIceCandidate(candidate, function () {
1471 info(self + ": Successfully added an ICE candidate");
1472 onSuccess();
1473 }, generateErrorCallback());
1474 },
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;
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 },
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 },
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 },
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 },
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 },
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;
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 }
1561 self.ice_connection_callbacks.waitForIceConnected = iceConnectedChanged;
1562 },
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');
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 },
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;
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 }
1600 _checkMediaFlowPresent(0, onSuccess);
1601 },
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;
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 },
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 }
1636 const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
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;
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");
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 }
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());
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) *");
1727 is(toNum(counters.outboundrtp), nout, "Have " + nout + " outboundrtp stat(s)");
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 },
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 },
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'");
1765 this.ondatachannel = function (targetChannel) {
1766 targetChannel.onopen = function (targetChannel) {
1767 onDataChannelOpened(targetChannel);
1768 };
1770 this.dataChannels.push(targetChannel);
1771 };
1772 },
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 };