Bug 1355220 add RTCRtpSender/Receiver.getStats;r?jib draft
authorNico Grunbaum
Thu, 20 Apr 2017 17:54:20 -0700
changeset 582726 801293e6c43bdbb685856af538c734c42795a44f
parent 582725 c2692d96a53e462c01c882810712e0b2afeba378
child 629834 3d554e8a334b9580a3ea569c30c9ab0256a14a53
push id60153
push userna-g@nostrum.com
push dateTue, 23 May 2017 01:17:28 +0000
reviewersjib
bugs1355220
milestone55.0a1
Bug 1355220 add RTCRtpSender/Receiver.getStats;r?jib MozReview-Commit-ID: LZ4ItjFYxmk
dom/media/PeerConnection.js
dom/media/tests/mochitest/mochitest.ini
dom/media/tests/mochitest/pc.js
dom/media/tests/mochitest/test_peerConnection_sender_and_receiver_stats.html
dom/webidl/RTCRtpReceiver.webidl
dom/webidl/RTCRtpSender.webidl
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -1528,17 +1528,17 @@ class PeerConnectionObserver {
   onRemoveStream(stream) {
     this.dispatchEvent(new this._dompc._win.MediaStreamEvent("removestream",
                                                              { stream }));
   }
 
   onAddTrack(track, streams) {
     let pc = this._dompc;
     let receiver = pc._win.RTCRtpReceiver._create(pc._win,
-                                                  new RTCRtpReceiver(this,
+                                                  new RTCRtpReceiver(pc,
                                                                      track));
     pc._receivers.push(receiver);
     let ev = new pc._win.RTCTrackEvent("track", { receiver, track, streams });
     this.dispatchEvent(ev);
 
     // Fire legacy event as well for a little bit.
     ev = new pc._win.MediaStreamTrackEvent("addtrack", { track });
     this.dispatchEvent(ev);
@@ -1660,27 +1660,37 @@ class RTCRtpSender {
   setParameters(parameters) {
     return this._pc._win.Promise.resolve()
       .then(() => this._pc._setParameters(this, parameters));
   }
 
   getParameters() {
     return this._pc._getParameters(this);
   }
+
+  getStats() {
+    return this._pc._async(
+      async () => this._pc._getStats(this.track));
+  }
 }
 setupPrototype(RTCRtpSender, {
   classID: PC_SENDER_CID,
   contractID: PC_SENDER_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports])
 });
 
 class RTCRtpReceiver {
   constructor(pc, track) {
     Object.assign(this, { _pc: pc, track });
   }
+
+  getStats() {
+    return this._pc._async(
+      async () => this._pc.getStats(this.track));
+  }
 }
 setupPrototype(RTCRtpReceiver, {
   classID: PC_RECEIVER_CID,
   contractID: PC_RECEIVER_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports])
 });
 
 class CreateOfferRequest {
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -282,8 +282,10 @@ skip-if = (android_version == '18') # an
 skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_threeUnbundledConnections.html]
 skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_selftest.html]
 # Bug 1227781: Crash with bogus TURN server.
 [test_peerConnection_bug1227781.html]
 [test_peerConnection_stats.html]
 skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_sender_and_receiver_stats.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
--- a/dom/media/tests/mochitest/pc.js
+++ b/dom/media/tests/mochitest/pc.js
@@ -813,16 +813,33 @@ function PeerConnectionWrapper(label, co
     this.dataChannels.push(wrapper);
   });
 
   createOneShotEventWrapper(this, this._pc, 'signalingstatechange');
   createOneShotEventWrapper(this, this._pc, 'negotiationneeded');
 }
 
 PeerConnectionWrapper.prototype = {
+  /**
+   * Returns the senders
+   *
+   * @returns {sequence<RTCRtpSender>} the senders
+   */
+  getSenders: function() {
+    return this._pc.getSenders();
+  },
+
+  /**
+   * Returns the getters
+   *
+   * @returns {sequence<RTCRtpReceiver>} the receivers
+   */
+  getReceivers: function() {
+    return this._pc.getReceivers();
+  },
 
   /**
    * Returns the local description.
    *
    * @returns {object} The local description
    */
   get localDescription() {
     return this._pc.localDescription;
@@ -1476,16 +1493,57 @@ PeerConnectionWrapper.prototype = {
       Object.keys(this.expectedRemoteTrackInfoById)
           .map(id => this.remoteMediaElements
               .find(e => e.srcObject.getTracks().some(t => t.id == id)))
           .map(e => this.waitForMediaElementFlow(e)),
       this._pc.getSenders().map(sender => this.waitForRtpFlow(sender.track)),
       this._pc.getReceivers().map(receiver => this.waitForRtpFlow(receiver.track))));
   },
 
