Bug 831789 - Enhance existing peer connection mochitests to check for media flow. r=jesup
☠☠ backed out by a116d4a86539 ☠ ☠
authorJason Smith <jsmith@mozilla.com>
Sun, 26 May 2013 20:32:28 -0400
changeset 145766 84fb317ea1d2195a6246e94b40500b21ec1a37b2
parent 145765 dfcabf3d307a6f71b1dd26a21a62793666d67a21
child 145767 baf7e13ccedf7e92adff434f86f0ed63294248c8
push id2697
push userbbajaj@mozilla.com
push dateMon, 05 Aug 2013 18:49:53 +0000
treeherdermozilla-beta@dfec938c7b63 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjesup
bugs831789
milestone24.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 831789 - Enhance existing peer connection mochitests to check for media flow. r=jesup
dom/media/tests/mochitest/head.js
dom/media/tests/mochitest/pc.js
dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveAudio.html
dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveVideo.html
dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveVideoAudio.html
--- a/dom/media/tests/mochitest/head.js
+++ b/dom/media/tests/mochitest/head.js
@@ -185,8 +185,23 @@ function unexpectedCallbackAndFinish(err
       ok(false, "Unexpected error callback from " + where + " with name = '" +
                 aObj.name + "', message = '" + aObj.message + "'");
     } else {
       ok(false, "Unexpected error callback from " + where + " with " + aObj);
     }
     SimpleTest.finish();
   }
 }
