dom/media/tests/mochitest/pc.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

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
  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
  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
  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
  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
  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
  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
  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
  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
  1090    * @returns {String} The string representation
  1091    */
  1092   toString: function DCW_toString() {
  1093     return "DataChannelWrapper (" + this._pc.label + '_' + this._channel.label + ")";
  1095 };
  1098 /**
  1099  * This class acts as a wrapper around a PeerConnection instance.
  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 = "";
  1143   };
  1144   this.ondatachannel = unexpectedEventAndFinish(this, 'ondatachannel');
  1145   this.onsignalingstatechange = unexpectedEventAndFinish(this, 'onsignalingstatechange');
  1147   /**
  1148    * Callback for native peer connection 'onaddstream' events.
  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';
  1160     if (event.stream.getVideoTracks().length > 0) {
  1161       type += 'video';
  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.
  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.
  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   };
  1199 PeerConnectionWrapper.prototype = {
  1201   /**
  1202    * Returns the local description.
  1204    * @returns {object} The local description
  1205    */
  1206   get localDescription() {
  1207     return this._pc.localDescription;
  1208   },
  1210   /**
  1211    * Sets the local description.
  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.
  1223    * @returns {string}
  1224    */
  1225   get readyState() {
  1226     return this._pc.readyState;
  1227   },
  1229   /**
  1230    * Returns the remote description.
  1232    * @returns {object} The remote description
  1233    */
  1234   get remoteDescription() {
  1235     return this._pc.remoteDescription;
  1236   },
  1238   /**
  1239    * Sets the remote description.
  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.
  1251    * @returns {object} The local description
  1252    */
  1253   get signalingState() {
  1254     return this._pc.signalingState;
  1255   },
  1256   /**
  1257    * Returns the ICE connection state.
  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.
  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);
  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.
  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';
  1314           if (constraints.video) {
  1315             type += 'video';
  1318           self.attachMedia(stream, type, 'local');
  1320           _getAllUserMedia(constraintsList, index + 1);
  1321         }, generateErrorCallback());
  1322       } else {
  1323         onSuccess();
  1327     info("Get " + this.constraints.length + " local streams");
  1328     _getAllUserMedia(this.constraints, 0);
  1329   },
  1331   /**
  1332    * Create a new data channel instance
  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       };
  1353     this.dataChannels.push(wrapper);
  1354     return wrapper;
  1355   },
  1357   /**
  1358    * Creates an offer and automatically handles the failure case.
  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.
  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.
  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.
  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.
  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.
  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.
  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.
  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".
  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".
  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".
  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.
  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.
  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();
  1561     self.ice_connection_callbacks.waitForIceConnected = iceConnectedChanged;
  1562   },
  1564   /**
  1565    * Checks that we are getting the media streams we expect.
  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.
  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         });
  1600     _checkMediaFlowPresent(0, onSuccess);
  1601   },
  1603   /**
  1604    * Check that stats are present by checking for known stats.
  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.
  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;
  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;
  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)");
  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");
  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");
  1700                 ok(rem.ssrc == res.ssrc, "Remote ssrc match");
  1701               } else {
  1702                 info("No rtcp info received yet");
  1705             break;
  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;
  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");
  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.");
  1751     catch (e) {
  1752       info(this + ": Failure in closing connection - " + e.message);
  1754   },
  1756   /**
  1757    * Register all events during the setup of the data channel
  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
  1777    * @returns {String} The string representation
  1778    */
  1779   toString : function PCW_toString() {
  1780     return "PeerConnectionWrapper (" + this.label + ")";
  1782 };

mercurial