+  async waitForSyncedRtcp() {
+    // Ensures that RTCP is present
+    let ensureSyncedRtcp = async () => {
+      let report = await this._pc.getStats();
+      for (let [k, v] of report) {
+        if (v.type.endsWith("bound-rtp") && !v.remoteId) {
+          info(v.id + " is missing remoteId: " + JSON.stringify(v));
+          return null;
+        }
+        if (v.type == "inbound-rtp" && v.isRemote == true
+            && v.roundTripTime === undefined) {
+          info(v.id + " is missing roundTripTime: " + JSON.stringify(v));
+          return null;
+        }
+      }
+      return report;
+    }
+    let attempts = 0;
+    // Time-units are MS
+    const waitPeriod = 500;
+    const maxTime = 15000;
+    for (let totalTime = maxTime; totalTime > 0; totalTime -= waitPeriod) {
+      try {
+        let syncedStats = await ensureSyncedRtcp();
+        if (syncedStats) {
+          return syncedStats;
+        }
+      } catch (e) {
+          info(e);
+          info(e.stack);
+          throw e;
+      }
+      attempts += 1;
+      info("waitForSyncedRtcp: no synced RTCP on attempt" + attempts
+           + ", retrying.\n");
+      await wait(waitPeriod);
+    }
+    throw Error("Waiting for synced RTCP timed out after at least "
+                + maxTime + "ms");
+  },
+
   /**
    * Check that correct audio (typically a flat tone) is flowing to this
    * PeerConnection. Uses WebAudio AnalyserNodes to compare input and output
    * audio data in the frequency domain.
    *
    * @param {object} from
    *        A PeerConnectionWrapper whose audio RTPSender we use as source for
    *        the audio flow check.
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_sender_and_receiver_stats.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "1355220",
+    title: "RTCRtpSender.getStats() and RTCRtpReceiver.getStats()",
+    visible: true
+  });
+
+  var test;
+
+  var checkStats = (sndReport, rcvReport, mediaType) => {
+    // Returns SSRCs and checks that the tracks are of the correct mediaType
+    let getSsrcs = (report, kind) => {
+      return [...report.values()]
+        .filter(stat => stat.type.endsWith("bound-rtp")).map(stat =>{
+          is(stat.mediaType, kind, "mediaType of " + stat.id
+              + " is expected type " + kind);
+          return stat.ssrc;
+      }).sort().join("|");
+    };
+    let sndSsrcs = getSsrcs(sndReport, mediaType);
+    let rcvSsrcs = getSsrcs(rcvReport, mediaType);
+    ok(sndSsrcs, "sender SSRCs is not empty");
+    ok(rcvSsrcs, "receiver SSRCs is not empty");
+    is(sndSsrcs, rcvSsrcs, "sender SSRCs match receiver SSRCs");
+  };
+
+  // This MUST be run after PC_*_WAIT_FOR_MEDIA_FLOW to ensure that we have RTP
+  // before checking for RTCP.
+  // It will throw UnsyncedRtcpError if it times out waiting for sync.
+
+  var test;
+  runNetworkTest(function (options) {
+    test = new PeerConnectionTest(options);
+    test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+      async function PC_LOCAL_AND_REMOTE_CHECK_SENDER_RECEIVER_STATS(test) {
+        return Promise.all([test.pcLocal.waitForSyncedRtcp(),
+                  test.pcRemote.waitForSyncedRtcp()])
+          .then(async () => {
+            let senders = test.pcLocal.getSenders();
+            let receivers = test.pcRemote.getReceivers();
+            is(senders.length, 2, "Have exactly two senders.");
+            is(receivers.length, 2, "Have exactly two receivers.");
+            for(let kind of ["audio", "video"]) {
+              let senderStats =
+                  await senders.find(s => s.track.kind == kind).getStats();
+              is(senders.filter(s => s.track.kind == kind).length, 1,
+                  "Exactly 1 sender of kind " + kind);
+              let receiverStats =
+                  await receivers.find(r => r.track.kind == kind).getStats();
+              is(receivers.filter(r => r.track.kind == kind).length, 1,
+                  "Exactly 1 receiver of kind " + kind);
+
+              checkStats(senderStats, receiverStats, kind);
+            }
+          })
+        });
+    test.setMediaConstraints([{audio: true}, {video: true}], []);
+    test.run();
+  });
+
+</script>
+</pre>
+</body>
+</html>
\ No newline at end of file
--- a/dom/webidl/RTCRtpReceiver.webidl
+++ b/dom/webidl/RTCRtpReceiver.webidl
@@ -6,9 +6,10 @@
  * The origin of this IDL file is
  * http://lists.w3.org/Archives/Public/public-webrtc/2014May/0067.html
  */
 
 [Pref="media.peerconnection.enabled",
  JSImplementation="@mozilla.org/dom/rtpreceiver;1"]
 interface RTCRtpReceiver {
   readonly attribute MediaStreamTrack track;
+  Promise<RTCStatsReport> getStats();
 };
--- a/dom/webidl/RTCRtpSender.webidl
+++ b/dom/webidl/RTCRtpSender.webidl
@@ -68,11 +68,12 @@ dictionary RTCRtpParameters {
 
 [Pref="media.peerconnection.enabled",
  JSImplementation="@mozilla.org/dom/rtpsender;1"]
 interface RTCRtpSender {
   readonly attribute MediaStreamTrack track;
   Promise<void> setParameters (optional RTCRtpParameters parameters);
   RTCRtpParameters getParameters();
   Promise<void> replaceTrack(MediaStreamTrack track);
+  Promise<RTCStatsReport> getStats();
   [Pref="media.peerconnection.dtmf.enabled"]
   readonly attribute RTCDTMFSender? dtmf;
 };