+
+/**
+ * Generates a callback function fired only for unexpected events happening.
+ *
+ * @param {String} description a description of what the event fired on
+ * @param {String} eventName the name of the unexpected event
+ */
+function unexpectedEventAndFinish(description, eventName) {
+  return function () {
+    var e = new Error();
+    ok(false, "Unexpected event '" + eventName + "' fired for " + description +
+       " " + e.stack.split("\n")[1]);
+    SimpleTest.finish();
+  }
+}
--- a/dom/media/tests/mochitest/pc.js
+++ b/dom/media/tests/mochitest/pc.js
@@ -290,32 +290,130 @@ var commandsPeerConnection = [
     'PC_REMOTE_SET_LOCAL_DESCRIPTION',
     function (test) {
       test.pcRemote.setLocalDescription(test.pcRemote._last_answer, function () {
         test.next();
       });
     }
   ],
   [
-    'PC_LOCAL_CHECK_MEDIA',
+    'PC_LOCAL_CHECK_MEDIA_STREAMS',
     function (test) {
-      test.pcLocal.checkMedia(test.pcRemote.constraints);
+      test.pcLocal.checkMediaStreams(test.pcRemote.constraints);
+      test.next();
+    }
+  ],
+  [
+    'PC_REMOTE_CHECK_MEDIA_STREAMS',
+    function (test) {
+      test.pcRemote.checkMediaStreams(test.pcLocal.constraints);
       test.next();
     }
   ],
   [
-    'PC_REMOTE_CHECK_MEDIA',
+    'PC_LOCAL_CHECK_MEDIA_FLOW_PRESENT',
     function (test) {
-      test.pcRemote.checkMedia(test.pcLocal.constraints);
-      test.next();
+      test.pcLocal.checkMediaFlowPresent(function () {
+        test.next();
+      });
+    }
+  ],
+  [
+    'PC_REMOTE_CHECK_MEDIA_FLOW_PRESENT',
+    function (test) {
+      test.pcRemote.checkMediaFlowPresent(function () {
+        test.next();
+      });
     }
   ]
 ];
 
 /**
+ * This class provides a state checker for media elements which store
+ * a media stream to check for media attribute state and events fired.
+ * When constructed by a caller, an object instance is created with
+ * a media element, event state checkers for canplaythrough, timeupdate, and
+ * time changing on the media element and stream.
+ *
+ * @param {HTMLMediaElement} element the media element being analyzed
+ */
+function MediaElementChecker(element) {
+  this.element = element;
+  this.canPlayThroughFired = false;
+  this.timeUpdateFired = false;
+  this.timePassed = false;
+
+  var self = this;
+  var elementId = self.element.getAttribute('id');
+
+  // When canplaythrough fires, we track that it's fired and remove the
+  // event listener.
+  var canPlayThroughCallback = function() {
+    info('canplaythrough fired for media element ' + elementId);
+    self.canPlayThroughFired = true;
+    self.element.removeEventListener('canplaythrough', canPlayThroughCallback,
+                                     false);
+  };
+
+  // When timeupdate fires, we track that it's fired and check if time
+  // has passed on the media stream and media element.
+  var timeUpdateCallback = function() {
+    self.timeUpdateFired = true;
+    info('timeupdate fired for media element ' + elementId);
+
+    // If time has passed, then track that and remove the timeupdate event
+    // listener.
+    if(element.mozSrcObject && element.mozSrcObject.currentTime > 0 &&
+       element.currentTime > 0) {
+      info('time passed for media element ' + elementId);
+      self.timePassed = true;
+      self.element.removeEventListener('timeupdate', timeUpdateCallback,
+                                       false);
+    }
+  };
+
+  element.addEventListener('canplaythrough', canPlayThroughCallback, false);
+  element.addEventListener('timeupdate', timeUpdateCallback, false);
+}
+
+MediaElementChecker.prototype = {
+
+  /**
+   * Waits until the canplaythrough & timeupdate events to fire along with
+   * ensuring time has passed on the stream and media element.
+   *
+   * @param {Function} onSuccess the success callback when media flow is
+   *                             established
+   */
+  waitForMediaFlow : function MEC_WaitForMediaFlow(onSuccess) {
+    var self = this;
+    var elementId = self.element.getAttribute('id');
+    info('Analyzing element: ' + elementId);
+
+    if(self.canPlayThroughFired && self.timeUpdateFired && self.timePassed) {
+      ok(true, 'Media flowing for ' + elementId);
+      onSuccess();
+    } else {
+      setTimeout(function() {
+        self.waitForMediaFlow(onSuccess);
+      }, 100);
+    }
+  },
+
+  /**
+   * Checks if there is no media flow present by checking that the ready
+   * state of the media element is HAVE_METADATA.
+   */
+  checkForNoMediaFlow : function MEC_CheckForNoMediaFlow() {
+    ok(this.element.readyState === HTMLMediaElement.HAVE_METADATA,
+       'Media element has a ready state of HAVE_METADATA');
+  }
+};
+
+/**
  * This class handles tests for peer connections.
  *
  * @constructor
  * @param {object} [options={}]
  *        Optional options for the peer connection test
  * @param {object} [options.config_pc1=undefined]
  *        Configuration for the local peer connection instance
  * @param {object} [options.config_pc2=undefined]
@@ -406,24 +504,30 @@ PeerConnectionTest.prototype.teardown = 
  */
 function PeerConnectionWrapper(label, configuration) {
   this.configuration = configuration;
   this.label = label;
 
   this.constraints = [ ];
   this.offerConstraints = {};
   this.streams = [ ];
+  this.mediaCheckers = [ ];
+
+  this.onaddstream = unexpectedEventAndFinish(this.label, 'onaddstream');
 
   info("Creating new PeerConnectionWrapper: " + this.label);
   this._pc = new mozRTCPeerConnection(this.configuration);
 
   var self = this;
   this._pc.onaddstream = function (event) {
-    // Bug 834835: Assume type is video until we get get{Audio,Video}Tracks.
-    self.attachMedia(event.stream, 'video', 'remote');
+    info(this.label + ': onaddstream event fired');
+
+    self.onaddstream(event.stream);
+    self.onaddstream = unexpectedEventAndFinish(this.label,
+      'onaddstream');
   };
 }
 
 PeerConnectionWrapper.prototype = {
 
   /**
    * Returns the local description.
    *
@@ -477,16 +581,17 @@ PeerConnectionWrapper.prototype = {
     info("Got media stream: " + type + " (" + side + ")");
     this.streams.push(stream);
 
     if (side === 'local') {
       this._pc.addStream(stream);
     }
 
     var element = createMediaElement(type, this._label + '_' + side);
+    this.mediaCheckers.push(new MediaElementChecker(element));
     element.mozSrcObject = stream;
     element.play();
   },
 
   /**
    * Requests all the media streams as specified in the constrains property.
    *
    * @param {function} onSuccess
@@ -567,47 +672,103 @@ PeerConnectionWrapper.prototype = {
     var self = this;
     this._pc.setLocalDescription(desc, function () {
       info("Successfully set the local description for " + self.label);
       onSuccess();
     }, unexpectedCallbackAndFinish(new Error));
   },
 
   /**
-   * Sets the remote description and automatically handles the failure case.
+   * Sets the remote description on the peer connection object & waits
+   * for a resulting onaddstream callback to fire.
    *
    * @param {object} desc
    *        mozRTCSessionDescription for the remote description request
    * @param {function} onSuccess
    *        Callback to execute if the remote description was set successfully
    */
   setRemoteDescription : function PCW_setRemoteDescription(desc, onSuccess) {
     var self = this;
+    var onAddStreamFired = false;
+    var setRemoteDescriptionFinished = false;
+
+    // Fires onSuccess when both onaddstream and setRemoteDescription
+    // have finished.
+    function isFinished() {
+      if(onAddStreamFired && setRemoteDescriptionFinished) {
+        onSuccess();
+      }
+    }
+
+    // Sets up the provided stream in a media element when onaddstream fires.
+    this.onaddstream = function(stream) {
+      // bug 834835: Assume type is video until we get media tracks
+      self.attachMedia(stream, 'video', 'remote');
+      onAddStreamFired = true;
+      isFinished();
+    };
+
     this._pc.setRemoteDescription(desc, function () {
       info("Successfully set remote description for " + self.label);
-      onSuccess();
+      setRemoteDescriptionFinished = true;
+      isFinished();
     }, unexpectedCallbackAndFinish(new Error));
   },
 
   /**
-   * Checks that we are getting the media we expect.
+   * Checks that we are getting the media streams we expect.
    *
    * @param {object} constraintsRemote
    *        The media constraints of the remote peer connection object
    */
-  checkMedia : function PCW_checkMedia(constraintsRemote) {
+  checkMediaStreams : function PCW_checkMediaStreams(constraintsRemote) {
     is(this._pc.localStreams.length, this.constraints.length,
        this.label + ' has ' + this.constraints.length + ' local streams');
 
     // TODO: change this when multiple incoming streams are allowed.
     is(this._pc.remoteStreams.length, 1,
        this.label + ' has ' + 1 + ' remote streams');
   },
 
   /**
+   * Check that media flow is present on all media elements involved in this
+   * test by waiting for confirmation that media flow is present.
+   *
+   * @param {Function} onSuccess the success callback when media flow
+   *                             is confirmed on all media elements
+   */
+  checkMediaFlowPresent : function PCW_checkMediaFlowPresent(onSuccess) {
+    var self = this;
+
+    function _checkMediaFlowPresent(index, onSuccess) {
+      if(index >= self.mediaCheckers.length) {
+        onSuccess();
+      } else {
+        var mediaChecker = self.mediaCheckers[index];
+        mediaChecker.waitForMediaFlow(function() {
+          _checkMediaFlowPresent(index + 1, onSuccess);
+        });
+      }
+    }
+
+    _checkMediaFlowPresent(0, onSuccess);
+  },
+
+  /**
+   * Checks that media flow is not present on all media elements involved in
+   * this test by ensuring that the ready state is either HAVE_NOTHING or
+   * HAVE_METADATA.
+   */
+  checkMediaFlowNotPresent : function PCW_checkMediaFlowNotPresent() {
+    for(var mediaChecker of this.mediaCheckers) {
+      mediaChecker.checkForNoMediaFlow();
+    }
+  },
+
+  /**
    * Closes the connection
    */
   close : function PCW_close() {
     // It might be that a test has already closed the pc. In those cases
     // we should not fail.
     try {
       this._pc.close();
       info(this.label + ": Closed connection.");
--- a/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveAudio.html
+++ b/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveAudio.html
@@ -9,17 +9,30 @@
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "850275",
     title: "Simple offer media constraint test with audio"
   });
 
+  var steps = [
+   [
+      "CHECK_MEDIA_FLOW_NOT_PRESENT",
+      function (test) {
+        test.pcLocal.checkMediaFlowNotPresent();
+        test.pcRemote.checkMediaFlowNotPresent();
+        test.next();
+      }
+    ]
+  ];
+
   runTest(function() {
     var test = new PeerConnectionTest();
     test.setOfferConstraints({ mandatory: { OfferToReceiveAudio: true } });
+    test.chain.removeAfter('PC_REMOTE_CHECK_MEDIA_STREAMS');
+    test.chain.append(steps);
     test.run();
   });
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveVideo.html
+++ b/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveVideo.html
@@ -9,17 +9,30 @@
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "850275",
     title: "Simple offer media constraint test with video"
   });
 
