DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Mercurial (c68fe15a81fc)

VCS Links

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345
<!doctype html>
<meta charset=utf-8>
<title>RTCPeerConnection.prototype.getStats</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="RTCPeerConnection-helper.js"></script>
<script src="dictionary-helper.js"></script>
<script src="RTCStats-helper.js"></script>
<script>
  'use strict';

  // Test is based on the following editor draft:
  // webrtc-pc 20171130
  // webrtc-stats 20171122

  // The following helper function is called from RTCPeerConnection-helper.js
  //   getTrackFromUserMedia

  // The following helper function is called from RTCStats-helper.js
  //   validateStatsReport
  //   assert_stats_report_has_stats

  // The following helper function is called from RTCPeerConnection-helper.js
  //   exchangeIceCandidates
  //   exchangeOfferAnswer

  /*
    8.2.  getStats
      1.  Let selectorArg be the method's first argument.
      2.  Let connection be the RTCPeerConnection object on which the method was invoked.
      3.  If selectorArg is null, let selector be null.
      4.  If selectorArg is a MediaStreamTrack let selector be an RTCRtpSender
          or RTCRtpReceiver on connection which track member matches selectorArg.
          If no such sender or receiver exists, or if more than one sender or
          receiver fit this criteria, return a promise rejected with a newly
          created InvalidAccessError.
      5.  Let p be a new promise.
      6.  Run the following steps in parallel:
        1.  Gather the stats indicated by selector according to the stats selection algorithm.
        2.  Resolve p with the resulting RTCStatsReport object, containing the gathered stats.
   */
  promise_test(t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());
    return pc.getStats();
  }, 'getStats() with no argument should succeed');

  promise_test(t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());
    return pc.getStats(null);
  }, 'getStats(null) should succeed');

  /*
    8.2.  getStats
      4.  If selectorArg is a MediaStreamTrack let selector be an RTCRtpSender
          or RTCRtpReceiver on connection which track member matches selectorArg.
          If no such sender or receiver exists, or if more than one sender or
          receiver fit this criteria, return a promise rejected with a newly
          created InvalidAccessError.
   */
  promise_test(t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());
    return getTrackFromUserMedia('audio')
    .then(([track, mediaStream]) => {
      return promise_rejects_dom(t, 'InvalidAccessError', pc.getStats(track));
    });
  }, 'getStats() with track not added to connection should reject with InvalidAccessError');

  promise_test(t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());
    return getTrackFromUserMedia('audio')
    .then(([track, mediaStream]) => {
      pc.addTrack(track, mediaStream);
      return pc.getStats(track);
    });
  }, 'getStats() with track added via addTrack should succeed');

  promise_test(async t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
    const [track] = stream.getTracks();
    pc.addTransceiver(track);

    return pc.getStats(track);
  }, 'getStats() with track added via addTransceiver should succeed');

  promise_test(t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());
    return getTrackFromUserMedia('audio')
    .then(([track, mediaStream]) => {
      // addTransceiver allows adding same track multiple times
      const transceiver1 = pc.addTransceiver(track);
      const transceiver2 = pc.addTransceiver(track);

      assert_not_equals(transceiver1, transceiver2);
      assert_not_equals(transceiver1.sender, transceiver2.sender);
      assert_equals(transceiver1.sender.track, transceiver2.sender.track);

      return promise_rejects_dom(t, 'InvalidAccessError', pc.getStats(track));
    });
  }, `getStats() with track associated with more than one sender should reject with InvalidAccessError`);

  promise_test(t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());
    const transceiver1 = pc.addTransceiver('audio');

    // Create another transceiver that resends what
    // is being received, kind of like echo
    const transceiver2 = pc.addTransceiver(transceiver1.receiver.track);
    assert_equals(transceiver1.receiver.track, transceiver2.sender.track);

    return promise_rejects_dom(t, 'InvalidAccessError', pc.getStats(transceiver1.receiver.track));
  }, 'getStats() with track associated with both sender and receiver should reject with InvalidAccessError');

  /*
    8.5.  The stats selection algorithm
      2.  If selector is null, gather stats for the whole connection, add them to result,
          return result, and abort these steps.
   */
  promise_test(t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());
    return pc.getStats()
    .then(statsReport => {
      validateStatsReport(statsReport);
      assert_stats_report_has_stats(statsReport, ['peer-connection']);
    });
  }, 'getStats() with no argument should return stats report containing peer-connection stats on an empty PC');

  promise_test(t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());
    return getTrackFromUserMedia('audio')
    .then(([track, mediaStream]) => {
      pc.addTrack(track, mediaStream);
      return pc.getStats();
    })
    .then(statsReport => {
//      validateStatsReport(statsReport);
//      assert_stats_report_has_stats(statsReport, ['peer-connection']);
      assert_stats_report_has_stats(statsReport, ['outbound-rtp']);
    });
  }, 'getStats() with no argument should return stats report containing peer-connection stats and outbound-track-stats');

  promise_test(t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());
    return getTrackFromUserMedia('audio')
    .then(([track, mediaStream]) => {
      pc.addTrack(track);
      return pc.getStats();
    })
    .then(statsReport => {
      validateStatsReport(statsReport);
      assert_stats_report_has_stats(statsReport, ['peer-connection']);
      assert_stats_report_has_stats(statsReport, ['outbound-rtp']);
    });
  }, 'getStats() with no argument should return stats for no-stream tracks');

  /*
    8.5.  The stats selection algorithm
      3.  If selector is an RTCRtpSender, gather stats for and add the following objects
          to result:
        - All RTCOutboundRTPStreamStats objects corresponding to selector.
        - All stats objects referenced directly or indirectly by the RTCOutboundRTPStreamStats
          objects added.
  */
  promise_test(async t => {
    const pc = createPeerConnectionWithCleanup(t);
    const pc2 = createPeerConnectionWithCleanup(t);

    let [track, mediaStream] = await getTrackFromUserMedia('audio');
    pc.addTrack(track, mediaStream);
    exchangeIceCandidates(pc, pc2);
    await exchangeOfferAnswer(pc, pc2);
    await listenToIceConnected(pc);
    const stats = await pc.getStats(track);
    validateStatsReport(stats);
    assert_stats_report_has_stats(stats, ['outbound-rtp']);
  }, `getStats() on track associated with RtpSender should return stats report containing outbound-rtp stats`);


  /*
    8.5.  The stats selection algorithm
      4.  If selector is an RTCRtpReceiver, gather stats for and add the following objects
          to result:
        - All RTCInboundRTPStreamStats objects corresponding to selector.
        - All stats objects referenced directly or indirectly by the RTCInboundRTPStreamStats
          added.
   */
  promise_test(async t => {
    const pc = createPeerConnectionWithCleanup(t);
    const pc2 = createPeerConnectionWithCleanup(t);

    let [track, mediaStream] = await getTrackFromUserMedia('audio');
    pc.addTrack(track, mediaStream);
    exchangeIceCandidates(pc, pc2);
    await exchangeOfferAnswer(pc, pc2);
    // Wait for unmute if the track is not already unmuted.
    // According to spec, it should be muted when being created, but this
    // is not what this test is testing, so allow it to be unmuted.
    if (pc2.getReceivers()[0].track.muted) {
      await new Promise(resolve => {
        pc2.getReceivers()[0].track.addEventListener('unmute', resolve);
      });
    }
    const stats = await pc2.getStats(pc2.getReceivers()[0].track);
    validateStatsReport(stats);
    assert_stats_report_has_stats(stats, ['inbound-rtp']);
  }, `getStats() on track associated with RtpReceiver should return stats report containing inbound-rtp stats`);

  /*
    8.6   Mandatory To Implement Stats
      An implementation MUST support generating statistics of the following types
      when the corresponding objects exist on a PeerConnection, with the attributes
      that are listed when they are valid for that object.
   */

  const mandatoryStats = [
    "codec",
    "inbound-rtp",
    "outbound-rtp",
    "remote-inbound-rtp",
    "remote-outbound-rtp",
    "peer-connection",
    "data-channel",
    "transport",
    "candidate-pair",
    "local-candidate",
    "remote-candidate",
    "certificate"
  ];

  async_test(t => {
    const pc1 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc2.close());

    const dataChannel = pc1.createDataChannel('test-channel');

    return getNoiseStream({
      audio: true,
      video: true
    })
    .then(t.step_func(mediaStream => {
      const tracks = mediaStream.getTracks();
      const [audioTrack] = mediaStream.getAudioTracks();
      const [videoTrack] = mediaStream.getVideoTracks();

      for (const track of mediaStream.getTracks()) {
        t.add_cleanup(() => track.stop());
        pc1.addTrack(track, mediaStream);
      }

      const testStatsReport = (pc, statsReport) => {
        validateStatsReport(statsReport);
        assert_stats_report_has_stats(statsReport, mandatoryStats);

        const dataChannelStats = findStatsFromReport(statsReport,
          stats => {
            return stats.type === 'data-channel' &&
              stats.dataChannelIdentifier === dataChannel.id;
          },
          'Expect data channel stats to be found');

        assert_equals(dataChannelStats.label, 'test-channel');

        /* TODO track stats are obsolete - replace with sender/receiver? */
        const audioTrackStats = findStatsFromReport(statsReport,
          stats => {
            return stats.type === 'track' &&
              stats.trackIdentifier === audioTrack.id;
          },
          'Expect audio track stats to be found');

        assert_equals(audioTrackStats.kind, 'audio');

        const videoTrackStats = findStatsFromReport(statsReport,
          stats => {
            return stats.type === 'track' &&
              stats.trackIdentifier === videoTrack.id;
          },
          'Expect video track stats to be found');

        assert_equals(videoTrackStats.kind, 'video');

        assert_true(mediaStreamStats.trackIds.include(audioTrackStats.id));
        assert_true(mediaStreamStats.trackIds.include(videoTrackStats.id));
      }

      const onConnected = t.step_func(() => {
        // Wait a while for the peer connections to collect stats
        t.step_timeout(() => {
          Promise.all([
            /* TODO: for both pc1 and pc2 to expose all mandatory stats, they need to both send/receive tracks and data channels */
            pc1.getStats()
            .then(statsReport => testStatsReport(pc1, statsReport)),

            pc2.getStats()
            .then(statsReport => testStatsReport(pc2, statsReport))
          ])
          .then(t.step_func_done())
          .catch(t.step_func(err => {
            assert_unreached(`test failed with error: ${err}`);
          }));
        }, 200)
      })

      let onTrackCount = 0
      let onDataChannelCalled = false

      pc2.addEventListener('track', t.step_func(() => {
        onTrackCount++;
        if (onTrackCount === 2 && onDataChannelCalled) {
          onConnected();
        }
      }));

      pc2.addEventListener('datachannel', t.step_func(() => {
        onDataChannelCalled = true;
        if (onTrackCount === 2) {
          onConnected();
        }
      }));


      exchangeIceCandidates(pc1, pc2);
      exchangeOfferAnswer(pc1, pc2);
    }))
    .catch(t.step_func(err => {
      assert_unreached(`test failed with error: ${err}`);
    }));

  }, `getStats() with connected peer connections having tracks and data channel should return all mandatory to implement stats`);

</script>