+  var steps = [
+   [
+      "CHECK_MEDIA_FLOW_NOT_PRESENT",
+      function (test) {
+        test.pcLocal.checkMediaFlowNotPresent();
+        test.pcRemote.checkMediaFlowNotPresent();
+        test.next();
+      }
+    ]
+  ];
+
   runTest(function() {
     var test = new PeerConnectionTest();
     test.setOfferConstraints({ mandatory: { OfferToReceiveVideo: true } });
+    test.chain.removeAfter('PC_REMOTE_CHECK_MEDIA_STREAMS');
+    test.chain.append(steps);
     test.run();
   });
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveVideoAudio.html
+++ b/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveVideoAudio.html
@@ -9,20 +9,33 @@
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "850275",
     title: "Simple offer media constraint test with video/audio"
   });
 
+  var steps = [
+   [
+      "CHECK_MEDIA_FLOW_NOT_PRESENT",
+      function (test) {
+        test.pcLocal.checkMediaFlowNotPresent();
+        test.pcRemote.checkMediaFlowNotPresent();
+        test.next();
+      }
+    ]
+  ];
+
   runTest(function() {
     var test = new PeerConnectionTest();
     test.setOfferConstraints({ mandatory: {
       OfferToReceiveVideo: true,
       OfferToReceiveAudio: true
     }});
+    test.chain.removeAfter('PC_REMOTE_CHECK_MEDIA_STREAMS');
+    test.chain.append(steps);
     test.run();
   });
 </script>
 </pre>
 </body>
 </html>